Improving the quality of your C# scripts in Unity can make them integrate better with the editor, improve their readability and make them more maintainable. I’ve put together a list of techniques to use when writing Unity scripts that will improve the quality of your code.

Tl;dr: Use [SerializeField], use enums, use [Header] and [Tooltip], use the context field in Debug calls, use /// comments

1. Limit variable access by using [SerializeField] instead of public fields

Exposing variables to the inspector is very useful, but oftentimes these variables should be exposed to the inspector and to nowhere else. What happens when a field is marked public is that it becomes editable in the inspector window, but also it makes it editable from other scripts. This code example shows how a public field can be misused:

public class Enemy : MonoBehaviour {

public int HP = 100;

}

And then another script can do this:

Enemy unluckyEnemy = GameObject.FindObjectOfType<Enemy>();

unluckyEnemy.HP = 0;

Some poor enemy will be killed and it will be hard to track down what script is killing it. If you’re working on a project alone, you could just remember not to directly modify enemy’s HP like this. If you forget though, or if other people work on the code at some point, it could lead to bugs. The way to expose a variable to the inspector without allowing other scripts to edit it is to apply the “SerializeField” attribute to it, and mark the field as private or protected.

public class Enemy : MonoBehaviour {

[SerializeField]

private int HP = 100;

}

Now only the inspector and the enemy class an access it’s HP.

Not all types can be exposed to the inspector like this, see the docs for exactly what can and cannot be serialized.

2. Prevent bugs by representing discrete states with enums

Let’s say we want the enemy to have three modes it can be in: moving forward, turning, and standing still. To keep track of what state the enemy is in, we could use an int:

public class Enemy : MonoBehaviour {

private int movementMode = 0; void Update()

{

if (movementMode == 0)

{

transform.position +=

transform.forward * speed * Time.deltaTime;

}

else if (movementMode == 1)

{

transform.Rotate(

transform.up * rotationSpeed * Time.deltaTime

);

}

else if (movementMode == 2)

{

// do nothing

}

}

}

The downside of this is that it is cryptic as to what each of the numbers mean. In this example, we can carefully read the code an see what each does, but it will get hard to keep track as the enemy class gets more complex. To make the state values more descriptive, we could use strings:

public class Enemy : MonoBehaviour {

private string movementMode = "walking"; void Update()

{

if (movementMode.Equals("walking"))

{

transform.position +=

transform.forward * speed * Time.deltaTime;

}

else if (movementMode.Equals("turning"))

{

transform.Rotate(

transform.up * rotationSpeed * Time.deltaTime

);

}

else if (movementMode.Equals("standing"))

{

// do nothing

}

}

}

The movementMode variable is now very clear as to what each mode is, as the strings clearly name the modes. The problem now is that it is easy to misspell these mode names in subtle ways:

movementMode = "waking" // misspelled movementMode = "Walking" // capitalized a letter movementMode = "walking " // extra space at the end

These different spellings won’t prevent the code from compiling, but will introduce bugs. When you go to test the game, after setting the mode to a wrong string like “waking” or “Walking”, it will be hard to tell why it is not doing the “walking” behavior.

The type that allows for descriptive value names and prevents misspellings is enums.

public class Enemy : MonoBehaviour {

private enum MovementModes{

WALKING, TURNING, STANDING

}

private MovementModes movementMode = MovementModes.WALKING; void Update()

{

if (movementMode == MovementModes.WALKING)

{

transform.position +=

transform.forward * speed * Time.deltaTime;

}

else if (movementMode == MovementModes.TURNING)

{

transform.Rotate(

transform.up * rotationSpeed * Time.deltaTime

);

}

else if (movementMode == MovementModes.STANDING)

{

// do nothing

}

}

}

The enum values say exactly what the state is, like “WALKING” or “TURNING”, and the code will not compile if you misspell it.

People often learn that enums should always be put in their own script file, but I think that when only one class is using the enum, it is completely acceptable to make the enum private to the class.

