Skip to main content

08 Repeating Event: Automated Loops

GES ยท Examples

Drive a sonar beacon to learn how a single Raise() runs a whole loop, how finite counts stop on their own, and how Cancel() ends an infinite loop.

For developers building periodic effects like poison ticks, radar sweeps, or spawn waves.
๐Ÿ”ˆ Hover for sound

Overviewโ€‹

Creating a repeating pulse (radar scans, poison damage) normally means writing timer loops with InvokeRepeating or coroutines in C#. The GameEvent System moves that logic into the event asset itself. You configure it once in the Editor, then a single Raise() handles the repetition.

๐Ÿ’ก What You'll Learn
  • How to configure repeat intervals and counts in the Behavior Window
  • The difference between finite loops (N times) and infinite loops (forever)
  • How to cancel infinite loops with .Cancel()
  • When to use repeating events vs manual triggers

Demo Sceneโ€‹

Assets/TinyGiants/GameEventSystem/Demo/08_RepeatingEvent/08_RepeatingEvent.unity

Scene Compositionโ€‹

Visual Elements:

  • ๐Ÿ“ก SonarBeacon - Central tower beacon

    • Black cylindrical tower with grey base
    • RotatingCore - Spinning element at the top (rotation speed indicates active mode)
    • Emits expanding cyan shockwave rings when pulsing
  • ๐ŸŽฏ ScanTargets - Four floating green cubes scattered around the beacon

    • Display "?" text by default
    • Change to red material and show "DETECTED" when hit by shockwave
    • Reset to green after brief highlight
  • ๐Ÿ”ต Cyan Ring - Large circular boundary line

    • Indicates maximum scan range (40 units radius)
    • Visual guide for pulse expansion area

UI Layer (Canvas):

  • ๐ŸŽฎ Three Buttons - Bottom of the screen
    • "Activate Beacon" (White) โ†’ Triggers RepeatingEventRaiser.ActivateBeacon()
    • "Toggle Mode (Finite[5])" โ†’ Triggers RepeatingEventRaiser.ToggleMode()
      • Switches between Finite and Infinite modes
      • Text updates to show current mode
    • "StopSignal" (White) โ†’ Triggers RepeatingEventRaiser.StopSignal()

Game Logic Layer (Demo Scripts):

  • ๐Ÿ“ค RepeatingEventRaiser - GameObject with the raiser script

    • Manages two events: onFinitePulseEvent and onInfinitePulseEvent
    • Switches between modes and controls beacon rotation speed
    • Calls .Raise() onceโ€”system handles repetition automatically
  • ๐Ÿ“ฅ RepeatingEventReceiver - GameObject with the receiver script

    • Listens to pulse events
    • Spawns shockwave VFX and sonar audio
    • Runs physics-based scan routine to detect targets

Audio-Visual Feedback:

  • ๐Ÿ’ซ ShockwaveVFX - Expanding cyan particle ring
  • ๐Ÿ”Š Sonar Ping - Audio pulse on each scan
  • ๐ŸŽต Toggle/Stop Sounds - UI feedback

How to Interactโ€‹

The Two Loop Modesโ€‹

This demo showcases two distinct looping patterns:

Finite Mode (5 Pulses):

  • Interval: 1.5 seconds
  • Count: 5 repetitions
  • Behavior: Fires 5 times automatically, then stops

Infinite Mode (Continuous):

  • Interval: 1.0 second
  • Count: -1 (Infinite Loop)
  • Behavior: Fires forever until manually canceled

Step 1: Enter Play Modeโ€‹

Press the Play button in Unity. The beacon's core rotates slowly (idle state).

UI State:

  • Mode button shows: "Toggle Mode (Finite[5])"
  • Beacon rotation: ~20ยฐ/sec (idle speed)

Step 2: Test Finite Loop Modeโ€‹

Current Mode Check: Ensure the button displays "Toggle Mode (Finite[5])" (default mode).

Click "Activate Beacon":

What Happens:

  1. ๐ŸŽฏ Beacon core rotation speeds up to 150ยฐ/sec

  2. ๐Ÿ“ก First pulse fires immediately

    • Cyan shockwave ring spawns and expands outward
    • Sonar ping sound plays
    • Green cubes turn red briefly when ring reaches them
    • Console: [Raiser] Beacon Activated. Mode: Finite (5x)
    • Console: [Receiver] Pulse #1 emitted.
  3. โฑ๏ธ 1.5 seconds later - Second pulse

    • Console: [Receiver] Pulse #2 emitted.
    • Another shockwave expands
    • Targets flash red again
  4. โฑ๏ธ Pulses 3, 4, 5 continue at 1.5s intervals

    • Console counts up to [Receiver] Pulse #5 emitted.
  5. โœ… After 5th pulse - Auto-stop

    • Beacon core rotation slows down to 20ยฐ/sec (idle)
    • No more pulses fire
    • System automatically stoppedโ€”no manual intervention

