07 Delayed Event: The Time Bomb Scenario
Build a defuse minigame to see how an event waits in a pending state after Raise() and how Cancel() pulls it back before it fires.
For developers adding cooldowns, cast times, or any action a player can interrupt.Overviewโ
Standard events fire instantly (Raise() โ Execute()). Delayed events add a gap: Raise() โ [Pending State] โ Execute(). This demo covers the scheduling system through a Cut the Wire minigame, where you configure a delayed execution and cancel the pending event before it runs.
- How to configure action delays in the Behavior Window
- How the event scheduling system works internally
- How to cancel pending events with
.Cancel() - The difference between visual timers and logic timers
Demo Sceneโ
Assets/TinyGiants/GameEventSystem/Demo/07_DelayedEvent/07_DelayedEvent.unity
Scene Compositionโ
Visual Elements:
- ๐ฃ TimeBomb_TNT - Cylindrical bomb in the center
- Black cylinder body with red caps
- Orange timer display showing countdown: "04.046" (updates in real-time)
- Two colored indicator lights (red and green) on top
- Sits on a grey circular platform
UI Layer (Canvas):
- ๐ฎ Three Buttons - Bottom of the screen
- "Arm Bomb" (White) โ Triggers
DelayedEventRaiser.ArmBomb() - "Cut RedWire" (Red/Pink) โ Triggers
DelayedEventRaiser.CutRedWire() - "Cut GreenWire" (Green) โ Triggers
DelayedEventRaiser.CutGreenWire()
- "Arm Bomb" (White) โ Triggers
Game Logic Layer (Demo Scripts):
-
๐ค DelayedEventRaiser - GameObject with the raiser script
- Manages bomb arming and wire cutting logic
- Randomly determines which wire is safe each round
- Controls visual countdown timer (cosmetic)
- Calls
.Cancel()when correct wire is cut
-
๐ฅ DelayedEventReceiver - GameObject with the receiver script
- Listens to
onExplodeEvent - Executes explosion logic: VFX, physics, camera shake
- Only called if timer reaches zero (not canceled)
- Listens to
Audio-Visual Feedback:
- ๐ Tick Sound - Plays every second during countdown
- ๐ฅ Explosion VFX - Particle system spawns on detonation
- โก Wire Sparks - Particle effect when cutting wires
- ๐น Camera Shake - Intense shake on explosion
How to Interactโ
The Defusal Challengeโ
You have 5 seconds to identify and cut the correct wire. One wire is SAFE (cancels the event), the other is a TRAP (does nothing).
The safe wire is randomized every time you arm the bomb! Pay attention to the Console log (or take your chances).
Step 1: Enter Play Modeโ
Press the Play button in Unity. The bomb displays "READY" in white text.
Step 2: Arm the Bombโ
Click "Arm Bomb" (White Button):
What Happens:
- ๐ Ticking sound begins (beep every second)
- โฑ๏ธ Timer starts counting down from
05.000in orange - ๐ฒ System randomly selects safe wire (Red or Green)
- ๐ Console reveals the answer:
[Game Logic] Bomb Armed! The SAFE wire is: Red - ๐ฃ Event enters Pending State - will execute in 5 seconds
Visual Changes:
- Timer text turns from white to orange
- Timer counts down with millisecond precision:
04.987,04.834... - Color gradually shifts from orange โ red as time runs out
Behind the Scenes:
explodeEvent.Raise()is called- Because Action Delay = 5s is configured in the Behavior Window
- The event is queued in the GameEventManager's scheduler
- A countdown timer starts internally
Step 3: Choose Your Fateโ
You now have three options with very different outcomes:
Option A: Do Nothing (Let It Explode)โ
Action: Don't click any button. Wait.
Timeline:
04.000- Second tick sound03.000- Tick, timer turns more red02.000- Tick, urgency builds01.000- Final tick00.000- BOOM!
Result: ๐ฅ EXPLOSION
- Console:
BOOM! The event executed. - Massive explosion VFX spawns at bomb location
- Bomb cylinder becomes kinetic and launches into the air
- Camera shakes violently (0.5s duration, 0.8 magnitude)
- Explosion sound plays
- Timer text changes to "ERROR" in dark red
Why: The 5-second delay elapsed, so DelayedEventReceiver.OnExplode() was invoked by the scheduler.
Option B: Cut the Wrong Wireโ
Action: Click the button that is NOT the safe wire.
Example: If Console said The SAFE wire is: Red, click "Cut GreenWire"
What Happens:
- โก Wire sparks VFX plays
- ๐ Wire cutting sound
- ๐ Console:
[Player] Cutting Green Wire... - ๐ Console:
Wrong wire! The clock is still ticking... - โฑ๏ธ Timer continues counting down
- ๐ฃ Event remains in Pending State
Result: Nothing changes. The countdown continues.
- After a few seconds: BOOM! (same as Option A)
- You get to feel the tension of making the wrong choice
Why: The code checks if (color == _safeWireColor), and since it's false, .Cancel() is never called. The scheduler keeps running.
Option C: Cut the Correct Wire (Defuse)โ
Action: Click the button matching the safe wire.
Example: If Console said The SAFE wire is: Red, click "Cut RedWire"
What Happens:
- โก Wire sparks VFX plays
- ๐ Wire cutting sound
- ๐ Console:
[Player] Cutting Red Wire... - ๐ฏ CRITICAL:
explodeEvent.Cancel()is called - โฑ๏ธ Timer stops immediately at current value (e.g.,
03.247) - ๐ Console:
BOMB DEFUSED! Event Cancelled. - โ Timer text changes to "DEFUSED" in green
- ๐ Defuse success sound plays
- ๐ฃ Event removed from Pending State
Result: ๐ข SUCCESS - No Explosion
- The bomb is safe
DelayedEventReceiver.OnExplode()is NEVER CALLED- You can arm the bomb again for another round
Why: .Cancel() removes the scheduled event from the GameEventManager's internal queue. When the 5-second timer would have elapsed, there's nothing to execute.
Scene Architectureโ
The Scheduling Systemโ
Delayed events use an internal timer managed by the GameEventManager:
๐ Initiation: Raise()
โ
๐ฆ [ Queue Event + Start Timer ]
โ
โณ Status: Waiting...
โ
โโ โก Execution Path (Timer Expired)
โ โโโบ โ
Execute() โ Logic Invoked
โ
โโ ๐ Interruption Path (Manual/Condition)
โโโบ ๐งน Cancel() โ [ Removed from Queue ]
Key Concepts:
- Pending State: Between
Raise()and execution - Scheduler Queue: Internal list of timed events
- Cancellation: Removes event from queue before execution
- Atomic Operation: If canceled, receiver method never runs
Event Definitionโ

