DEV Community

Cover image for 🐞Bug Resistant Development: Fail Fast – Part 3 (Compile Time Safety)
Rahul Barate
Rahul Barate

Posted on

🐞Bug Resistant Development: Fail Fast – Part 3 (Compile Time Safety)

In the last blog we talked about Data Validation attributes like Min, Range, etc. These attributes help us Fail Fast. Fail Fast means detecting problems early rather than when the game is running.

In this blog we’ll talk about compile-time safety measures.


🤔What are Compile-Time Safety Measures?

Compile-time safety measures are simple tips and techniques that help catch errors during compilation, instead of Unity throwing them at you at runtime.

The earlier an error appears, the easier it is to fix.

Let’s look at some techniques I personally use.


✏️1. Enums > Strings

Now, what does this mean?

In your journey as a Game Developer you must have come across this scenario. Let’s say you have a state system that uses strings like "GameOver", "GameWon", etc. to set or get different states of the game.
Once the game is over you want to display a UI to restart the game, and you wrote the logic something like this:

if(state == "GamesOver")
{ 
    gameOverUI.SetActive(true);
}
else
{
    return;
}
Enter fullscreen mode Exit fullscreen mode

Now, if you have a keen eye then you might have noticed what’s wrong. If you didn’t, then you must have pulled your hair debugging this type of bug.

Just know that a simple spelling mistake might cost you hours of debugging.

But I have a solution — Enums.

Enum is a value type similar to int, float, etc. Enum allows us to define named constants that hold numerical values behind the scenes.

If you are not familiar with enum then check out the official documentation for more details.

Instead of using a string-based state system we can use an enum-based state system.

Take a look at the following example:

public enum State
{
    GameWon,
    GameOver,
    GamePaused
}

public State gameState;

void ReduceHealth(int points)
{
    health -= points;

    if(health <= 0)
        gameState = State.GameOver;
}

void Update()
{
    if(gameState == State.GameOver)
        gameOverUI.SetActive(true);
}
Enter fullscreen mode Exit fullscreen mode

You might be wondering how this is different from a string-based system.

In the string-based system your code will simply keep hitting the else block without telling you what went wrong. But in the enum example, if you mistype a value like State.GameOvr, the compiler (or even your IDE) will immediately show an error.

This saves you hours of debugging by failing fast.


✏️2. Avoid Magic Strings — Use nameof

Yes, this one is also about strings, and this will also save you from debugging for hours because of a simple spelling mistake.

nameof is an expression that returns the name of a variable, type, or method as a string value.

nameof specifically becomes useful when you need to provide the name of a method as a parameter, which opens the room for spelling mistakes.

Take a look at the following example:

// bad
Invoke("ResetGame", 2f); // using string again.

// good
Invoke(nameof(ResetGame), 2f); // using method name directly
Enter fullscreen mode Exit fullscreen mode

The problem with the "bad" example is that if you change the method name and forget to update the string value, then you won’t even know what went wrong.

But if you use nameof, the compiler will throw an error immediately if the method name changes.


✏️3. private > public

This one is simple.

Keep your class members private unless you really need to access them outside the class. This reduces the chances of accidental modifications.

Take a look at the following example:

// bad
public int health = 100;

// good
private int health = 100;
Enter fullscreen mode Exit fullscreen mode

In the "bad" example, any script can access the health variable and change its value.

Imagine your AudioManager having access to the player's health. You tried to change the volume but somehow your player's health goes up and down.

Nightmare scenario, isn’t it?

Keeping members private helps prevent such accidental access and modifications.


✏️4. readonly and const

To stop mutation or modification of values we use readonly or const.

We use const for compile-time constants like int, float, bool, etc. because the value is known at compile time and cannot be changed at runtime.

We can assign the value only during declaration.

We use readonly to stop reassignment of a reference type like List, Dictionary, etc.

With readonly, you can only assign the reference once during declaration or inside the constructor.

We cannot use const with types like List or Dictionary because their values are determined at runtime.

Take a look at the following example:

// bad
private Dictionary<string, int> map = new Dictionary<string, int>();
private int maxHealth = 100;

void Start()
{
    map = new Dictionary<string, int>(); // reassignment
    maxHealth = 80; // easily changed even though it should be fixed
}
Enter fullscreen mode Exit fullscreen mode
// good
private readonly Dictionary<string, int> map = new Dictionary<string, int>();
private const int maxHealth = 100;

void Start()
{
    map = new Dictionary<string, int>(); // will throw error
    maxHealth = 80; // will throw error
}
Enter fullscreen mode Exit fullscreen mode

⚠️Note: readonly prevents reassignment of the reference, but the contents of the object can still be modified.


💡Key Takeaway

Compile Time Safety helps catch problems before the game even runs.

Using enums instead of strings, avoiding magic strings with nameof, keeping members private, and using const or readonly where appropriate can significantly reduce the chances of runtime bugs.

The earlier a bug appears, the easier it is to fix.

Top comments (0)