Unity6 Game Development · Lesson 12

Events and activation-scoped subscriptions

Pair subscription with unsubscription using the same handler identity.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

A subscription is a relationship with an owner and an end

A publisher’s event stores delegates that can invoke subscribers and keep their referenced state reachable. Repeated activation can accidentally add the same relationship several times if teardown is missing. EventCounter pairs OnEnable subscription with OnDisable removal for its own named handler. That policy says the listener belongs to the component’s enabled lifetime. A longer-lived publisher or static event may require another explicit owner, especially when Editor domain reload settings preserve static state between Play sessions instead of recreating a fresh managed world.

Trace one score action and its listener

In Play mode, Add One Point increments the component’s score and invokes ScoreChanged if a listener exists. OnEnable previously added ReportScore, so one invocation prints the current score once. Disabling removes that listener, and re-enabling adds it again. The script is deliberately self-contained: it demonstrates the publisher/listener relationship inside one component without requiring a hidden second file. A larger system can split those responsibilities, but then reference acquisition and the publisher’s lifetime must be configured before a subscriber relies on them.

Observe only the publication made during subscription

The portable Signal publishes integer values through an ordinary C# event. The code saves one listener delegate, subscribes it, publishes one, removes it and publishes two. The observed counter increments only for the first publication, so the program prints one. The values one and two are event payloads, not the final observed count. This isolates a delegate-identity and lifetime contract without using UnityEvent, Inspector callbacks or engine activation. A successful result therefore does not prove a scene’s actual publisher reference is assigned correctly.

A disabled component is not a universal method-access barrier

Other code can still call ordinary methods on a disabled MonoBehaviour, and documented physics callbacks have their own disabled-behaviour rules. Enabled state must not be confused with an automatic access-control boundary. In this sample, manually invoking AddOnePoint while the listener is unsubscribed can still alter score even though the self-listener does not report it. Also avoid removing a freshly created lambda as though it were necessarily the earlier captured delegate. Keep the actual handler identity or a clearly owned subscription token when the relationship needs deterministic removal.

Practice: test repeated enable cycles for duplicate listeners

Enter Play mode, invoke Add One Point twice and expect one report per action. Toggle the component several times, then invoke again and verify that a single action still produces one report. Use a separate experiment to call an ordinary public method while disabled and distinguish state mutation from listener notification. If the policy should reject disabled gameplay actions, add that explicit guard deliberately rather than assuming OnDisable made every public entry point unreachable.

  • Count notifications separately from score increments so duplicate callbacks cannot hide behind a plausible final score.
  • If splitting publisher and subscriber, initialize references needed by OnEnable before that callback, not in Start.

Practice: preserve the handler identity

For an independent C# exercise, subscribe the saved listener twice and inspect how many increments one publication causes, then remove it once and inspect the remaining relationship. Restore the single-subscription baseline afterward. Next create two separate captured listeners with distinct state and remove only one. These variations reveal that event registration is concrete delegate state, not a magical declaration that the runtime automatically deduplicates. Write the intended relationship count before choosing whether a disposable subscription abstraction would make the ownership clearer.

  • Test publication after unsubscription, not only publication immediately after adding a handler.
  • Do not clear an entire shared publisher event merely to remove one subscriber unless that publisher owns such a reset policy.

Verify payload, relationship count and publisher lifetime

An event-driven design is easier to trust when it names who publishes, who listens, how the handler is retained and when the relationship ends. The portable baseline verifies one subscription interval; the real component adds Unity activation behavior that needs Editor checks. Static publishers, pooled actors and scene transitions make the same questions more important rather than removing them. Preserve the distinction between a gameplay state change and a notification about it so cleanup, validation and duplicate prevention can each be tested without guessing from one final log line.

  • Portable output is one observed notification.
  • The named handler is added in OnEnable and removed in OnDisable.
  • A public method call, an event publication and an enabled callback lifetime are different boundaries.
Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Attach EventCounter.cs to an active object and enter Play mode.
  2. Invoke Add One Point twice from the component context menu and observe one message per call.
  3. Disable and re-enable the component several times, then invoke the action again.
  4. Compare a named handler with separately created lambda delegates in a separate plain C# experiment.
  5. When extending to a shared publisher, identify which component owns the publisher lifetime and whether its events are static.
using System;
using UnityEngine;
public sealed class EventCounter : MonoBehaviour
{
    public event Action<int> ScoreChanged;
    private int score;
    private void OnEnable() { ScoreChanged += ReportScore; }
    private void OnDisable() { ScoreChanged -= ReportScore; }
    [ContextMenu("Add One Point")]
    public void AddOnePoint()
    {
        if (!Application.isPlaying) { Debug.LogWarning("Enter Play mode first.", this); return; }
        score++;
        if (ScoreChanged != null) ScoreChanged(score);
    }
    private void ReportScore(int value) { Debug.Log("Score: " + value, this); }
}

What to observe in Unity

Each context action while enabled produces one score message. Re-enabling does not accumulate the component’s own listener; ordinary method calls and enabled callback lifetime remain different concepts.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var signal = new Signal(); int observed = 0;
Action<int> listener = value => observed++;
signal.Changed += listener; signal.Publish(1); signal.Changed -= listener; signal.Publish(2);
Console.WriteLine(observed);
sealed class Signal { public event Action<int>? Changed; public void Publish(int value) { Changed?.Invoke(value); } }
Open C# practice

C# practice output

1
Try this
Store or name the delegate you subscribe so you can remove that exact handler.

Further reading