Writing readable code (tips and guides)

Published October 08, 2020
Advertisement

If you're a seasoned programmer, you might know this already, but it's also likely that you ignore it for the same reason.

I am a huge fan of writing clear, readable code, so in stead of a pure devblog, I wanted to spend some time on what I think should be adopted by every programmer - readability.
We may argue what would be the best way to do certain things, or how the coding standard should be (should the opening brackets be on a new line? :OP )

But keeping code readable is important even if you're a one-man band, because you might come back way later and not remember what the code actually does.
There are several ways of keeping code readable, and here are some of my favorites.

1) Use descriptive names for variables, properties and methods.

For some reason a lot of programmers like to use cryptic names for their variables.
I see a lot of “curH”, “maxH” or “speedMult”. Why not use “currentHealth”, “maxHealth” and “speedMultiplier”?
Yes, there are a few extra letters, but if someone else is reading your code, they don't have to guess what the variable is supposed to be used for. And you don't have to remember what you were thinking back when you first wrote the code.

Use names that can not, in any way, be interpreted as something other than exactly what it is.

private int _currentHealth = 100;
private int _maxHealth = 100;
private bool _isDead = false;

The same goes for methods.
The method name “Damage” is often used in games. Sure, you can accurately guess that the method will cause the character's health to diminish. But can you be absolutely sure? Could it mean something else? Could it be used to change the mesh or something to make the visual representation of the character look more damaged? Well, it could.

By calling the same method “ReduceHealth”, you are more likely to understand exactly what it does, without any guesswork.

2) Use intellisense commenting where possible.

In both cases, I would also make sure to comment the method properly, so that the description shows up in the intellisense, and to make sure you absolutely document what the method is supposed to do.

This is especially helpful when you are calling the method from a different class - you don't have to go back into the code to remember what it does; it's already described in the intellisense.

    /// <summary>
    /// Decreases current health by the number of health points, or to 0, in which case the entity will die.
    /// </summary>
    /// <param name="healthPoints">The amount of health points to decrease health by.</param>
    public void ReduceHealth(int healthPoints)
    {
        _currentHealth = Mathf.Max(_currentHealth - healthPoints, 0);
        if (_currentHealth <= 0) Die();
    }

3) Break code into several methods with descriptive names.

In stead of having a long piece of code with all if's and but's, you should break the code into the smallest manageable piece.

For example, look at this method:

    /// <summary>
    /// Decreases current health by the number of health points, or to 0, in which case the entity will die.
    /// </summary>
    /// <param name="healthPoints">The amount of health points to decrease health by.</param>
    public void ReduceHealth(int healthPoints)
    {
        _currentHealth = Mathf.Max(_currentHealth - healthPoints, 0);
        if (_currentHealth <= 0)
        {
        	_isDead = true;
        	onDie.Invoke();
        }
	}

This is such a short and sweet method. However, it would be even more readable to separate the dying code into it's own method. Like this:

    /// <summary>
    /// Decreases current health by the number of health points, or to 0, in which case the entity will die.
    /// </summary>
    /// <param name="healthPoints">The amount of health points to decrease health by.</param>
    public void ReduceHealth(int healthPoints)
    {
        _currentHealth = Mathf.Max(_currentHealth - healthPoints, 0);
        if (_currentHealth <= 0) Die();
    }
    
    
    /// <summary>
    /// Kills the entity immediately.
    /// </summary>
    public void Die()
    {
        _isDead = true;
        onDie.Invoke();
    }

This is especially useful when the methods become long and complicated.
It makes the code more readable, more reusable, and easier to maintain.

4) Comment inside methods

If you can't break the code into smaller methods, but it still becomes somewhat cluttered, use comments inside the code.
Like this:

    /// <summary>
    /// Decreases current health by the number of health points, or to 0, in which case the entity will die.
    /// </summary>
    /// <param name="healthPoints">The amount of health points to decrease health by.</param>
    public void ReduceHealth(int healthPoints)
    {
    	// Reduce the health, but make sure it doesn't fall below 0.
        _currentHealth = Mathf.Max(_currentHealth - healthPoints, 0);
        
        
// If the health is 0, kill the character.
        if (_currentHealth <= 0)
        {
        	_isDead = true;
        	onDie.Invoke();
        }
	}

