Events & tokens
Token Bags
Own and clean up groups of subscription tokens from a Unity lifecycle.
- Bag ownership
- DisposeAll
- PauseAll
- Capacity
Use AzEventTokenBag when one Unity lifecycle owns several subscription tokens.
AzEventTokenBag
AzEventTokenBag stores many AzEventToken values and performs lifecycle operations on all of them.
private readonly AzEventTokenBag _subscriptions = new();It is ideal for MonoBehaviours because Unity components often create several subscriptions and need to clean them up together.
Basic Bag Pattern
using Azkar.Eda;
using UnityEngine;
public sealed class CombatHud : MonoBehaviour
{
private readonly AzEventTokenBag _subscriptions = new();
private void OnEnable()
{
CombatEvents.CombatStarted.Subscribe(OnCombatStarted).AddTo(_subscriptions);
CombatEvents.HealthChanged.Subscribe(OnHealthChanged).AddTo(_subscriptions);
CombatEvents.DamageReported.Subscribe(OnDamageReported).AddTo(_subscriptions);
}
private void OnDisable()
{
_subscriptions.DisposeAll();
}
private void OnCombatStarted()
{
}
private void OnHealthChanged(int health)
{
}
private void OnDamageReported(in DamageReport report)
{
}
}AddTo adds the token to the bag and returns the same token, so you can keep a copy when you need individual control:
private AzEventToken _healthToken;
private void OnEnable()
{
_healthToken = CombatEvents.HealthChanged.Subscribe(OnHealthChanged).AddTo(_subscriptions);
}Bag API
Add(token)Adds a live
AzEventTokento the bag. Invalid or disposed tokens are ignored.token.AddTo(bag)Fluent extension for
bag.Add(token). Returns the token.PauseAll()Pauses all live tokens. Returns the number of tokens it attempted to pause.
ResumeAll()Resumes all live tokens. Returns the number of tokens it attempted to resume.
DisposeAll()Disposes all stored tokens and clears the bag.
Dispose()Calls
DisposeAll()and permanently disposes the bag itself.PruneInvalid()Removes disposed tokens from the bag and returns the count removed.
HasValidTokens()True if the bag contains at least one live token.
Clear()Clears the bag without disposing tokens.
RemoveToken(token, dispose)Removes one matching token, optionally disposing it.
TryRemoveToken(token, dispose)Like
RemoveToken, but returns false if the bag itself has already been disposed.RemoveWhere(predicate, dispose)Removes every token matching a predicate.
ForEach(action)Applies an action to each live token using a snapshot.
Tokens()Enumerates a snapshot of stored tokens.
EnsureCapacity(min)Preallocates internal list storage.
EnsureHeadroom(additional)Preallocates enough room for more tokens.
ShrinkToFit()Shrinks internal storage to current count.
CountNumber of tokens stored in the bag, including tokens that may now be disposed.
IsDisposedTrue after the bag itself is disposed.
DisposeAll vs Dispose
This distinction matters.
DisposeAll():
- disposes every token currently in the bag
- clears the bag
- leaves the bag reusable
Dispose():
- calls
DisposeAll() - marks the bag itself as disposed
- makes future bag operations throw
ObjectDisposedException
Use DisposeAll() in OnDisable if the component may be enabled again.
private void OnDisable()
{
_subscriptions.DisposeAll();
}Use Dispose() in OnDestroy when the bag will never be used again.
private void OnDestroy()
{
_subscriptions.Dispose();
}If you call Dispose() in OnDisable, then OnEnable will not be able to add subscriptions to that same bag later.
PauseAll and ResumeAll
Pause/resume is useful for pooled objects that stay subscribed while temporarily inactive.
public sealed class PooledSeeker : MonoBehaviour
{
private readonly AzEventTokenBag _subscriptions = new();
public void Enroll(AzEvent<Vector3> targetEvent)
{
targetEvent.Subscribe(OnTargetMoved).AddTo(_subscriptions);
}
private void OnEnable()
{
_subscriptions.ResumeAll();
}
private void OnDisable()
{
_subscriptions.PauseAll();
}
private void OnDestroy()
{
_subscriptions.Dispose();
}
private void OnTargetMoved(Vector3 target)
{
}
}This pattern avoids subscribing every time the object is enabled and unsubscribing every time it is disabled. It works well when the object is enrolled once and then repeatedly enabled/disabled by a pool.
DisposeAll for Normal Components
For ordinary components, it is usually simpler to subscribe in OnEnable and dispose in OnDisable.
private readonly AzEventTokenBag _subscriptions = new();
private void OnEnable()
{
GameEvents.ScoreChanged.Subscribe(OnScoreChanged).AddTo(_subscriptions);
}
private void OnDisable()
{
_subscriptions.DisposeAll();
}This makes the component's active subscription state match Unity's enabled state.
Remove One Token from a Bag
private AzEventToken _expensiveToken;
private void EnableExpensiveListener()
{
if (!_expensiveToken.IsDisposed)
{
return;
}
_expensiveToken = GameEvents.ScoreChanged.Subscribe(OnExpensiveScoreChanged).AddTo(_subscriptions);
}
private void DisableExpensiveListener()
{
_expensiveToken.RemoveFrom(_subscriptions, dispose: true);
_expensiveToken = AzEventToken.Invalid;
}RemoveFrom is a fluent extension over bag.TryRemoveToken.
Pruning Invalid Tokens
Count is the number of tokens stored, not necessarily the number of live subscriptions. If tokens are disposed individually, the bag can still contain old token values until you prune them.
int removed = _subscriptions.PruneInvalid();
Debug.Log($"Pruned {removed} disposed token(s).");You usually do not need to call this every frame. Use it after manual removals or during maintenance for long-lived bags.
Clear Without Disposing
Clear() removes tokens from the bag without calling Dispose() on them.
_subscriptions.Clear();Use this only when ownership is intentionally moving somewhere else or you have already disposed the tokens separately. For normal lifecycle cleanup, prefer DisposeAll().
Capacity and Memory Management
Each event exposes:
int count = evt.Count;
int capacity = evt.Capacity;
bool hasSubscribers = evt.HasSubscribers;
evt.EnsureCapacity(128);
evt.TrimExcess();
evt.Clear();EnsureCapacity
Use EnsureCapacity when you know an event will receive many subscriptions.
CombatEvents.HealthChanged.EnsureCapacity(256);This can avoid internal growth during setup.
TrimExcess
TrimExcess compacts internal storage.
CombatEvents.HealthChanged.TrimExcess();Important: when TrimExcess actually compacts an event's storage, live token identities can be reassigned. Outstanding AzEventToken values for that event may become disposed and no longer control their subscriptions after compaction.
That means this is not a normal per-frame maintenance operation. Use it during a controlled maintenance phase when nobody still needs old tokens.
If components are holding tokens or bags for subscriptions to that event, do not call TrimExcess casually. After compaction, those old tokens will report disposed and will no longer be able to pause, resume, or dispose their previous subscriptions.
Clear
Clear removes all subscriptions from the event.
CombatEvents.HealthChanged.Clear();Use it when the event owner is resetting the whole event. Component owners should usually dispose their own tokens instead.
