Events & Delegates in Unity/C#

Let’s say your character just lost 10 health points and you want to update your health bar. This is when events and delegates come in very handy.

With events we can make our character say “I just lost 10 health points”, while the health bar can say “notify me when the character lost health points, so I can update myself”.

Health Bar Example – Tell me when it hurts!

The character.

Here we have a character with health points. Every time the health of our characters changes, it tells everybody (who subscribed to their latest health points news): “hey, my health points just changed”. This is basically what an event is. Here is the full code first:

using UnityEngine;

public class Character : MonoBehaviour
{
    public delegate void OnHealthChangedDelegate();
    public event OnHealthChangedDelegate OnHealthChanged;

    private float _health;

    public void TakeDamage(float damage)
    {
        _health -= damage;

        OnHealthChanged?.Invoke();
    }
}

At first we define a delegate, which has a return type and parameters. In this case the return type is void and there are no parameters passed. It is basically a “template” for a function. Other classes will implement this function, using the same return type and the same parameters.

public delegate void OnHealthChangedDelegate();

Now we define an event. An event stores a list of function pointers. When it is invoked, it calls all those functions. Every function pointer stored in the list will point to a function with the same format as the delegate. Meaning they will all return the same type and receive the same parameters.

public event OnHealthChangedDelegate OnHealthChanged;

Then we invoke the event, which means it will now iterate through that list to call the functions. The “?” after OnHealthChanged ensures it gets only invoked, when the list is not empty.

OnHealthChanged?.Invoke();

The health bar.

The other class is the health bar. The health bar basically says “hey, give me all the latest health updates about that one character I’m interested in”. Every time the health of the character changes, we want to call a function that updates our health bar. Here is the full code first:

using UnityEngine;

public class Healthbar : MonoBehaviour
{
    [SerializeField] private Character _character;

    private void Start()
    {
        if (_character != null)
        {
            _character.OnHealthChanged += _character_OnHealthChanged;
        }  
    }

    private void OnDestroy()
    {
        if (_character != null)
        {
            _character.OnHealthChanged -= _character_OnHealthChanged;
        }
    }

    private void _character_OnHealthChanged()
    {
        // redraw the healthbar
    }
}

Let’s start by implementing the function that is called when the health of the character changes. Remember that this function has to match the format of the delegate (OnHealthChangedDelegate). How you name this function is up to personal taste and does not matter.

private void _character_OnHealthChanged()
{
    // redraw the healthbar
}

Now we need a reference to a character. We could e.g. add a field to our class and drag and drop a character reference in the editor.

[SerializeField] private Character _character;

Now comes the important part: updating the health bar when the characters health points changed. To do this we simply need to add our function to the function pointer list of the event. To do this we use the += operator.

_character.OnHealthChanged += _character_OnHealthChanged;

We can also remove the function from the list again by using the -= operator. This is important when you e.g. destroy the health bar, yet the character is still alive. Otherwise the character would try to call the function, yet the health bar doesn’t exist anymore, leading to an error. It’s basically like the character screaming into the void… and if something replies… it would be scary. (Never scream into the void)

_human.OnHealthChanged -= _human_OnHealthChanged;

I typically use Start to bind to an event and OnDestroy to unbind again.

When the character gets destroyed, yet the health bar is still alive, you do not have to unbind anything. The character stores the event, which means when it is destroyed, the list of function pointers also gets destroyed. Therefore nothing can be invoked anymore. So the clean up is only needed when something on the receiving end of the event gets destroyed.

Funeral Company Example – Call us when you die!

What if we want to give a funeral every time someone dies? But there are 1 million people in town and we do not want to check every second if they are all still alive. How about they notify us when they die? In their final moments they grab their phone, call us and tell us: “see you on the other side, buddy”.

Here is some code that does exactly what I just described, by using a static event:

using UnityEngine;

public class Character : MonoBehaviour
{
    public delegate void OnDeathDelegate(Character character, string lastWords);
    public static event OnDeathDelegate OnDeath;

    public void Kill()
    {
        OnDeath?.Invoke(this, "see you on the other side, buddy");
    }
}

