13 Runtime API: Code-First Workflow
π Overviewβ
Previous demos (01-11) demonstrated the Visual Workflowβbinding listeners in Inspector, configuring conditions in Behavior windows, and building flow graphs visually. This approach is perfect for designers and rapid prototyping. However, programmers often prefer full control in code for complex systems, dynamic behavior, or when visual tools become limiting.
Demo 13 proves a critical architectural principle: Every feature you've seen in the visual workflow has a complete, type-safe C# API. This demo revisits all 11 previous scenarios, removing all Inspector bindings and Graph configurations, replacing them with runtime code.
- How to register/remove listeners programmatically (
AddListener,RemoveListener) - Dynamic priority control (
AddPriorityListener) - Runtime condition registration (
AddConditionalListener) - Scheduling APIs (
RaiseDelayed,RaiseRepeating,Cancel) - Building Flow Graphs in code (
AddTriggerEvent,AddChainEvent) - Persistent listener management (
AddPersistentListener) - Lifecycle management (
OnEnable,OnDisable, cleanup patterns)
π¬ Demo Structureβ
π Assets/TinyGiants/GameEventSystem/Demo/13_RuntimeAPI/
β
βββ π 01_VoidEvent β π [ Code-based void event binding ]
βββ π 02_BasicTypesEvent β π’ [ Generic event registration ]
βββ π 03_CustomTypeEvent β π [ Custom class binding ]
βββ π 04_CustomSenderTypeEvent β π₯ [ Dual-generic listeners ]
β
βββ π 05_PriorityEvent β π₯ [ Priority management in code ]
βββ π 06_ConditionalEvent β π‘οΈ [ Predicate-based filtering ]
βββ π 07_DelayedEvent β β±οΈ [ Scheduling & cancellation ]
βββ π 08_RepeatingEvent β π [ Loop management & callbacks ]
β
βββ π 09_PersistentEvent β π‘οΈ [ Cross-scene listener survival ]
βββ π 10_TriggerEvent β πΈοΈ [ Parallel graph construction ]
βββ π 11_ChainEvent β βοΈ [ Sequential pipeline building ]
Key Difference from 01-11:
- Scene Setup: Identical (same turrets, targets, UI buttons)
- Visual Configuration: β REMOVED (no Behavior window configs, no Flow Graphs)
- Code Implementation: All logic moved to
OnEnable/OnDisable/lifecycle methods
π Visual vs Code Paradigm Shiftβ
| Feature | Visual Workflow (01-11) | Code Workflow (Demo 13) |
|---|---|---|
| Listener Binding | Drag & drop in Behavior window | event.AddListener(Method) in OnEnable |
| Conditional Logic | Condition Tree in Inspector | event.AddConditionalListener(Method, Predicate) |
| Execution Priority | Drag to reorder in Behavior window | event.AddPriorityListener(Method, priority) |
| Delay/Repeat | Delay nodes in Behavior window | event.RaiseDelayed(seconds), event.RaiseRepeating(interval, count) |
| Flow Graphs | Visual connections in Flow Graph window | event.AddTriggerEvent(target, ...), event.AddChainEvent(target, ...) |
| Cleanup | Automatic when GameObject destroyed | Manual in OnDisable/OnDestroy |
Manual registration = Manual cleanup. Every AddListener in OnEnable MUST have corresponding RemoveListener in OnDisable. Failure to cleanup causes:
- Memory leaks
- Duplicate listener execution
- Listeners executing on destroyed objects (NullReferenceException)
π API Scenariosβ
01 Void Event: Basic Registrationβ
Visual β Code Translation:
- β Inspector: Drag
OnEventReceivedinto Behavior window - β
Code: Call
AddListenerinOnEnable
RuntimeAPI_VoidEventRaiser.cs:
using TinyGiants.GameEventSystem.Runtime;
public class RuntimeAPI_VoidEventRaiser : MonoBehaviour
{
[GameEventDropdown]
public GameEvent voidEvent; // β Still uses asset reference
public void RaiseBasicEvent()
{
if (voidEvent) voidEvent.Raise(); // β Identical to visual workflow
}
}
RuntimeAPI_VoidEventReceiver.cs:
using TinyGiants.GameEventSystem.Runtime;
public class RuntimeAPI_VoidEventReceiver : MonoBehaviour
{
[GameEventDropdown]
public GameEvent voidEvent;
[SerializeField] private Rigidbody targetRigidbody;
// β
REGISTER: When enabled
private void OnEnable()
{
voidEvent.AddListener(OnEventReceived); // β Replaces Inspector binding
}
// β
CLEANUP: When disabled
private void OnDisable()
{
voidEvent.RemoveListener(OnEventReceived); // β MANDATORY cleanup
}
// Listener method (same as visual workflow)
public void OnEventReceived()
{
// Apply physics...
targetRigidbody.AddForce(Vector3.up * 5f, ForceMode.Impulse);
}
}
Key Points:
- π― Event Asset: Still referenced via
[GameEventDropdown] - π Registration:
AddListener(MethodName)inOnEnable - π§Ή Cleanup:
RemoveListener(MethodName)inOnDisable - β‘ Signature: Method must match event type (
voidforGameEvent)
02 Basic Types: Generic Registrationβ
Demonstrates: Type inference for generic events
RuntimeAPI_BasicTypesEventRaiser.cs:
[GameEventDropdown] public GameEvent<string> messageEvent;
[GameEventDropdown] public GameEvent<Vector3> movementEvent;
[GameEventDropdown] public GameEvent<GameObject> spawnEvent;
[GameEventDropdown] public GameEvent<Material> changeMaterialEvent;
public void RaiseString()
{
messageEvent.Raise("Hello World"); // β Type inferred from event
}
public void RaiseVector3()
{
movementEvent.Raise(new Vector3(0, 2, 0));
}
RuntimeAPI_BasicTypesEventReceiver.cs:
private void OnEnable()
{
// Compiler infers <string>, <Vector3>, etc. from method signatures
messageEvent.AddListener(OnMessageReceived); // void(string)
movementEvent.AddListener(OnMoveReceived); // void(Vector3)
spawnEvent.AddListener(OnSpawnReceived); // void(GameObject)
changeMaterialEvent.AddListener(OnMaterialReceived); // void(Material)
}
private void OnDisable()
{
messageEvent.RemoveListener(OnMessageReceived);
movementEvent.RemoveListener(OnMoveReceived);
spawnEvent.RemoveListener(OnSpawnReceived);
changeMaterialEvent.RemoveListener(OnMaterialReceived);
}
public void OnMessageReceived(string msg) { /* ... */ }
public void OnMoveReceived(Vector3 pos) { /* ... */ }
public void OnSpawnReceived(GameObject prefab) { /* ... */ }
public void OnMaterialReceived(Material mat) { /* ... */ }
Key Points:
- β Type Safety: Compiler enforces signature match
- β Auto-Inference: No manual type specification needed
- β οΈ Mismatch Error:
void(int)cannot bind toGameEvent<string>
03 Custom Type: Complex Data Bindingβ
Demonstrates: Auto-generated generic classes
RuntimeAPI_CustomTypeEventRaiser.cs:
[GameEventDropdown] public GameEvent<DamageInfo> physicalDamageEvent;
[GameEventDropdown] public GameEvent<DamageInfo> fireDamageEvent;
[GameEventDropdown] public GameEvent<DamageInfo> criticalStrikeEvent;
public void DealPhysicalDamage()
{
DamageInfo info = new DamageInfo(10f, false, DamageType.Physical, hitPoint, "Player01");
physicalDamageEvent.Raise(info); // β Custom class as argument
}
RuntimeAPI_CustomTypeEventReceiver.cs:
private void OnEnable()
{
// Bind multiple events to same handler
physicalDamageEvent.AddListener(OnDamageReceived);
fireDamageEvent.AddListener(OnDamageReceived);
criticalStrikeEvent.AddListener(OnDamageReceived);
}
private void OnDisable()
{
physicalDamageEvent.RemoveListener(OnDamageReceived);
fireDamageEvent.RemoveListener(OnDamageReceived);
criticalStrikeEvent.RemoveListener(OnDamageReceived);
}
public void OnDamageReceived(DamageInfo info)
{
// Parse custom class fields
float damage = info.amount;
DamageType type = info.type;
bool isCrit = info.isCritical;
// Apply logic based on data...
}
Key Points:
- π¦ Auto-Generated:
GameEvent<DamageInfo>class created by plugin - π Multiple Bindings: Same method can listen to multiple events
- β‘ Data Access: Full access to custom class properties
04 Custom Sender: Dual-Generic Listenersβ
Demonstrates: Accessing event source context
RuntimeAPI_CustomSenderTypeEventRaiser.cs:
// Physical sender: GameObject
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> turretEvent;
// Logical sender: Custom class
[GameEventDropdown] public GameEvent<PlayerStats, DamageInfo> systemEvent;
public void RaiseTurretDamage()
{
DamageInfo info = new DamageInfo(15f, false, DamageType.Physical, hitPoint, "Turret");
turretEvent.Raise(this.gameObject, info); // β Pass sender as first arg
}
public void RaiseSystemDamage()
{
PlayerStats admin = new PlayerStats("DragonSlayer_99", 99, 1);
DamageInfo info = new DamageInfo(50f, true, DamageType.Void, hitPoint, "Admin");
systemEvent.Raise(admin, info); // β Custom class as sender
}
RuntimeAPI_CustomSenderTypeEventReceiver.cs:
private void OnEnable()
{
turretEvent.AddListener(OnTurretAttackReceived); // (GameObject, DamageInfo)
systemEvent.AddListener(OnSystemAttackReceived); // (PlayerStats, DamageInfo)
}
private void OnDisable()
{
turretEvent.RemoveListener(OnTurretAttackReceived);
systemEvent.RemoveListener(OnSystemAttackReceived);
}
// Signature: void(GameObject, DamageInfo)
public void OnTurretAttackReceived(GameObject sender, DamageInfo args)
{
Vector3 attackerPos = sender.transform.position; // β Access sender GameObject
// React to physical attacker...
}
// Signature: void(PlayerStats, DamageInfo)
public void OnSystemAttackReceived(PlayerStats sender, DamageInfo args)
{
string attackerName = sender.playerName; // β Access sender data
int factionId = sender.factionId;
// React to logical attacker...
}
Key Points:
- π― Context Awareness: Listeners know WHO triggered the event
- π Flexible Senders: GameObject OR custom class
- β‘ Signature Match: Method params MUST match event generics
05 Priority: Execution Order Controlβ
Visual β Code Translation:
- β Inspector: Drag to reorder listeners in Behavior window
- β
Code: Specify
priorityparameter (higher = earlier)
RuntimeAPI_PriorityEventReceiver.cs:
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> orderedHitEvent;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> chaoticHitEvent;
private void OnEnable()
{
// β
ORDERED: High priority executes FIRST
orderedHitEvent.AddPriorityListener(ActivateBuff, priority: 100); // Runs 1st
orderedHitEvent.AddPriorityListener(ResolveHit, priority: 50); // Runs 2nd
// β CHAOTIC: Wrong order intentionally
chaoticHitEvent.AddPriorityListener(ResolveHit, priority: 80); // Runs 1st (too early!)
chaoticHitEvent.AddPriorityListener(ActivateBuff, priority: 40); // Runs 2nd (too late!)
}
private void OnDisable()
{
// MUST remove priority listeners specifically
orderedHitEvent.RemovePriorityListener(ActivateBuff);
orderedHitEvent.RemovePriorityListener(ResolveHit);
chaoticHitEvent.RemovePriorityListener(ResolveHit);
chaoticHitEvent.RemovePriorityListener(ActivateBuff);
}
public void ActivateBuff(GameObject sender, DamageInfo args)
{
_isBuffActive = true; // β Must run BEFORE ResolveHit
}
public void ResolveHit(GameObject sender, DamageInfo args)
{
float damage = _isBuffActive ? args.amount * 5f : args.amount; // β Checks buff state
}
Key Points:
- π’ Priority Values: Higher numbers = earlier execution
- β οΈ Order Matters:
ActivateBuff(100) β ResolveHit(50)= CRIT HIT - β Wrong Order:
ResolveHit(80) β ActivateBuff(40)= Normal hit - π§Ή Cleanup: Use
RemovePriorityListener(notRemoveListener)
06 Conditional: Predicate-Based Filteringβ
Visual β Code Translation:
- β Inspector: Visual Condition Tree in Behavior window
- β
Code: Predicate function passed to
AddConditionalListener
RuntimeAPI_ConditionalEventReceiver.cs:
[GameEventDropdown] public GameEvent<AccessCard> requestAccessEvent;
private void OnEnable()
{
// Register with condition function
// OpenVault ONLY called if CanOpen returns true
requestAccessEvent.AddConditionalListener(OpenVault, CanOpen);
}
private void OnDisable()
{
requestAccessEvent.RemoveConditionalListener(OpenVault);
}
// β
CONDITION FUNCTION (Predicate)
// Replaces visual Condition Tree
public bool CanOpen(AccessCard card)
{
return securityGrid.IsPowerOn && (
card.securityLevel >= 4 ||
departments.Contains(card.department) ||
(card.securityLevel >= 1 && Random.Range(0, 100) > 70)
);
}
// β
ACTION (Only executes if condition passed)
public void OpenVault(AccessCard card)
{
// Assumes all conditions met
Debug.Log($"ACCESS GRANTED to {card.holderName}");
StartCoroutine(OpenDoorSequence());
}
Key Points:
- β
Predicate Function: Returns
bool, takes event args - π Gate Keeper: Action ONLY runs if predicate returns
true - π§Ή Cleanup: Use
RemoveConditionalListener(notRemoveListener) - β‘ Evaluation: Predicate runs BEFORE action method
07 Delayed: Scheduling & Cancellationβ
Visual β Code Translation:
- β Behavior: "Action Delay = 5.0s" in Inspector
- β
Code:
event.RaiseDelayed(5f)returnsScheduleHandle
RuntimeAPI_DelayedEventRaiser.cs:
[GameEventDropdown] public GameEvent explodeEvent;
private ScheduleHandle _handle; // β Track the scheduled task
public void ArmBomb()
{
// Schedule event 5 seconds later
_handle = explodeEvent.RaiseDelayed(5f); // β Returns handle
Debug.Log("Bomb armed! 5 seconds to defuse...");
}
public void CutRedWire() => ProcessCut("Red");
public void CutGreenWire() => ProcessCut("Green");
private void ProcessCut(string color)
{
if (color == _safeWireColor)
{
// Cancel the scheduled explosion
explodeEvent.CancelDelayed(_handle); // β Use handle to cancel
Debug.Log("DEFUSED! Event cancelled.");
}
else
{
Debug.LogWarning("Wrong wire! Clock still ticking...");
}
}
Key Points:
- β±οΈ Scheduling:
RaiseDelayed(seconds)queues event - π Handle: Store return value to cancel later
- π Cancellation:
CancelDelayed(handle)removes from queue - β οΈ Timing: Event executes AFTER delay if not cancelled
08 Repeating: Loop Management & Callbacksβ
Visual β Code Translation:
- β Behavior: "Repeat Interval = 1.0s, Repeat Count = 5" in Inspector
- β
Code:
event.RaiseRepeating(interval, count)with callbacks
RuntimeAPI_RepeatingEventRaiser.cs:
[GameEventDropdown] public GameEvent finitePulseEvent;
private ScheduleHandle _handle;
public void ActivateBeacon()
{
// Start loop: 1s interval, 5 times
_handle = finitePulseEvent.RaiseRepeating(interval: 1.0f, count: 5);
// β
HOOK: Triggered every iteration
_handle.OnStep += (currentCount) =>
{
Debug.Log($"Pulse #{currentCount} emitted");
};
// β
HOOK: Triggered when loop finishes naturally
_handle.OnCompleted += () =>
{
Debug.Log("Beacon sequence completed");
UpdateUI("IDLE");
};
// β
HOOK: Triggered when cancelled manually
_handle.OnCancelled += () =>
{
Debug.Log("Beacon interrupted");
UpdateUI("ABORTED");
};
}
public void StopSignal()
{
if (_handle != null)
{
finitePulseEvent.CancelRepeating(_handle); // β Stops loop
}
}
Key Points:
- π Finite Loop:
RaiseRepeating(1.0f, 5)= 5 pulses at 1s intervals - β Infinite Loop:
RaiseRepeating(1.0f, -1)= endless until cancelled - π‘ Callbacks:
OnStep,OnCompleted,OnCancelledevents - π Manual Stop:
CancelRepeating(handle)for infinite loops
09 Persistent: Cross-Scene Listener Survivalβ
Visual β Code Translation:
- β Inspector: Check "Persistent Event" in Behavior window
- β
Code:
AddPersistentListenerinAwake+DontDestroyOnLoad
RuntimeAPI_PersistentEventReceiver.cs:
[GameEventDropdown] public GameEvent fireAEvent; // Persistent
[GameEventDropdown] public GameEvent fireBEvent; // Standard
private void Awake()
{
DontDestroyOnLoad(gameObject); // β Survive scene loads
// β
PERSISTENT LISTENER (Survives scene reload)
fireAEvent.AddPersistentListener(OnFireCommandA);
}
private void OnDestroy()
{
// MUST remove persistent listeners manually
fireAEvent.RemovePersistentListener(OnFireCommandA);
}
private void OnEnable()
{
// β STANDARD LISTENER (Dies with scene)
fireBEvent.AddListener(OnFireCommandB);
}
private void OnDisable()
{
fireBEvent.RemoveListener(OnFireCommandB);
}
public void OnFireCommandA()
{
Debug.Log("Persistent listener survived scene reload");
}
public void OnFireCommandB()
{
Debug.Log("Standard listener (will break after reload)");
}
Key Points:
- 𧬠Singleton Pattern:
DontDestroyOnLoad+ persistent listener - β
Survives Reload:
AddPersistentListenerbinds to global registry - β Standard Dies:
AddListenerbindings destroyed with scene - π§Ή Cleanup: Use
OnDestroyfor persistent,OnDisablefor standard
10 Trigger Event: Building Parallel Graphs in Codeβ
Visual β Code Translation:
- β Flow Graph: Visual nodes and connections
- β
Code:
AddTriggerEvent(target, ...)inOnEnable
RuntimeAPI_TriggerEventRaiser.cs:
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> onCommand; // Root
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> onActiveBuff; // Branch A
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> onTurretFire; // Branch B
[GameEventDropdown] public GameEvent<DamageInfo> onHoloData; // Branch C (type conversion)
[GameEventDropdown] public GameEvent onGlobalAlarm; // Branch D (void)
private TriggerHandle _buffAHandle;
private TriggerHandle _fireAHandle;
private TriggerHandle _holoHandle;
private TriggerHandle _alarmHandle;
private void OnEnable()
{
// β
BUILD PARALLEL GRAPH IN CODE
// Branch A: Buff (Priority 100, Conditional)
_buffAHandle = onCommand.AddTriggerEvent(
targetEvent: onActiveBuff,
delay: 0f,
condition: (sender, args) => sender == turretA, // β Only Turret A
passArgument: true,
priority: 100 // β High priority
);
// Branch B: Fire (Priority 50, Conditional)
_fireAHandle = onCommand.AddTriggerEvent(
targetEvent: onTurretFire,
delay: 0f,
condition: (sender, args) => sender == turretA,
passArgument: true,
priority: 50 // β Lower priority (runs after buff)
);
// Branch C: Holo Data (Type conversion, Delayed)
_holoHandle = onCommand.AddTriggerEvent(
targetEvent: onHoloData, // β GameEvent<DamageInfo> (no sender)
delay: 1f, // β 1 second delay
passArgument: true
);
// Branch D: Global Alarm (Void conversion)
_alarmHandle = onCommand.AddTriggerEvent(
targetEvent: onGlobalAlarm // β GameEvent (void, no args)
);
// β
HOOK: Callback when trigger fires
_buffAHandle.OnTriggered += () => Debug.Log("Buff triggered via code graph");
}
private void OnDisable()
{
// β
CLEANUP: MANDATORY for dynamic triggers
onCommand.RemoveTriggerEvent(_buffAHandle);
onCommand.RemoveTriggerEvent(_fireAHandle);
onCommand.RemoveTriggerEvent(_holoHandle);
onCommand.RemoveTriggerEvent(_alarmHandle);
}
Graph Visualization (Code-Defined):
π‘ Root: onCommand.Raise(sender, info)
β
ββ π± [ Branch: Unit A ] β π‘οΈ Guard: `Sender == Turret_A`
β ββ π [Prio: 100] β π‘οΈ onActiveBuff() β
High-Priority Sync
β ββ β‘ [Prio: 50 ] β π₯ onTurretFire() β
Sequential Action
β
ββ π± [ Branch: Analytics ] β π’ Signature: `<DamageInfo>`
β ββ β±οΈ [ Delay: 1.0s ] β π½οΈ onHoloData() β
Delayed Data Relay
β
ββ π± [ Branch: Global ] β π Signature: `<void>`
ββ π [ Instant ] β π¨ onGlobalAlarm() β
Immediate Signal
Key Points:
- π³ Parallel Execution: All branches evaluate simultaneously
- π’ Priority: Controls execution order within passing branches
- β Conditions: Predicate functions filter by sender/args
- π Type Conversion: Automatic argument adaptation
- π‘ Callbacks:
OnTriggeredevent per handle - π§Ή Cleanup:
RemoveTriggerEvent(handle)REQUIRED
11 Chain Event: Building Sequential Pipelines in Codeβ
Visual β Code Translation:
- β Flow Graph: Linear node sequence
- β
Code:
AddChainEvent(target, ...)inOnEnable
RuntimeAPI_ChainEventRaiser.cs:
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnStartSequenceEvent; // Root
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnSystemCheckEvent; // Step 1
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnChargeEvent; // Step 2
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnFireEvent; // Step 3
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnCoolDownEvent; // Step 4
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnArchiveEvent; // Step 5
private ChainHandle _checkHandle;
private ChainHandle _chargeHandle;
private ChainHandle _fireHandle;
private ChainHandle _cooldownHandle;
private ChainHandle _archiveHandle;
private void OnEnable()
{
// β
BUILD SEQUENTIAL CHAIN IN CODE
// Step 1: System Check (Conditional gate)
_checkHandle = OnStartSequenceEvent.AddChainEvent(
targetEvent: OnSystemCheckEvent,
delay: 0f,
duration: 0f,
condition: (sender, args) => chainEventReceiver.IsSafetyCheckPassed, // β Gate
passArgument: true,
waitForCompletion: false
);
// Step 2: Charge (1 second duration)
_chargeHandle = OnStartSequenceEvent.AddChainEvent(
targetEvent: OnChargeEvent,
delay: 0f,
duration: 1f, // β Chain pauses here for 1s
passArgument: true
);
// Step 3: Fire (Instant)
_fireHandle = OnStartSequenceEvent.AddChainEvent(
targetEvent: OnFireEvent,
passArgument: true
);
// Step 4: Cool Down (0.5s delay + 1s duration + wait for completion)
_cooldownHandle = OnStartSequenceEvent.AddChainEvent(
targetEvent: OnCoolDownEvent,
delay: 0.5f, // β Pre-delay
duration: 1f, // β Duration after action
passArgument: true,
waitForCompletion: true // β Waits for receiver coroutines
);
// Step 5: Archive (Arguments blocked)
_archiveHandle = OnStartSequenceEvent.AddChainEvent(
targetEvent: OnArchiveEvent,
passArgument: false // β Downstream receives null/default
);
}
private void OnDisable()
{
// β
CLEANUP: MANDATORY for dynamic chains
OnStartSequenceEvent.RemoveChainEvent(_checkHandle);
OnStartSequenceEvent.RemoveChainEvent(_chargeHandle);
OnStartSequenceEvent.RemoveChainEvent(_fireHandle);
OnStartSequenceEvent.RemoveChainEvent(_cooldownHandle);
OnStartSequenceEvent.RemoveChainEvent(_archiveHandle);
// Alternative: OnStartSequenceEvent.RemoveAllChainEvents();
}
Pipeline Visualization (Code-Defined):
π [ ROOT ] OnStartSequenceEvent
β
ββ π‘οΈ [ GUARD ] β Safety Check
β βββΊ βοΈ OnSystemCheckEvent β
Condition Passed
β
ββ β±οΈ [ FLOOR ] β Duration: 1.0s
β βββΊ β‘ OnChargeEvent β
Minimum Pacing Met
β
ββ π [ INSTANT ] β Immediate Trigger
β βββΊ π₯ OnFireEvent β
Executed
β
ββ β [ ASYNC ] β Delay: 0.5s | Dur: 1.0s | Wait: ON
β βββΊ βοΈ OnCoolDownEvent β
Async Recovery Done
β
ββ π§Ή [ FILTER ] β Block Arguments
βββΊ πΎ OnArchiveEvent β
Data Cleaned & Saved
Key Points:
- π Sequential Execution: Steps run one-by-one, not parallel
- β Conditional Gate: Failed condition terminates entire chain
- β±οΈ Duration: Chain pauses for specified time
- π Wait For Completion: Blocks until receiver coroutines finish
- π Argument Blocking:
passArgument: falsesends default values - π§Ή Cleanup:
RemoveChainEvent(handle)orRemoveAllChainEvents()
π API Reference Summaryβ
Listener Registrationβ
| Method | Use Case | Cleanup Method |
|---|---|---|
AddListener(method) | Standard binding | RemoveListener(method) |
AddPriorityListener(method, priority) | Execution order control | RemovePriorityListener(method) |
AddConditionalListener(method, predicate) | Predicate-based filtering | RemoveConditionalListener(method) |
AddPersistentListener(method) | Cross-scene survival | RemovePersistentListener(method) |
Event Raisingβ
| Method | Use Case | Returns |
|---|---|---|
Raise() | Immediate execution | void |
Raise(arg) | With single argument | void |
Raise(sender, arg) | With sender context | void |
RaiseDelayed(seconds) | Scheduled execution | ScheduleHandle |
RaiseRepeating(interval, count) | Loop execution | ScheduleHandle |
Schedule Managementβ
| Method | Use Case |
|---|---|
CancelDelayed(handle) | Stop pending delayed event |
CancelRepeating(handle) | Stop active loop |
handle.OnStep | Loop iteration callback |
handle.OnCompleted | Loop completion callback |
handle.OnCancelled | Cancellation callback |
Flow Graph Constructionβ
| Method | Use Case | Returns |
|---|---|---|
AddTriggerEvent(target, ...) | Parallel branch | TriggerHandle |
RemoveTriggerEvent(handle) | Remove branch | void |
AddChainEvent(target, ...) | Sequential step | ChainHandle |
RemoveChainEvent(handle) | Remove step | void |
RemoveAllChainEvents() | Clear all steps | void |
β οΈ Critical Best Practicesβ
β DOβ
private void OnEnable()
{
myEvent.AddListener(OnReceived); // β Register
}
private void OnDisable()
{
myEvent.RemoveListener(OnReceived); // β ALWAYS cleanup
}
β DON'Tβ
private void Start()
{
myEvent.AddListener(OnReceived); // β Registered in Start...
}
// β NO OnDisable cleanup β MEMORY LEAK
Handle Managementβ
private ScheduleHandle _handle;
public void StartLoop()
{
_handle = myEvent.RaiseRepeating(1f, -1);
}
public void StopLoop()
{
if (_handle != null) myEvent.CancelRepeating(_handle); // β Use stored handle
}
Lifecycle Patternsβ
| Lifecycle Method | Use For |
|---|---|
Awake | Persistent listeners + DontDestroyOnLoad |
OnEnable | Standard listeners, triggers, chains |
OnDisable | Remove standard listeners |
OnDestroy | Remove persistent listeners |
π― When to Choose Code vs Visualβ
Choose Visual Workflow When:β
- β Designers need direct control
- β Rapid iteration is priority
- β Logic is relatively static
- β Visual debugging is beneficial
- β Team collaboration across disciplines
Choose Code Workflow When:β
- β Logic is highly dynamic (runtime graph building)
- β Conditions require complex C# code
- β Integration with existing code systems
- β Advanced scheduling patterns
- β Programmatic listener management
- β Version control of logic (code diffs clearer than .asset diffs)
Hybrid Approach:β
- π¨ Visual: Event definitions, simple bindings
- π» Code: Complex conditions, dynamic graphs, runtime scheduling
- Example: Define events visually, but build Trigger/Chain graphs in code for procedural systems
π Related Documentationβ
- Raising and Scheduling - Complete scheduling API guide
- Listening Strategies - Listener patterns and best practices
- Programmatic Flow - Building Trigger/Chain graphs via code
- Best Practices - Code patterns and anti-patterns
- API Reference - Complete method signatures