Skip to main content

Manage Event Lifetimes and Prevent Recursion

TinyGiants
Unity Games & Tools Developer

Check the lifetime of every listener and scheduled action attached to an event. Scene objects should remove their own callbacks when disabled, and callbacks that can raise another event should account for paths that lead back to the source.

Match the subscription to the listener's lifetime​

If opening a panel twice makes its callback run twice, check where it subscribes and unsubscribes. Pair OnEnable with OnDisable so the panel listens only while enabled. Store a lambda as a UnityAction<int> for an Int32GameEvent.

using TinyGiants.GES.Runtime;
using UnityEngine;
using UnityEngine.Events;

public sealed class HealthDisplay : MonoBehaviour
{
[GameEventDropdown, SerializeField] private Int32GameEvent onHealthChanged;

private UnityAction<int> _listener;

private void OnEnable()
{
_listener = health => Debug.Log($"Health: {health}");
onHealthChanged.AddListener(_listener);
}

private void OnDisable()
{
onHealthChanged.RemoveListener(_listener);
_listener = null;
}
}

Use RemoveListener, RemovePriorityListener, RemoveConditionalListener, or RemovePersistentListener to remove the matching registration. RemoveAllListeners() clears all Basic, Priority, and Conditional registrations on that event, including other components' callbacks.

For a service that should listen across scene loads, use AddPersistentListener and keep its owner alive. Remove its registration when the service stops or is destroyed. When a scene unloads, ordinary event assets clear their Basic, Priority, and Conditional callbacks. The event asset's Persistent setting preserves those registrations across scene unload, and AddPersistentListener registers the individual callback in the persistent stage.

If your own static state carries from one Play session to the next, reset it at RuntimeInitializeLoadType.SubsystemRegistration so it also resets with Domain Reload disabled. GES uses that entry point to reset its event cache.

Raise after changing state​

A health-change listener that raises the same event can call itself synchronously. Route requests and notifications through different events. Apply damage on the damage request, write the new health once, then notify views that read it.

private void ApplyDamage(int amount)
{
int next = Mathf.Max(0, health - Mathf.Max(0, amount));
if (next == health)
return;

health = next;
onHealthChanged.Raise(health);
}

Have the HUD subscribe to onHealthChanged and refresh from the new value. A reaction that deals damage again needs a gameplay condition or another event that ends the feedback loop.

When reproducing repeated raises, look at the event order in Game Event Monitor and use Warnings to check what GES recorded.

Monitor Warnings

Keep and cancel scheduled work​

RaiseDelayed and RaiseRepeating return a ScheduleHandle. Store the handle where you start the work. For a poison effect, cancel the previous schedule before starting a new one and cancel it when the effect ends or the component is disabled. Here onPoisonTick is an Int32GameEvent, and repeatCount: -1 repeats until cancelled.

private ScheduleHandle _poison;

private void StartPoison()
{
StopPoison();
_poison = onPoisonTick.RaiseRepeating(10, interval: 1f, repeatCount: -1);
}

private void StopPoison()
{
_poison?.Cancel();
_poison = null;
}

private void OnDisable() => StopPoison();

For several schedules, keep each handle and call Cancel() on the ones your component owns. Calling Cancel() on the event itself controls dispatch scheduled through its Inspector settings. Use the handle for an explicit RaiseDelayed or RaiseRepeating call.

Keep frequent predicates cheap​

A Conditional listener checks its predicate each time its event dispatch reaches that stage. For an event that fires on position changes, update the nearby-enemy flag on a suitable interval and read the flag from the predicate. Here onPositionUpdate is a Vector3GameEvent, so the predicate takes one Vector3 argument.

private bool _hasNearbyEnemies;

private void FixedUpdate()
{
_hasNearbyEnemies = Physics.CheckSphere(transform.position, 10f, enemyLayer);
}

private void OnEnable()
{
onPositionUpdate.AddConditionalListener(
HandlePosition, condition: _ => _hasNearbyEnemies);
}

private void OnDisable()
{
onPositionUpdate.RemoveConditionalListener(HandlePosition);
}

Mute Inspector actions during a batch​

When Inspector actions are normally enabled on an event, turn them off while processing a batch and restore them in finally. C# listeners and registered Trigger and Chain connections still run. Use a separate completion event for a single summary update.

onItemsProcessed.SetInspectorListenersActive(false);
try
{
foreach (int itemCount in batches)
onItemsProcessed.Raise(itemCount);
}
finally
{
onItemsProcessed.SetInspectorListenersActive(true);
}

onBatchFinished.Raise();

🚀 Global Developer Service Matrix

🇨🇳 China Developer Community

🌐 Global Developer Community

📧 Support & Collaboration