Timeline:

๐Ÿ–ผ๏ธ T+0.0s | Initial
โšก Pulse #1 (First Trigger)
โ”‚
โ”† (ฮ” 1.5s Loop)
โ–ผ
๐Ÿ–ผ๏ธ T+1.5s | Repeat 1
โšก Pulse #2
โ”‚
โ”† (ฮ” 1.5s Loop)
โ–ผ
๐Ÿ–ผ๏ธ T+3.0s | Repeat 2
โšก Pulse #3
โ”‚
โ”† (ฮ” 1.5s Loop)
โ–ผ
๐Ÿ–ผ๏ธ T+4.5s | Repeat 3
โšก Pulse #4
โ”‚
โ”† (ฮ” 1.5s Loop)
โ–ผ
๐Ÿ–ผ๏ธ T+6.0s | Repeat 4
โšก Pulse #5 (Final)
โ”‚
โ”† (ฮ” 1.5s Gap)
โ–ผ
๐Ÿ›‘ T+7.5s | Lifecycle End
๐Ÿ [ Auto-stopped: No Pulse #6 ]

Result: โœ… Event repeated exactly 5 times, then terminated automatically.


Step 3: Test Infinite Loop Modeโ€‹

Click "Toggle Mode":

  • Button text changes to: "Toggle Mode (Infinite)"
  • Toggle sound plays
  • If beacon was active, it stops first
  • Console: Mode switched

Click "Activate Beacon":

What Happens:

  1. ๐ŸŽฏ Beacon core rotation speeds up to 300ยฐ/sec (faster than finite mode!)

  2. ๐Ÿ“ก Continuous pulses begin

    • First pulse fires immediately
    • Console: [Raiser] Beacon Activated. Mode: Infinite
    • Console: [Receiver] Pulse #1 emitted.
  3. โฑ๏ธ Every 1.0 second - New pulse

    • Faster interval than finite mode (1.0s vs 1.5s)
    • Pulses keep coming: #2, #3, #4, #5...
    • Counter increments indefinitely
  4. โš ๏ธ Never stops automatically

    • Pulse #10, #20, #100...
    • Will continue until manually canceled
    • Beacon spins rapidly throughout

Observation Period: Let it run for ~10 seconds to see it won't auto-stop. Console shows pulse counts increasing without limit.


Step 4: Manual Cancellationโ€‹

While Infinite Mode is Running:

Click "StopSignal":

What Happens:

  1. ๐Ÿ›‘ Pulses cease immediately

    • Current pulse finishes, but no new pulse is scheduled
    • Beacon core rotation slows to idle (20ยฐ/sec)
    • Console: [Raiser] Signal Interrupted manually.
  2. ๐Ÿ”„ System state resets

    • Pulse counter resets to 0
    • Power down sound plays
    • Beacon returns to standby mode

Result: โœ… Infinite loop successfully canceled via .Cancel() API.

๐Ÿ”‘ Key Difference
  • Finite Mode: Stops automatically after N repetitions
  • Infinite Mode: Requires manual .Cancel() to stop

Scene Architectureโ€‹

The Repeating Event Systemโ€‹

Unlike delayed events (wait once, execute once), repeating events use a timer loop:

๐Ÿš€ Initiation: Raise()
โ”‚
โ–ผ โฎโ”€โ”€โ”€ Loop Cycle โ”€โ”€โ”€โ”
โšก [ Execute Action ] โ”‚
โ”‚ โ”‚
โณ [ Wait Interval ] โ”‚ (ฮ” Delta Time)
โ”‚ โ”‚
๐Ÿ”„ [ Repeat Check ] โ”€โ”€โ”˜ (If Remaining > 0)
โ”‚
๐Ÿ›‘ [ Stop Condition ] โž” ๐Ÿ Lifecycle Finalized

Stop Conditions:

  1. Repeat Count Reached: Finite mode auto-stops after N executions
  2. Manual Cancel: .Cancel() terminates infinite loops immediately
  3. Scene Unload: All pending events are cleaned up

Internal Scheduling:

  • GameEventManager maintains a scheduler queue
  • Each repeating event has an internal timer
  • Timer resets after each execution to maintain precise intervals

Event Definitionsโ€‹

Game Event Editor

Event NameTypeRepeat IntervalRepeat Count
onFinitePulseEventGameEvent (void)1.5 seconds5
onInfinitePulseEventGameEvent (void)1.0 second-1 (Infinite)

Same Receiver Method: Both events are bound to RepeatingEventReceiver.OnPulseReceived(). The receiver doesn't know or care which event triggered itโ€”it just responds to each pulse.


Behavior Configuration Comparisonโ€‹

Finite Loop Configurationโ€‹

Click the (void) icon for onFinitePulseEvent to open the Behavior Window:

Finite Behavior

Schedule Configuration:

  • โฑ๏ธ Action Delay: 0 (no initial delay)
  • ๐Ÿ”„ Repeat Interval: 1.5 seconds
    • Time between each pulse execution
  • ๐Ÿ”ข Repeat Count: 5
    • Total number of pulses
    • Stops automatically after 5th execution

Behavior:

๐Ÿ–ผ๏ธ T+0.0s | Initial Raise
๐Ÿš€ Raise() โž” โšก Execute #1
โ”‚
โ”† (ฮ” 1.5s Interval)
โ–ผ
๐Ÿ–ผ๏ธ T+1.5s | Repeat 1/4
โšก Execute #2
โ”‚
โ”† (ฮ” 1.5s Interval)
โ–ผ
๐Ÿ–ผ๏ธ T+3.0s | Repeat 2/4
โšก Execute #3
โ”‚
โ”† (ฮ” 1.5s Interval)
โ–ผ
๐Ÿ–ผ๏ธ T+4.5s | Repeat 3/4
โšก Execute #4
โ”‚
โ”† (ฮ” 1.5s Interval)
โ–ผ
๐Ÿ–ผ๏ธ T+6.0s | Repeat 4/4
โšก Execute #5 โž” [Final Execution]
โ”‚
๐Ÿ T+7.5s | Lifecycle End
๐Ÿ›‘ [ Sequence Terminated: Counter at 0 ]

Infinite Loop Configurationโ€‹

Click the (void) icon for onInfinitePulseEvent to open the Behavior Window:

Infinite Behavior

Schedule Configuration:

  • โฑ๏ธ Action Delay: 0
  • ๐Ÿ”„ Repeat Interval: 1 second (faster than finite mode)
  • ๐Ÿ”ข Repeat Count: Infinite Loop โ™พ๏ธ
    • Special value: -1 means unlimited
    • Will never auto-stop

Behavior:

๐Ÿš€ Initiation: Raise()
โ”‚
โ–ผ โฎโ”โ”โ”โ”โ”โ”โ”โ”โ” Perpetual Loop โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“
โšก Execute #1 (Initial) โ”ƒ
โ”‚ โ”ƒ
โณ (Wait 1.0s) โ”ƒ
โ”‚ โ”ƒ
โšก Execute #2 (Repeat) โ”ƒ
โ”‚ โ”ƒ
โณ (Wait 1.0s) โ”ƒ
โ”‚ โ”ƒ
โšก Execute #N... (Repeat) โ”›
โ”‚
โ”‚ [ External Intervention Required ]
โ””โ”€โ–บ ๐Ÿ› ๏ธ Call: .Cancel()
โ””โ”€โ–บ ๐Ÿ›‘ Loop Terminated โž” ๐Ÿ Cleanup
โš™๏ธ Configuring Infinite Loops

To set infinite repetition, click the Infinite Loop toggle button (โ™พ๏ธ icon) next to Repeat Count. This automatically sets the value to -1.


Sender Setup (RepeatingEventRaiser)โ€‹

Select the RepeatingEventRaiser GameObject:

RepeatingEventRaiser Inspector

Event Channels:

  • Finite Pulse Event: onFinitePulseEvent
    • Configured: Interval = 1.5s, Count = 5
  • Infinite Pulse Event: onInfinitePulseEvent
    • Configured: Interval = 1.0s, Count = -1 (Infinite)

References:

  • Repeating Event Receiver: RepeatingEventReceiver (for coordination)

Visual References:

  • Rotating Core: RotatingCore (Transform) - visual indicator of active state
  • Mode Text: Text (TMP) (TextMeshProUGUI) - displays current mode

Receiver Setup (RepeatingEventReceiver)โ€‹

Select the RepeatingEventReceiver GameObject:

RepeatingEventReceiver Inspector

Configuration:

  • Beacon Origin: SonarBeacon (Transform) - pulse spawn point

Visual Resources:

  • Shockwave Prefab: ShockwaveVFX (Particle System) - expanding ring effect
  • Scanned Material: Prototype_Guide_Red - target highlight material
  • Default Material: Prototype_Guide_Default - target normal material

Audio:

  • Sonar Ping Clip: SonarPingSFX - pulse sound
  • Power Down Clip: PowerDownSFX - stop sound

Code Breakdownโ€‹

RepeatingEventRaiser.cs (Sender)โ€‹

using UnityEngine;
using TinyGiants.GES.Runtime;
using TMPro;

public class RepeatingEventRaiser : MonoBehaviour
{
[Header("Event Channels")]
[Tooltip("Configured in Editor: Interval = 1.5s, Count = 5.")]
[GameEventDropdown] public GameEvent finitePulseEvent;

[Tooltip("Configured in Editor: Interval = 1.0s, Count = -1 (Infinite).")]
[GameEventDropdown] public GameEvent infinitePulseEvent;

[SerializeField] private Transform rotatingCore;
[SerializeField] private TextMeshProUGUI modeText;

private bool _isInfiniteMode = false;
private bool _isActive = false;
private GameEvent _currentEvent;

private void Update()
{
// Visual feedback: Rotation speed indicates state
if (rotatingCore != null)
{
float speed = _isActive
? (_isInfiniteMode ? 300f : 150f) // Active: fast or medium
: 20f; // Idle: slow
rotatingCore.Rotate(Vector3.up, speed * Time.deltaTime);
}
}

/// <summary>
/// Button Action: Starts the repeating event loop.
///
/// CRITICAL: This calls Raise() only ONCE.
/// The Event System's scheduler handles all repetition automatically
/// based on the Repeat Interval and Repeat Count configured in the Editor.
/// </summary>
public void ActivateBeacon()
{
if (_isActive) return;

_isActive = true;

// Select which event to use based on current mode
_currentEvent = _isInfiniteMode ? infinitePulseEvent : finitePulseEvent;

if (_currentEvent != null)
{
// THE MAGIC: Single Raise() call starts entire loop
// System checks event's Repeat Interval & Repeat Count
// Automatically schedules all future executions
_currentEvent.Raise();

Debug.Log($"[Raiser] Beacon Activated. Mode: " +
$"{(_isInfiniteMode ? "Infinite" : "Finite (5x)")}");
}
}

/// <summary>
/// Button Action: Switches between Finite and Infinite modes.
/// Stops any active loop before switching.
/// </summary>
public void ToggleMode()
{
// Must stop before switching modes
if (_isActive) StopSignal();

_isInfiniteMode = !_isInfiniteMode;
UpdateUI();
}

/// <summary>
/// Button Action: Manually cancels the active loop.
///
/// Essential for Infinite loops - they never auto-stop.
/// For Finite loops, this allows early termination.
/// </summary>
public void StopSignal()
{
if (!_isActive || _currentEvent == null) return;

// THE CRITICAL API: Cancel removes event from scheduler
// Stops timer immediately - no more pulses will fire
_currentEvent.Cancel();

_isActive = false;
UpdateUI();

Debug.Log("[Raiser] Signal Interrupted manually.");
}

private void UpdateUI()
{
if (modeText)
modeText.text = _isInfiniteMode
? "Toggle Mode\n<b>(Infinite)</b>"
: "Toggle Mode\n<b>(Finite[5])</b>";
}
}

Key Points:

  • ๐ŸŽฏ Single Raise() - Only called once to start entire loop
  • ๐Ÿ”€ Mode Selection - Switches between two pre-configured events
  • ๐Ÿ›‘ Cancel API - Stops infinite loops or terminates finite loops early
  • ๐ŸŽจ Visual Feedback - Rotation speed indicates active state and mode

RepeatingEventReceiver.cs (Listener)โ€‹

using UnityEngine;
using System.Collections;

public class RepeatingEventReceiver : MonoBehaviour
{
[Header("Configuration")]
public Transform beaconOrigin;

[Header("Visual Resources")]
public ParticleSystem shockwavePrefab;
public Material scannedMaterial;
public Material defaultMaterial;

[Header("Audio")]
public AudioClip sonarPingClip;

private AudioSource _audioSource;
private int _pulseCount = 0;

private void Start()
{
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null) _audioSource = gameObject.AddComponent<AudioSource>();
}

/// <summary>
/// [Event Callback - Repeating Execution]
///
/// Bound to both 'onFinitePulseEvent' and 'onInfinitePulseEvent'.
///
/// This method executes:
/// - Immediately when Raise() is called (first pulse)
/// - Then repeatedly at each Repeat Interval
/// - Until Repeat Count reached (finite) or Cancel() called (infinite)
///
/// The receiver is STATELESS - it doesn't track pulse numbers or loop status.
/// It simply reacts to each trigger.
/// </summary>
public void OnPulseReceived()
{
_pulseCount++;
Debug.Log($"[Receiver] Pulse #{_pulseCount} emitted.");

Vector3 spawnPos = beaconOrigin != null
? beaconOrigin.position
: transform.position;

// Spawn visual shockwave
if (shockwavePrefab != null)
{
var vfx = Instantiate(shockwavePrefab, spawnPos, Quaternion.identity);
vfx.Play();
Destroy(vfx.gameObject, 2.0f);
}

// Play sonar ping with slight pitch variation
if (sonarPingClip)
{
_audioSource.pitch = Random.Range(0.95f, 1.05f);
_audioSource.PlayOneShot(sonarPingClip);
}

// Start physics-based target scanning
StartCoroutine(ScanRoutine(spawnPos));
}

public void OnPowerDown()
{
_pulseCount = 0; // Reset counter when system powers down
}

/// <summary>
/// Expands an invisible sphere from the beacon origin.
/// Targets within the expanding wavefront get highlighted.
/// </summary>
private IEnumerator ScanRoutine(Vector3 center)
{
float maxRadius = 40f; // Match cyan ring size
float speed = 10f; // Expansion speed
float currentRadius = 0f;

while (currentRadius < maxRadius)
{
currentRadius += speed * Time.deltaTime;

// Physics sphere cast to find targets
Collider[] hits = Physics.OverlapSphere(center, currentRadius);

foreach (var hit in hits)
{
if (hit.name.Contains("ScanTarget"))
{
var rend = hit.GetComponent<Renderer>();
if (rend && rend.sharedMaterial != scannedMaterial)
{
float dist = Vector3.Distance(center, hit.transform.position);

// Only highlight if at wavefront edge (within 1 unit)
if (dist <= currentRadius && dist > currentRadius - 1.0f)
{
StartCoroutine(HighlightTarget(rend));
}
}
}
}

yield return null;
}
}

private IEnumerator HighlightTarget(Renderer target)
{
// Flash red temporarily
target.material = scannedMaterial;

var tmp = target.GetComponentInChildren<TMPro.TextMeshPro>();
if(tmp) tmp.text = "DETECTED";

yield return new WaitForSeconds(0.4f);

// Reset to default
target.material = defaultMaterial;
if(tmp) tmp.text = "?";
}
}