5) Separate sections

I personally use comments to separate sections. You could use the Visual Studio #region and #endregion tags to be able to expand and collapse these areas as well, but I actually think it makes code harder to read, and more cumbersome to navigate. So my personal preference is to use comments like this:

    //------------------------------------------
    // Serializable, private variables
    //------------------------------------------

You can see them used in the final code for the class Killable below.
As you can see, I like to separate into Private variables, public variables, public methods, and private methods.
Regardless of what language you use to program, following these guides will help make your code readable and easier to maintain.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Killable : MonoBehaviour
{
    //------------------------------------------
    // Serializable, private variables
    //------------------------------------------

    [SerializeField] int _currentHealth = 100;
    [SerializeField] int _maxHealth = 100;
    [SerializeField] bool _isDead = false;



    //------------------------------------------
    // Public variables
 / properties
    //------------------------------------------


    public UnityEvent onDie;


    /// <summary>
    /// Returns true if the entity is dead, false if not.
    /// </summary>
    public bool IsDead { get { return _isDead; } }




    //------------------------------------------
    // Public methods
    //------------------------------------------




    /// <summary>
    /// Increases current health by the number of health points, or to maximum allowed health.
    /// </summary>
    /// <param name="healthPoints">The amount of health points to increase health by.</param>
    public void AddHealth(int healthPoints)
    {
        _currentHealth = Mathf.Min(_currentHealth + healthPoints, _maxHealth);
    }




    /// <summary>
    /// Decreases current health by the number of health points, or to 0, in which case the entity will die.
    /// </summary>
    /// <param name="healthPoints">The amount of health points to decrease health by.</param>
    public void ReduceHealth(int healthPoints)
    {
        _currentHealth = Mathf.Max(_currentHealth - healthPoints, 0);
        if (_currentHealth <= 0) Die();
    }



    /// <summary>
    /// Increases max health by a given value.
    /// </summary>
    /// <param name="value">The value to increase max health by.</param>
    public void IncreaseMaxHealth(int value)
    {
        _maxHealth += value;
    }




    /// <summary>
    /// Kills the entity immediately.
    /// </summary>
    public void Die()
    {
        _isDead = true;
        onDie.Invoke();
    }
}
1 likes 3 comments

Comments

Alberth

I also tend to document what a value in a variable means. Health points are a bit simple, but typically it means documenting the boundaries, and any “special value”, for example if -1 means “health points are not known" that fact should be written for the variable where the -1 is being stored. (One step further is to have a named constant for such magic values of course, which is also more documenting in the code.)

For other values, eg in physics, the unit may become relevant. If speed is in [km/h], and distance in [m], then somewhere a factor 1000 must be injected when doing computations with speed and distance.

For catching errors quickly, you can add assertions in your code (an assert is basically a check that does nothing if everything is ok, else it typically crashes or throws an error). For example, in all your current health change functions you silently assume a non-negative value as input but you don't check it. If in some unknown way the code would execute IncreaseMaxHealth(-10000); you get negative max health without getting warned about it. Then in some completely other code the health is improved due to some action (eg visiting a hospital), and the NPC immediately dies as a result. Tracing that event back to the call where the max health becomes negative can take weeks. On the other hand, if the program crashes as soon as you try to make the maximum health negative, it takes much less time to understand and repair the code.

November 26, 2022 08:55 PM
AndyPett

@Alberth Awesome example! You are so right. Since I wrote this post, I actually have started to comment better, specifically to explain the boundaries or what will happen if the value is set to 0 for example. Using constants is also extremely valuable and more readable.
You are of course right about the assertions, and I hadn't thought so much about that. Will definitely implement this where necessary. Thanks! ?

November 26, 2022 09:03 PM
Alberth

Setting max health to zero is also a nice one to find again ?

This shows how thinking about boundaries of variables is also important. “health” can be zero, but “maxHealth” should be a positive value.

November 26, 2022 09:01 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement
Advertisement