Menu
CoddyTech

C# Events in Unity

Part of the Game Architecture in Unity section of Coddy's Unity journey. Lesson 12 of 64.

When the score changes, several things care: the score text, a sound, an achievement. If the ScoreKeeper calls each of them, it needs a field for every one, and each new feature means editing it. An event turns that around: the ScoreKeeper announces the change, and whoever cares subscribes.

Two C# words make this work. Action<int> is the type of a method that takes one int and returns nothing. An event of that type is a list of such methods: other code adds its method with +=, and the owner calls them all with Invoke. In plain C#:

using System;

class Alarm
{
    public event Action<int> Rang;

    public void Ring(int hour)
    {
        Rang?.Invoke(hour);
    }
}

class Sleeper
{
    public void WakeUp(int hour)
    {
        Console.WriteLine("Up at " + hour);
    }
}

// elsewhere
Alarm alarm = new Alarm();
Sleeper sleeper = new Sleeper();
alarm.Rang += sleeper.WakeUp;
alarm.Ring(7);   // Up at 7

In Unity the publisher and its listeners are components, usually on different GameObjects:

using System;
using UnityEngine;

public class ScoreKeeper : MonoBehaviour
{
    public event Action<int> OnScoreChanged;
    private int score;

    public void AddPoints(int points)
    {
        score += points;
        OnScoreChanged?.Invoke(score);
    }
}
// ScoreLabel.cs, on the HUD's text
public ScoreKeeper keeper;
public TMP_Text label;

void Start()
{
    keeper.OnScoreChanged += ShowScore;
}

void ShowScore(int score)
{
    label.text = "Score: " + score;
}

Action<int> means each listener is a method that takes one int, here the new score. Invoke calls every subscribed method, in the order they subscribed. With no listener the event is null, so a plain OnScoreChanged(score) would throw a NullReferenceException, and ?. skips the call instead. The event keyword limits other scripts to += and -=: in ScoreLabel, keeper.OnScoreChanged(5) or keeper.OnScoreChanged = null does not compile.

The ScoreKeeper has no field for the label and does not know it exists. A combo counter or a sound player subscribes the same way and the ScoreKeeper stays as it is, and a test scene with no HUD runs without errors. A listener finds the publisher through an Inspector reference, as here, or any way you know, such as GetComponent or a singleton. Removing a listener with -= matters once objects get destroyed, which is the next lesson.

challenge icon

Challenge

Easy

Make the fighter announce its combo.

  1. In Fighter.cs, add using System; and public event Action<int> OnComboChanged;.
  2. Replace the lines that use comboText and flame with OnComboChanged?.Invoke(combo);, and delete both fields.
  3. In the Start() of ComboText and ComboFlame, write fighter.OnComboChanged += Show; and += Grow;.

Press Play and hit Space a few times: the combo grows, then clears. Then Check.

Try it yourself

using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;

public class Fighter : MonoBehaviour
{
    public float comboWindow = 1f;
    public TMP_Text comboText;
    public Transform flame;
    private int combo;
    private float sinceHit;

    void Update()
    {
        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            combo++;
            sinceHit = 0f;
            comboText.text = "Combo x" + combo;
            flame.localScale = Vector3.one * (0.5f + 0.25f * combo);
        }
        else if (combo > 0)
        {
            sinceHit += Time.deltaTime;
            if (sinceHit > comboWindow)
            {
                combo = 0;
                comboText.text = "";
                flame.localScale = Vector3.one * 0.5f;
            }
        }
    }
}

All lessons in Game Architecture in Unity

Practice on your own: Unity playground