| Event Name | Type | Configured Delay |
|---|---|---|
onExplodeEvent | GameEvent (void) | 5.0 seconds |
Behavior Configuration with Delayโ
Click the (void) icon in the Behavior column to open the Behavior Window:

Schedule Configuration Section:
-
โฑ๏ธ Action Delay:
5seconds- This is the time gap between
Raise()and execution - Configurable per-event in the Editor
- No code changes needed to adjust timing
- This is the time gap between
-
๐ Repeat Interval:
0(disabled) -
๐ข Repeat Count:
Infinite Loop(not used in this demo) -
๐พ Persistent Event: Unchecked
Event Action:
- Method:
DelayedEventReceiver.OnExplode() - Mode: Runtime Only
Want to make the bomb countdown faster or slower? Just change the Action Delay value in this window. Try 3 for harder difficulty or 10 for easier!
Sender Setup (DelayedEventRaiser)โ
Select the DelayedEventRaiser GameObject:

Event Channels:
Explode Event:onExplodeEvent- Tooltip: "Configuration: Start Delay = 5.0 seconds"
References:
Bomb Receiver: DelayedEventReceiver (for callback coordination)
Visuals:
Timer Text: TimerText (TextMeshPro) - displays countdownSparks VFX: WireSparksVFX (Particle System) - wire cutting effect
Receiver Setup (DelayedEventReceiver)โ
Select the DelayedEventReceiver GameObject:

