Unity

Events plus Unity plus advanced C#

Estimated reading: 7 minutes 25 views Contributors

We’ll do this:

  • A UI Button that triggers a custom C# event.
  • Multiple different GameObjects listen to that event and react in different ways.
  • You can extend it to show delegates, Action<>, event, and decoupling.

1. Concept and learning goals

Scenario

There is a “Grant Points” button in the UI. When you press it:

  • The ScoreManager increases the player score and updates UI text.
  • A ConsoleLogger prints a debug line.
  • A ColorFlasher object briefly flashes a color.

All three are listening to the same event. The button does not know who is listening. That is the key teaching point.

C# concepts you can highlight

  • Delegates vs Action vs event
  • Publisher / Subscriber pattern
  • Subscribing and unsubscribing in OnEnable / OnDisable
  • Why events decouple UI from game logic

2. Unity project setup

  1. Create a new Unity 3D or 2D project. Any Render Pipeline is fine.
  2. In Hierarchy:
    • Right click → UI → Canvas.
    • Add:
      • A Button: name it GrantPointsButton.
      • A Text (UI) or TextMeshProUGUI text element: name it ScoreText.
  3. In Scene:
    • Create an empty GameObject: EventChannel.
    • Create another empty GameObject: ScoreManager.
    • Create a Cube or Sprite called FlashObject.
    • Create an empty GameObject: ConsoleLogger.

We will attach scripts to these.


3. Create the Event Channel (publisher)

This is your central hub that defines and raises the event.

Script 1: ScoreEventChannel.cs

using System;
using UnityEngine;

/// <summary>
/// Publishes score events to any listeners.
/// This is the "event channel" or "publisher".
/// </summary>
public class ScoreEventChannel : MonoBehaviour
{
    // Simple singleton to make it easy to access in a demo.
    public static ScoreEventChannel Instance { get; private set; }

    /// <summary>
    /// Event that fires whenever score should change.
    /// int: scoreDelta, string: reason
    /// </summary>
    public event Action<int, string> OnScoreChanged;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Debug.LogWarning("Multiple ScoreEventChannels found. Destroying duplicate.");
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }

    /// <summary>
    /// Call this to raise the score event.
    /// </summary>
    public void RaiseScoreChanged(int delta, string reason)
    {
        // Null conditional operator safely invokes if there are subscribers
        OnScoreChanged?.Invoke(delta, reason);
    }
}

Attach ScoreEventChannel to the EventChannel GameObject.

Teaching notes

  • Show public event Action<int, string> OnScoreChanged;
  • Explain Action<int, string> as a delegate type: method signature void Something(int, string).
  • The event keyword restricts who can invoke it (only the publisher).

4. Create the UI Button script (event source)

The button will call into the event channel when clicked.

Script 2: GrantPointsButton.cs

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Button))]
public class GrantPointsButton : MonoBehaviour
{
    [SerializeField] private int scoreDelta = 10;
    [SerializeField] private string reason = "Button Press";

    private Button _button;

    private void Awake()
    {
        _button = GetComponent<Button>();
    }

    private void OnEnable()
    {
        _button.onClick.AddListener(HandleClick);
    }

    private void OnDisable()
    {
        _button.onClick.RemoveListener(HandleClick);
    }

    private void HandleClick()
    {
        if (ScoreEventChannel.Instance == null)
        {
            Debug.LogWarning("No ScoreEventChannel found in scene.");
            return;
        }

        ScoreEventChannel.Instance.RaiseScoreChanged(scoreDelta, reason);
    }
}

Attach GrantPointsButton to the GrantPointsButton UI Button.

In the inspector you can tweak:

  • Score Delta (for example 5, 10, etc)
  • Reason (for example “Test Grant”, “Bonus from button”)

Teaching notes

  • Point out this button never references ScoreManager, ConsoleLogger, or FlashObject.
  • It only knows about the event channel.

5. Create a listener: ScoreManager

This will subscribe to the event and update the score text.

Script 3: ScoreManager.cs

using UnityEngine;
using UnityEngine.UI;

public class ScoreManager : MonoBehaviour
{
    [SerializeField] private Text scoreText; // Or TextMeshProUGUI if you use TMP

    public int CurrentScore { get; private set; }

    private void OnEnable()
    {
        if (ScoreEventChannel.Instance != null)
        {
            ScoreEventChannel.Instance.OnScoreChanged += HandleScoreChanged;
        }
    }

    private void OnDisable()
    {
        if (ScoreEventChannel.Instance != null)
        {
            ScoreEventChannel.Instance.OnScoreChanged -= HandleScoreChanged;
        }
    }

    private void Start()
    {
        UpdateScoreText();
    }

    private void HandleScoreChanged(int delta, string reason)
    {
        CurrentScore += delta;
        UpdateScoreText();

        Debug.Log($"[ScoreManager] Score changed by {delta} due to: {reason}. New total: {CurrentScore}");
    }

    private void UpdateScoreText()
    {
        if (scoreText != null)
        {
            scoreText.text = $"Score: {CurrentScore}";
        }
    }
}

Attach ScoreManager to the ScoreManager GameObject, then drag the ScoreText UI element into the Score Text field in the inspector.

