Menu
CoddyTech

C# Events in Unity

Part of the Game Architecture in Unity section of Coddy's Unity journey. Lesson 10 of 50.

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. You know event and Action from C#. 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

The fighter's combo grows with every hit of Space and falls back to 0 after comboWindow (1) second without a hit. Right now Fighter.cs sets the combo text and the flame's size itself, so it needs a field for each. Turn it around with an event:

  • In Fighter.cs, declare a public event named OnComboChanged that passes the combo (an int). Raise it with the new combo every time the combo changes, the reset to 0 included, and make sure a hit causes no error when nobody listens.
  • Delete Fighter's comboText and flame fields and the lines that use them: the Fighter knows nothing of the HUD.
  • In ComboText.cs and ComboFlame.cs, subscribe their Show and Grow methods to the fighter's event in Start(). Both have a fighter field, linked in the scene.

Press Play, hit the bag with Space a few times, wait a second, 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;
            }
        }
    }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Game Architecture in Unity