Key Points:

  • ๐ŸŽฏ Stateless Receiver - Doesn't track loop count or timing
  • ๐Ÿ“ก Physics Scanning - Expanding sphere cast detects targets
  • ๐ŸŽจ Wavefront Detection - Only highlights targets at shockwave edge
  • ๐Ÿ”ข Pulse Counter - Tracks total pulses received (cosmetic)

Key Takeawaysโ€‹

ConceptImplementation
๐Ÿ”„ Repeat IntervalTime between each execution (configured in Editor)
๐Ÿ”ข Repeat CountNumber of repetitions (N for finite, -1 for infinite)
๐ŸŽฏ Single Raise()One call starts entire loopโ€”no manual triggers needed
โœ… Auto-StopFinite loops terminate automatically after N executions
๐Ÿ›‘ Manual Cancel.Cancel() required to stop infinite loops
๐ŸŽจ Stateless ReceiversCallbacks don't need to track loop state
๐ŸŽ“ Design Insight

Repeating events are perfect for:

  • Periodic abilities - Poison damage, regeneration, area denial
  • Environmental effects - Lava bubbles, steam vents, lighthouse beacons
  • Spawning systems - Enemy waves, item drops, particle bursts
  • Radar/detection - Sonar pulses, security scans, proximity alerts
  • Gameplay loops - Turn timers, checkpoint autosaves, periodic events

Use Finite loops when you know exactly how many times something should repeat (e.g., "fire 3 shots"). Use Infinite loops for ongoing effects that should continue until a specific condition is met (e.g., "pulse until player leaves area").

๐Ÿ’ป Programmatic API

You can also configure loops purely via code, overriding Inspector settings:

// Override Inspector settings temporarily
myEvent.RaiseRepeating(interval: 0.5f, repeatCount: 10);

// Or use default Inspector settings
myEvent.Raise();

This allows dynamic adjustment based on runtime conditions (e.g., difficulty modifiers, power-ups).