Teaching notes

  • Show subscription in OnEnable and unsubscription in OnDisable.
  • Explain why unsubscribing matters (memory leaks, stale references, events on destroyed objects).

6. Create another listener: ConsoleLogger

This one only logs messages to show multiple listeners can react.

Script 4: ConsoleLogger.cs

using UnityEngine;

public class ConsoleLogger : MonoBehaviour
{
    private void OnEnable()
    {
        if (ScoreEventChannel.Instance != null)
        {
            ScoreEventChannel.Instance.OnScoreChanged += LogScoreEvent;
        }
    }

    private void OnDisable()
    {
        if (ScoreEventChannel.Instance != null)
        {
            ScoreEventChannel.Instance.OnScoreChanged -= LogScoreEvent;
        }
    }

    private void LogScoreEvent(int delta, string reason)
    {
        Debug.Log($"[ConsoleLogger] Event received. Delta: {delta}, Reason: {reason}");
    }
}

Attach ConsoleLogger to the ConsoleLogger GameObject.

Hit Play, press the button, and show that multiple listeners receive the same event.


7. Create a visual listener: ColorFlasher

Now we use a GameObject (cube, sprite, whatever) that flashes a color when the event fires.

Script 5: ColorFlasher.cs

using UnityEngine;

[RequireComponent(typeof(Renderer))]
public class ColorFlasher : MonoBehaviour
{
    [SerializeField] private Color flashColor = Color.yellow;
    [SerializeField] private float flashDuration = 0.2f;

    private Renderer _renderer;
    private Color _originalColor;
    private float _flashTimer;
    private bool _isFlashing;

    private void Awake()
    {
        _renderer = GetComponent<Renderer>();
        _originalColor = _renderer.material.color;
    }

    private void OnEnable()
    {
        if (ScoreEventChannel.Instance != null)
        {
            ScoreEventChannel.Instance.OnScoreChanged += HandleScoreChanged;
        }
    }

    private void OnDisable()
    {
        if (ScoreEventChannel.Instance != null)
        {
            ScoreEventChannel.Instance.OnScoreChanged -= HandleScoreChanged;
        }
    }

    private void Update()
    {
        if (!_isFlashing) return;

        _flashTimer -= Time.deltaTime;

        if (_flashTimer <= 0f)
        {
            _renderer.material.color = _originalColor;
            _isFlashing = false;
        }
    }

    private void HandleScoreChanged(int delta, string reason)
    {
        _renderer.material.color = flashColor;
        _flashTimer = flashDuration;
        _isFlashing = true;
    }
}

Attach ColorFlasher to the FlashObject (cube).

Now your pipeline is:

Button → EventChannel → ScoreManager / ConsoleLogger / ColorFlasher


8. Using this in class to explain advanced C#

Here is a suggested teaching flow.

a) Start with the naive approach

Write a version where the button directly references the ScoreManager:

public class NaiveGrantPointsButton : MonoBehaviour
{
    public ScoreManager scoreManager;

    public void HandleClick()
    {
        scoreManager.AddScore(10);
    }
}

Point out that:

  • UI now depends directly on one specific component.
  • You cannot easily add more listeners without modifying the button code.
  • This causes tight coupling.

b) Introduce the event version

Switch to the event channel version we built.

Emphasize:

  • Button only knows about ScoreEventChannel.
  • Any number of listeners can subscribe with matching method signatures.
  • You can add or remove listeners without touching the UI code.

c) Talk about delegates vs events

Use ScoreEventChannel as your code lab.

  1. Show how it would look with a raw delegate field: public Action<int, string> OnScoreChanged;
  2. Explain why event is safer:
    • Outside classes cannot reassign the delegate.
    • They can only += or -=.
  3. If you want to go further, show a custom delegate: public delegate void ScoreChangedHandler(int delta, string reason); public event ScoreChangedHandler OnScoreChanged; Then compare that to Action<int, string>.

9. Bonus: event arguments type for extra clarity

If you want to be fancy and more “advanced C#”, you can introduce an event args class:

Script 6: ScoreChangedEventArgs.cs

public class ScoreChangedEventArgs
{
    public int Delta { get; }
    public string Reason { get; }

    public ScoreChangedEventArgs(int delta, string reason)
    {
        Delta = delta;
        Reason = reason;
    }
}

Then in the channel:

public event Action<ScoreChangedEventArgs> OnScoreChanged;

public void RaiseScoreChanged(int delta, string reason)
{
    var args = new ScoreChangedEventArgs(delta, reason);
    OnScoreChanged?.Invoke(args);
}

Listeners:

private void HandleScoreChanged(ScoreChangedEventArgs args)
{
    CurrentScore += args.Delta;
    // use args.Reason etc...
}

This mirrors the classic .NET pattern (EventHandler<TEventArgs>) and is a nice bridge from Unity to general C#.


10. Ideas to extend the demo

Once students grasp this, you can branch into:

  • A “QuestCompletedEventChannel” or “ItemPickedEventChannel”.
  • A ScriptableObject event channel (common Unity architecture pattern).
  • A mini achievement system where different listeners unlock achievements based on events.

The key lesson is that events turn a messy web of direct references into a clean signal system: one thing shouts, whoever cares can listen.

Share this Doc

Events plus Unity plus advanced C#

Or copy link

CONTENTS

Chat Icon Close Icon