Skip to main content

13 Runtime API: The Code-Driven Workflow

πŸ“ Demo Info​

  • Scene Path

    Assets/TinyGiants/GameEventSystem/Demo/13_RuntimeAPI/**.unity

    (Note: This folder contains variants of all previous demos, refactored to use the Runtime API.)

    Goal

    To demonstrate that everything you can do in the Inspector (Visual Binding, Logic Graphs, Delays) can also be done purely via Code. This chapter revisits scenarios 01-11, replacing Inspector configuration with C# API calls to prove the system's flexibility for programmers.


πŸ“ Description​

Visual vs. Code: The Great Divide In previous demos, we used the Inspector to bind listeners and the Flow Graph to create logic. This is great for designers. However, programmers often prefer full control in IDEs.

The Paradigm Shift In this chapter, the Event Assets (.asset) are still used as "Keys" or "Channels", but ALL Logic and Bindings are moved into C# scripts.

FeatureVisual Workflow (Prev)Code Workflow (This Demo)
BindingInspector Drag & Dropevent.AddListener(...)
LogicCondition Nodes (Graph)event.AddConditionalListener(...)
TimingDelay Nodes (Graph)event.RaiseDelayed(...)
FlowConnections (Graph)event.AddTriggerEvent(...)

01 Void Event (Code Binding)​

Scenario: The Receiver manually registers itself using AddListener, replacing the Inspector binding.

The Raiser (RuntimeAPI_VoidEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_VoidEventRaiser : MonoBehaviour
{
[Header("GameEvent")]
[GameEventDropdown] public GameEvent voidEvent;

public void RaiseBasicEvent()
{
if (voidEvent == null) return;

// API: Standard execution
voidEvent.Raise();
Debug.Log("[VoidEvent] Raise() called on GameEvent.");
}
}

The Receiver (RuntimeAPI_VoidEventReceiver.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_VoidEventReceiver : MonoBehaviour
{
[Header("GameEvent")]
[GameEventDropdown] public GameEvent voidEvent;

[SerializeField] private Rigidbody targetRigidbody;

private void OnEnable()
{
// Note: Register event listener via API
voidEvent.AddListener(OnEventReceived);
}

private void OnDisable()
{
// Note: Always remove listener to prevent memory leaks
voidEvent.RemoveListener(OnEventReceived);
}

public void OnEventReceived()
{
Debug.Log("[VoidEvent] OnEventReceived() called on GameEvent.");
// ... Physics Logic ...
}
}

02 Basic Types (Generics)​

Scenario: Using Generic GameEvent<T> via code. The API automatically infers the type <T>.

The Raiser (RuntimeAPI_BasicTypesEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_BasicTypesEventRaiser : MonoBehaviour
{
[GameEventDropdown] public GameEvent<string> messageEvent;
[GameEventDropdown] public GameEvent<Vector3> movementEvent;
[GameEventDropdown] public GameEvent<GameObject> spawnEvent;
[GameEventDropdown] public GameEvent<Material> changeMaterialEvent;

public void RaiseString()
{
// Raises event with a dynamic string argument
messageEvent.Raise($"Hello World");
}

public void RaiseVector3()
{
// Calculates a random position and sends it
movementEvent.Raise(new Vector3(0, 2, 0));
}

// ... RaiseGameObject and RaiseMaterial follow the same pattern
}

The Receiver (RuntimeAPI_BasicTypesEventReceiver.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_BasicTypesEventReceiver : MonoBehaviour
{
[Header("GameEvent")]
[GameEventDropdown] public GameEvent<string> messageEvent;
[GameEventDropdown] public GameEvent<Vector3> movementEvent;
[GameEventDropdown] public GameEvent<GameObject> spawnEvent;
[GameEventDropdown] public GameEvent<Material> changeMaterialEvent;

private void OnEnable()
{
// Note: Register generic listeners
// The compiler infers <string>, <Vector3>, etc.
messageEvent.AddListener(OnMessageReceived);
movementEvent.AddListener(OnMoveReceived);
spawnEvent.AddListener(OnSpawnReceived);
changeMaterialEvent.AddListener(OnMaterialReceived);
}

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) { ... }
}

03 Custom Type (Complex Data)​

Scenario: Passing the DamageInfo class. Code binding works seamlessly with auto-generated types.

The Raiser (RuntimeAPI_CustomEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_CustomEventRaiser : MonoBehaviour
{
[GameEventDropdown] public GameEvent<DamageInfo> fireDamageEvent;

public void DealFireDamage()
{
// Construct the data packet
DamageInfo info = new DamageInfo(
25f, false, DamageType.Fire, Vector3.zero, "Player02"
);

// Raise with custom class
fireDamageEvent.Raise(info);
}
}

The Receiver (RuntimeAPI_CustomTypeEventReceiver.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_CustomTypeEventReceiver : MonoBehaviour
{
[Header("GameEvent")]
[GameEventDropdown] public GameEvent<DamageInfo> physicalDamageEvent;
[GameEventDropdown] public GameEvent<DamageInfo> fireDamageEvent;
[GameEventDropdown] public GameEvent<DamageInfo> criticalStrikeEvent;

private void OnEnable()
{
// Binding multiple events to the 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)
{
// Parsing logic (Type check, Crit check, Physics...)
}
}

04 Custom Sender (Context)​

Scenario: Using the Dual-Generic AddListener<TSender, TArgs> to access the event source.

The Raiser (RuntimeAPI_CustomSenderTypeEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_CustomSenderTypeEventRaiser : MonoBehaviour
{
// Scenario A: Physical Sender <GameObject, DamageInfo>
[GameEventDropdown]
public GameEvent<GameObject, DamageInfo> turretEvent;

// Scenario B: Logical Sender <PlayerStats, DamageInfo>
[GameEventDropdown]
public GameEvent<PlayerStats, DamageInfo> systemEvent;

public void RaiseTurretDamage()
{
// Pass 'this.gameObject' as Sender
turretEvent.Raise(this.gameObject, new DamageInfo(15, ...));
}

public void RaiseSystemDamage()
{
// Create a virtual sender on the fly
PlayerStats adminStats = new PlayerStats("DragonSlayer_99", 99, 1);

// Pass the C# object as the sender
systemEvent.Raise(adminStats, new DamageInfo(50, ...));
}
}

The Receiver (RuntimeAPI_CustomSenderTypeEventReceiver.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_CustomSenderTypeEventReceiver : MonoBehaviour
{
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> attackEvent1;
[GameEventDropdown] public GameEvent<PlayerStats, DamageInfo> globalSystemEvent;

private void OnEnable()
{
attackEvent1.AddListener(OnTurretAttackReceived);
globalSystemEvent.AddListener(OnSystemAttackReceived);
}

private void OnDisable()
{
attackEvent1.RemoveListener(OnTurretAttackReceived);
globalSystemEvent.RemoveListener(OnSystemAttackReceived);
}

// Signature Match: void (GameObject, DamageInfo)
public void OnTurretAttackReceived(GameObject sender, DamageInfo args)
{
// Access sender.transform for logic
}

// Signature Match: void (PlayerStats, DamageInfo)
public void OnSystemAttackReceived(PlayerStats sender, DamageInfo args)
{
// Access sender.playerName for UI
}
}

05 Priority (Sorting)​

Scenario: Using AddPriorityListener to control execution order (Buff before Damage).

The Receiver (RuntimeAPI_PriorityEventReceiver.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_PriorityEventReceiver : MonoBehaviour
{
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> orderedHitEvent;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> chaoticHitEvent;

private void OnEnable()
{
// ORDERED: High Priority first
// 1. Activate Buff (100)
orderedHitEvent.AddPriorityListener(ActivateBuff, 100);
// 2. Resolve Hit (50)
orderedHitEvent.AddPriorityListener(ResolveHit, 50);

// CHAOTIC: Wrong order intentionally
// 1. Resolve Hit (80) - Runs too early!
chaoticHitEvent.AddPriorityListener(ResolveHit, 80);
// 2. Activate Buff (40) - Runs too late!
chaoticHitEvent.AddPriorityListener(ActivateBuff, 40);
}

private void OnDisable()
{
// Must remove priority listeners specifically
orderedHitEvent.RemovePriorityListener(ActivateBuff);
orderedHitEvent.RemovePriorityListener(ResolveHit);

chaoticHitEvent.RemovePriorityListener(ActivateBuff);
chaoticHitEvent.RemovePriorityListener(ResolveHit);
}

public void ActivateBuff(GameObject sender, DamageInfo args) { ... }
public void ResolveHit(GameObject sender, DamageInfo args) { ... }
}

06 Conditional (Predicates)​

Scenario: Using AddConditionalListener to filter logic without if statements inside the callback.

The Receiver (RuntimeAPI_ConditionalEventReceiver.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_ConditionalEventReceiver : MonoBehaviour
{
[GameEventDropdown] public GameEvent<AccessCard> requestAccessEvent;

private void OnEnable()
{
// API: Register with a Condition Function (CanOpen)
// The listener 'OpenVault' is ONLY called if 'CanOpen' returns true.
requestAccessEvent.AddConditionalListener(OpenVault, CanOpen);
}

private void OnDisable()
{
requestAccessEvent.RemoveConditionalListener(OpenVault);
}

// The Predicate Logic (Returns bool)
// Replaces the Visual Logic Tree from the Inspector
public bool CanOpen(AccessCard card)
{
return securityGrid.IsPowerOn && (
card.securityLevel >= 4 ||
departments.Contains(card.department) ||
(card.securityLevel >= 1 && UnityEngine.Random.Range(0, 100) > 70)
);
}

public void OpenVault(AccessCard card)
{
// Logic assumes conditions are met
Debug.Log($"ACCESS GRANTED to {card.holderName}");
}
}

07 Delayed (Manual Scheduling)​

Scenario: Using RaiseDelayed and managing ScheduleHandle for cancellation.

The Raiser (RuntimeAPI_DelayedEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_DelayedEventRaiser : MonoBehaviour
{
[GameEventDropdown] public GameEvent explodeEvent;

private ScheduleHandle _handle; // Handle is required for cancellation

public void ArmBomb()
{
// API: Schedule event 5 seconds later
// Stores the handle so we can refer to this specific task later
_handle = explodeEvent.RaiseDelayed(5f);
}

private void ProcessCut(string color)
{
if (color == _safeWireColor)
{
// API: Cancel the specific delayed task using the handle
explodeEvent.CancelDelayed(_handle);
DisarmSuccess();
}
}
}

08 Repeating (Callbacks)​

Scenario: Using RaiseRepeating and hooking into lifecycle callbacks.

The Raiser (RuntimeAPI_RepeatingEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_RepeatingEventRaiser : MonoBehaviour
{
[GameEventDropdown] public GameEvent finitePulseEvent;
private ScheduleHandle _handle;

public void ActivateBeacon()
{
// API: Start the loop (Interval 1s, Count 5)
_handle = finitePulseEvent.RaiseRepeating(1.0f, 5);

// API Hook: Triggers every time the event fires
_handle.OnStep += (count) => Debug.Log($"[Callback] Loop Step: {count}");

// API Hook: Triggers when the loop finishes naturally
_handle.OnCompleted += () => Debug.Log("[Callback] Loop Completed");

// API Hook: Triggers when cancelled manually
_handle.OnCancelled += () => Debug.Log("[Callback] Loop Cancelled");
}

public void StopSignal()
{
// API: Stop the specific loop
if (_handle != null)
{
finitePulseEvent.CancelRepeating(_handle);
}
}
}

09 Persistent (Cross-Scene)​

Scenario: Registering listeners that survive scene loads via AddPersistentListener.

The Receiver (RuntimeAPI_PersistentEventReceiver.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_PersistentEventReceiver : MonoBehaviour
{
[GameEventDropdown] public GameEvent fireAEvent; // Persistent
[GameEventDropdown] public GameEvent fireBEvent; // Standard

private void Awake()
{
DontDestroyOnLoad(gameObject);

// API: Register Persistent Listener (Survives Scene Load)
// This registers to the Global Manager, not the local delegate.
fireAEvent.AddPersistentListener(OnFireCommandA);
}

private void OnDestroy()
{
// API: Must remove persistent listeners manually
fireAEvent.RemovePersistentListener(OnFireCommandA);
}

private void OnEnable()
{
// Standard Listener (Dies with Scene)
fireBEvent.AddListener(OnFireCommandB);
}

public void OnFireCommandA() { ... }
public void OnFireCommandB() { ... }
}

10 Trigger Event (Graph in Code)​

Scenario: Constructing a Parallel Execution Graph entirely in OnEnable.

The Raiser (RuntimeAPI_TriggerEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_TriggerEventRaiser : MonoBehaviour
{
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> onCommand;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> onActiveBuff;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> onTurretFire;
[GameEventDropdown] public GameEvent<DamageInfo> onHoloData;
[GameEventDropdown] public GameEvent onGlobalAlarm;

// Handles to track dynamic connections
private TriggerHandle onActiveBuffAHandle;
private TriggerHandle onTurretFireAHandle;
// ... (other handles)

private void OnEnable()
{
// 1. Wiring Turret A (Smart Logic)
// AddTriggerEvent(target, delay, condition, passArg, transformer, priority)

// High Priority (100): Activate Buff
onActiveBuffAHandle = onCommand.AddTriggerEvent(
onActiveBuff,
condition: (sender, info) => sender == turretA,
priority: 100
);

// Medium Priority (50): Fire Turret
onTurretFireAHandle = onCommand.AddTriggerEvent(
onTurretFire,
condition: (sender, info) => sender == turretA,
priority: 50
);

// 2. Global Nodes
// Holo Data (Delay 1s)
onHoloDataHandle = onCommand.AddTriggerEvent(onHoloData, delay: 1f);

// Global Alarm (Void target)
onGlobalAlarmHandle = onCommand.AddTriggerEvent(onGlobalAlarm);

SetupHandleCallbacks();
}

private void OnDisable()
{
// Cleanup is mandatory for dynamic triggers
onCommand.RemoveTriggerEvent(onActiveBuffAHandle);
onCommand.RemoveTriggerEvent(onTurretFireAHandle);
onCommand.RemoveTriggerEvent(onHoloDataHandle);
onCommand.RemoveTriggerEvent(onGlobalAlarmHandle);
}

private void SetupHandleCallbacks()
{
// You can even hook into the graph execution!
onActiveBuffAHandle.OnTriggered += () => Debug.Log("[Callback] Buff Triggered via Code Graph");
}
}

11 Chain Event (Sequence in Code)​

Scenario: Constructing a Sequential Execution Pipeline entirely in OnEnable.

The Raiser (RuntimeAPI_ChainEventRaiser.cs)​

using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;

public class RuntimeAPI_ChainEventRaiser : MonoBehaviour
{
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnStartSequenceEvent;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnSystemCheckEvent;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnChargeEvent;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnFireEvent;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnCoolDownEvent;
[GameEventDropdown] public GameEvent<GameObject, DamageInfo> OnArchiveEvent;

private ChainHandle onSystemCheckHandle;
private ChainHandle onChargeHandle;
private ChainHandle onFireHandle;
private ChainHandle onCoolDownHandle;
private ChainHandle onArchiveHandle;

private void OnEnable()
{
// Step 1: System Check (Conditional Gate)
// If condition fails, the chain stops here.
onSystemCheckHandle = OnStartSequenceEvent.AddChainEvent(
OnSystemCheckEvent,
condition: (o, info) => chainEventReceiver.IsSafetyCheckPassed
);

// Step 2: Charge (Duration 1.0s)
// Blocks the chain for 1 second after firing.
onChargeHandle = OnStartSequenceEvent.AddChainEvent(
OnChargeEvent,
duration: 1f
);

// Step 3: Fire (Instant)
onFireHandle = OnStartSequenceEvent.AddChainEvent(OnFireEvent);

// Step 4: Cooldown (Wait For Completion + Delay)
// Waits 0.5s pre-delay, then waits for coroutines to finish, then waits 1s duration.
onCoolDownHandle = OnStartSequenceEvent.AddChainEvent(
OnCoolDownEvent,
delay: 0.5f,
duration: 1f,
waitForCompletion: true
);

// Step 5: Archive (Block Argument)
// PassArgument = false ensures downstream event receives null/default data.
onArchiveHandle = OnStartSequenceEvent.AddChainEvent(
OnArchiveEvent,
passArgument: false
);
}

private void OnDisable()
{
// Clean up individual nodes
OnStartSequenceEvent.RemoveChainEvent(onSystemCheckHandle);
OnStartSequenceEvent.RemoveChainEvent(onChargeHandle);
OnStartSequenceEvent.RemoveChainEvent(onFireHandle);
OnStartSequenceEvent.RemoveChainEvent(onCoolDownHandle);
OnStartSequenceEvent.RemoveChainEvent(onArchiveHandle);

// Alternative: OnStartSequenceEvent.RemoveAllChainEvents();
}
}