References:
Bomb Raiser: DelayedEventRaiser (for state callback)Bomb Rigidbody: TimeBomb_TNT (Rigidbody) - for explosion physics
Visuals:
Explosion VFX Prefab: BombExplosionVFX (Particle System)
Audio:
Tick Clip: BeepSFX (tick sound every second)Explosion Clip: BoomSFX (explosion sound)Defuse Clip: DefuseSFX (success sound)
Code Breakdownโ
DelayedEventRaiser.cs (Sender)โ
using UnityEngine;
using TinyGiants.GES.Runtime;
using System.Collections;
using TMPro;
public class DelayedEventRaiser : MonoBehaviour
{
[Header("Event Channels")]
[Tooltip("Configuration: Start Delay = 5.0 seconds.")]
[GameEventDropdown] public GameEvent explodeEvent;
[Header("Visuals")]
[SerializeField] private TextMeshPro timerText;
private bool _isArmed;
private float _countDownTime = 5.0f;
private string _safeWireColor; // Randomized each round
/// <summary>
/// Button Action: Arms the bomb and starts the delayed event.
/// </summary>
public void ArmBomb()
{
if (_isArmed || explodeEvent == null) return;
_isArmed = true;
// Randomize the puzzle solution
_safeWireColor = Random.value > 0.5f ? "Red" : "Green";
Debug.Log($"[Game Logic] Bomb Armed! The SAFE wire is: " +
$"<color={_safeWireColor.ToLower()}>{_safeWireColor}</color>");
// CRITICAL: Raise the delayed event
// This does NOT execute immediately!
// The event enters "Pending State" for 5 seconds
explodeEvent.Raise();
// Start cosmetic countdown (visual only)
StartCoroutine(CountdownRoutine());
}
/// <summary>
/// Button Action: Player attempts to cut the Red wire.
/// </summary>
public void CutRedWire() => ProcessCut("Red");
/// <summary>
/// Button Action: Player attempts to cut the Green wire.
/// </summary>
public void CutGreenWire() => ProcessCut("Green");
private void ProcessCut(string color)
{
if (!_isArmed) return;
Debug.Log($"[Player] Cutting {color} Wire...");
// Play wire cutting VFX...
// CRITICAL DECISION POINT
if (color == _safeWireColor)
{
// THE MAGIC: Cancel the pending event
// This removes it from the scheduler's queue
// OnExplode() will NEVER be called
explodeEvent.Cancel();
DisarmSuccess();
}
else
{
// Wrong wire - event remains pending
Debug.LogWarning("Wrong wire! The clock is still ticking...");
}
}
private void DisarmSuccess()
{
_isArmed = false;
StopAllCoroutines(); // Stop visual countdown
// Update UI to show success...
Debug.Log("<color=green>BOMB DEFUSED! Event Cancelled.</color>");
}
private IEnumerator CountdownRoutine()
{
// This is PURELY COSMETIC
// The real timer is managed by GameEventManager's scheduler
// Even if this coroutine stops, the bomb would still explode
float _currentTimer = _countDownTime;
while (_currentTimer > 0)
{
_currentTimer -= Time.deltaTime;
if (_currentTimer < 0) _currentTimer = 0;
// Update visual timer text
if (timerText)
{
timerText.text = _currentTimer.ToString("00.000");
// Color lerp from orange to red for urgency
float urgency = 1f - (_currentTimer / _countDownTime);
timerText.color = Color.Lerp(new Color(1f, 0.5f, 0f),
Color.red, urgency);
}
yield return null;
}
}
}
Key Points:
- ๐ฏ Separation of Concerns - Visual timer (coroutine) vs Logic timer (scheduler)
- ๐ฒ Random Selection -
_safeWireColordetermined each round - ๐ด Cancel API -
.Cancel()removes pending event from queue - โฑ๏ธ Cosmetic Countdown - UI updates independently of event system
DelayedEventReceiver.cs (Listener)โ
using UnityEngine;
using System.Collections;
public class DelayedEventReceiver : MonoBehaviour
{
[SerializeField] private Rigidbody bombRigidbody;
[SerializeField] private ParticleSystem explosionVFXPrefab;
[Header("Audio")]
[SerializeField] private AudioClip explosionClip;
private AudioSource _audioSource;
private Camera _mainCamera;
private void Start()
{
_mainCamera = Camera.main;
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null) _audioSource = gameObject.AddComponent<AudioSource>();
}
/// <summary>
/// [Event Callback - Delayed Execution]
///
/// This method is ONLY called if:
/// 1. explodeEvent.Raise() was called
/// 2. 5 seconds elapsed
/// 3. explodeEvent.Cancel() was NOT called during that time
///
/// If the correct wire is cut, this method never runs.
/// </summary>
public void OnExplode()
{
Debug.Log("<color=red><b>BOOM! The event executed.</b></color>");
// Spawn explosion VFX
if (explosionVFXPrefab != null)
{
ParticleSystem vfx = Instantiate(explosionVFXPrefab,
transform.position,
Quaternion.identity);
vfx.Play();
Destroy(vfx.gameObject, 3.0f);
}
// Enable physics on bomb
if (bombRigidbody)
{
bombRigidbody.isKinematic = false;
// Apply explosion force (launches bomb upward)
bombRigidbody.AddExplosionForce(2000f,
transform.position + Vector3.down * 0.5f,
5f);
bombRigidbody.AddTorque(Random.insideUnitSphere * 100f,
ForceMode.Impulse);
}
// Audio + Camera shake
if (explosionClip) _audioSource.PlayOneShot(explosionClip);
StartCoroutine(ShakeCamera(0.5f, 0.8f));
}
private IEnumerator ShakeCamera(float duration, float magnitude)
{
if (_mainCamera == null) yield break;
Vector3 originalPos = _mainCamera.transform.position;
float elapsed = 0f;
while (elapsed < duration)
{
float x = Random.Range(-1f, 1f) * magnitude;
float y = Random.Range(-1f, 1f) * magnitude;
_mainCamera.transform.position = originalPos + new Vector3(x, y, 0);
elapsed += Time.deltaTime;
yield return null;
}
_mainCamera.transform.position = originalPos;
}
}
Key Points:
- ๐ฏ Conditional Execution - Only runs if not canceled
- ๐ฅ Explosion Logic - VFX, physics, audio, camera shake
- ๐ฌ Pure Reaction - No knowledge of timers or cancellation
- โฑ๏ธ Delayed Invocation - Called 5 seconds after
Raise()(if not canceled)
Key Takeawaysโ
| Concept | Implementation |
|---|---|
| โฑ๏ธ Action Delay | Configure execution delay in Behavior Window (no code) |
| ๐ Pending State | Events wait in scheduler queue between Raise and Execute |
| ๐ด Cancellation API | .Cancel() removes event from queue before execution |
| ๐ฏ Atomic Execution | Canceled events never invoke receiver methods |
| ๐จ Visual vs Logic | Separate cosmetic timers from event system timers |
Delayed events are perfect for:
- Timed abilities - Cooldowns, cast times, channeling
- Countdown mechanics - Bombs, buffs expiring, reinforcements arriving
- Cancelable actions - Interrupt casting, defuse mechanics
- Turn-based delays - Wait for animation before next action
- Scheduled events - Day/night cycle triggers, periodic spawns
The .Cancel() API is critical for interactive gameplayโletting players interrupt dangerous actions adds tension and player agency!
Related Documentationโ
- Game Event Behavior - Complete guide to schedule configuration
- Raising and Scheduling - API reference for
.Raise()and.Cancel() - Best Practices - Patterns for timed gameplay mechanics