Every time any character dies, it invokes the OnDeath event. But who cares? Of course: the funeral company. So let’s make a funeral company class.

using UnityEngine;

public class FuneralCompany : MonoBehaviour
{
    private void Start()
    {
        Character.OnDeath += Character_OnDeath;
    }

    private void OnDestroy()
    {
        Character.OnDeath -= Character_OnDeath;
    }

    private void Character_OnDeath(Character character, string lastWords)
    {
        // call the characters relatives
        // make a price offer
        // chisel last words into the grave stone
    }
}

As you can see, here we do not need a reference to any character anymore and can just use the Character class itself to subscribe to the event.

The problem of too many delegates

So let’s say your character has a lot of stats. Health, mana, stamina, armor, speed, charisma, strength, vision, agility… and so on. I personally found it quiet annoying to set up a delegate and event for every single stat. That’s why I prefer using a Number class. Here is the complete code of my class:

public class Number
{
    public delegate void OnValueChangedDelegate();
    public event OnValueChangedDelegate OnValueChanged;
    public delegate void OnMinValueChangedDelegate();
    public event OnMinValueChangedDelegate OnMinValueChanged;
    public delegate void OnMaxValueChangedDelegate();
    public event OnMaxValueChangedDelegate OnMaxValueChanged;
    public delegate void OnValueDepletedDelegate();
    public event OnValueChangedDelegate OnValueDepleted;

    private float _value;
    private float _minValue;
    private float _maxValue;

    public float Value
    {
        get => _value;

        set
        {
            if (value != _value)
            {
                _value = Mathf.Clamp(value, _minValue, _maxValue);
                OnValueChanged?.Invoke();

                if (_value == _minValue)
                {
                    OnValueDepleted?.Invoke();
                }
            }
        }
    }

    public float MinValue
    {
        get => _minValue;

        set
        {
            if (value != _minValue)
            {
                _minValue = Mathf.Min(value, _maxValue);
                OnMinValueChanged?.Invoke();

                if (_value < _minValue)
                {
                    Value = _minValue;
                }
            }
        }
    }

    public float MaxValue
    {
        get => _maxValue;

        set
        {
            if (value != _maxValue)
            {
                _maxValue = Mathf.Max(value, _minValue);
                OnMaxValueChanged?.Invoke();

                if (_value > _maxValue)
                {
                    Value = _maxValue;
                }
            }
        }
    }

    public Number(float value, float minValue, float maxValue)
    {
        _minValue = minValue;
        _maxValue = Mathf.Max(_minValue, maxValue);
        _value = Mathf.Clamp(value, _minValue, _maxValue);
    }
}

Here I have a value, min value and max value for every number. And I use accessors to handle their changes, and to get the comfort of only having to write this, when I want to attack a character:

character.Health.Value -= 1.0f;

Now I can use this class to add stats to my character, like this:

using UnityEngine;

public class Character : MonoBehaviour
{
    private Number _health;

    public Number Health => _health;

    private void Start()
    {
        _health = new(50.0f, 0.0f, 100.0f);
        _health.OnValueDepleted += _health_OnValueDepleted;
    }

    private void OnDestroy()
    {
        _health.OnValueDepleted -= _health_OnValueDepleted;
    }

    private void _health_OnValueDepleted()
    {
        Destroy(gameObject);
    }
}

I just have to add a new health number and can subscribe to its changes or when it is depleted, in this case I used it to destroy the character when the health is depleted.

Now I can keep adding more and more stats and just subscribe to their changes. I can pass my character class to the UI class and it can subscribe to all the stats and update itself whenever something changes. I personally find this very handy.

I also like making binding classes the that take in a Number and e.g. a Text element and update the text whenever the number changes. So e.g. you have something like this:

// set up the binding
NumberTextBinding healthBinding = new(character.Health, textElement);
// and on destroy:
healthBinding.Destroy();

In the NumberTextBinding class you can subscribe to the value changes in the constructor and implement a function that updates the text when the value changed. And define a Destroy function to call when you want to remove the binding again.

What about the return type of the delegate?

I haven’t found a use case yet for a return type other than void.

Leave a Reply

Your email address will not be published. Required fields are marked *