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.