Enums can also be exposed to the inspector with the “System.Serializable” attribute.

public class Enemy : MonoBehaviour {

[System.Serializable]

private enum MovementModes{

WALKING, TURNING, STANDING

}



[SerializeField]

private MovementModes movementMode = MovementModes.WALKING;

}

And now the enemy’s movement state can be changed in the inspector.

The enum values can be selected from a dropdown

3. Improve the inspector experience by using [Header] and [Tooltip] attributes

Descriptive variable names oftentimes do not provide enough information about an inspectable field in a script. Many people on the team that use your script components will never go read the code (such as artists, level designers, other programmers even). These people will only interact with your scripts by using the inspector, so no matter how readable your code is, the inspector experience must be enough to explain how to use the component. Adding [Header] attributes allows you to organize the fields, and [Tooltip] attributes let you define some text to appear when hovering over the field. Tooltips are useful for explaining some detail that can’t fit into the variable name, such as the units the variables are measured in.

public class Enemy : MonoBehaviour

{

[Header("Movement")]

[Tooltip("units/second moved when walking")]

[SerializeField] private float WalkSpeed = 5f; [Tooltip("degrees/second turned when turning")]

[SerializeField] private float TurnSpeed = 50f; [Header("Combat")]

[Tooltip("Hit points the enemy spawns with")]

[SerializeField] private float InitialHP = 100f; [Tooltip("How much damage a standard attack does")]

[SerializeField] private float BaseDamage = 10f; [Tooltip("Damage multiplier when landing a critical hit")]

[SerializeField] private float CritMultiplier = 2.0f; [Tooltip("Chance of an attack being a critical hit")]

[SerializeField] private float CritRate = 0.05f;

}

The inspector now has sections of related fields together, and tooltips to explain each field

4. Make debugging easier by including the object reference in Debug.Log calls

Now, let’s say we want to make sure that the movement speed of an enemy is not set to a negative value. We can check the MovementSpeed variable value on Start(), and show a warning if it’s negative. A Debug.LogWarning() call works for this, like in the following code:

public class Enemy : MonoBehaviour

{

[SerializeField]

private float MovementSpeed; private void Start()

{

if (MovementSpeed < 0f)

{

Debug.LogWarning(

"Enemy should not have a negative movement speed"

);

}

}

}

If we have many enemies in the scene, and one has a negative movement speed, we get a warning in the console.

Very helpful, we now know that an enemy has been configured incorrectly and has been given a negative speed. However, when there are many enemies in the scene we have to manually check all of them to find out which one it is.

We can avoid this manual search by using the “context” parameter of Debug calls. The context parameter comes after the debug message parameter and tells the debugger which object the message is coming from.

Debug.LogWarning(

"Enemy should not have a negative movement speed",

this // context parameter

);

With the context parameter in use, clicking on the debug warning message in the console will highlight the object in the hierarchy.

By using the context parameter, clicking on the console log highlights which object it came from

We can even use this when handling exceptions to show which object threw it:

try

{

MethodThatMayThrowException();

}

catch (System.InvalidOperationException opException)

{

// using the context parameter

Debug.LogException(opException, this);

}

5. Make your workflow faster by using /// comments

In Visual Studio, you can type /// above a class or method definition to generate a summary template. Write a brief explanation of the class or method, as well as any parameters and the return type.

/// <summary>

/// A controller that approaches and attacks the player

/// </summary>

public class Enemy : MonoBehaviour

{

private Transform currentTarget; /// <summary>

/// Changes what object the enemy is targeting

/// </summary>

/// <param name="target">The transform of the gameobject the enemy will target</param>

public void TargetObject(Transform target)

{

currentTarget = target;

}

}

Once this is written, you can see these summaries when hovering over a class name or when auto-completing a method call. This saves you time because in the future you won’t have to go back and read the code to remember what your classes and methods are used for.

You can see what that method is for without having to read the script!

I hope these five C# tips have been helpful 💪Go write excellent code!