wrote a C# event wrapper for Unity that auto-unsubscribes on destroy — tired of hunting phantom callbacks

384 views 1 reply

Spent way too much time last week tracking a callback firing on a destroyed object. Again. Instead of relying on OnDestroy discipline across every subscriber, I wrote a small generic wrapper that handles cleanup automatically. Dead owners get pruned before each invoke, no manual unsubscribe needed.

using System;
using System.Collections.Generic;
using UnityEngine;

public class ManagedEvent<T>
{
    private readonly List<Subscription> _subs = new();

    private class Subscription
    {
        public MonoBehaviour Owner;
        public Action<T> Handler;
    }

    public void Subscribe(MonoBehaviour owner, Action<T> handler)
    {
        _subs.Add(new Subscription { Owner = owner, Handler = handler });
    }

    public void Unsubscribe(MonoBehaviour owner)
    {
        _subs.RemoveAll(s => s.Owner == owner);
    }

    public void Invoke(T arg)
    {
        _subs.RemoveAll(s => s.Owner == null);
        foreach (var sub in new List<Subscription>(_subs))
            sub.Handler(arg);
    }
}

The snapshot in Invoke means handler-triggered subscribe/unsubscribe calls during iteration don't corrupt the loop. Usage ends up pretty clean:

// Publisher side
public class PlayerHealth : MonoBehaviour
{
    public readonly ManagedEvent<int> OnDamaged = new();

    public void TakeDamage(int amount)
    {
        _hp -= amount;
        OnDamaged.Invoke(amount);
    }
}

// Subscriber side — no OnDestroy needed
public class DamageUI : MonoBehaviour
{
    [SerializeField] private PlayerHealth _health;

    private void Start() =>
        _health.OnDamaged.Subscribe(this, HandleDamage);

    private void HandleDamage(int amount) { /* update UI */ }
}

Main tradeoff: Invoke allocates a list snapshot every call. Fine for low-frequency events like damage, quest flags, or dialogue triggers. Not great for anything per-frame. For those I just use a plain C# event and accept the discipline overhead.

Also doesn't handle the publisher-gets-destroyed-first case. Subscribers would hold a stale reference to a dead publisher. I work around this by putting ManagedEvents on long-lived manager objects, but it's a real gap in the design and I'm not thrilled with it as a constraint.

Anyone solved this more cleanly? Poked at WeakReference<T> as an alternative but it doesn't play nicely with value-type delegates and I couldn't find a clean path. Curious what patterns others are actually using for this.

Worth flagging if you haven't hit this yet: if your subscribing objects go through an object pool rather than getting truly destroyed, OnDestroy won't fire on pool return, only on final scene cleanup. The wrapper would need to hook into the pool's return lifecycle too, not just MonoBehaviour lifetime. I tried a nearly identical pattern and didn't account for pooling, and it took embarrassingly long to figure out why callbacks were still firing on recycled objects.

Moonjump
Forum Search Shader Sandbox
Sign In Register