09 Persistent Event: Surviving Scene Loads
๐ Overviewโ
In Unity, when you load a new scene, all GameObjects (and their event listeners) from the previous scene are destroyed. Persistent Events solve this problem by storing listener bindings in a global manager that survives scene transitionsโessential for global systems like music controllers, inventory managers, or achievement trackers.
- The scene transition cleanup problem in Unity
- How to enable event persistence with a single checkbox
- The difference between persistent and non-persistent event behavior
- Architectural patterns for cross-scene event systems
๐ฌ Demo Sceneโ
Assets/TinyGiants/GameEventSystem/Demo/09_PersistentEvent/09_PersistentEvent_1.unity
Scene Compositionโ
Visual Elements:
-
๐ด Turret_A (Left) - Red turret with grey base
- Controlled by persistent event
OnTurretA - Has rotating head mechanism
- Will continue working after scene reload
- Controlled by persistent event
-
๐ต Turret_B (Right) - Blue turret with grey base
- Controlled by non-persistent event
OnTurretB - Identical functionality to Turret A
- Will stop working after scene reload
- Controlled by non-persistent event
-
๐ฏ TargetDummy - Center capsule target
- Both turrets aim and fire at this target
- Has Rigidbody for knockback physics
-
๐ HoloDisplay - Information panel
- Displays explanatory text about the experiment
- Shows persistent state information
UI Layer (Canvas):
- ๐ฎ Three Buttons - Bottom of the screen
- "Fire A" (White) โ Triggers
PersistentEventRaiser.FireTurretA() - "Fire B" (White) โ Triggers
PersistentEventRaiser.FireTurretB() - "Load Scene 2" (Green) โ Reloads the scene to test persistence
- "Fire A" (White) โ Triggers
Game Logic Layer (Demo Scripts):
-
๐ค PersistentEventRaiser - Standard scene-based raiser
- Holds references to both events
- Destroyed and recreated on scene reload
-
๐ฅ PersistentEventReceiver - DontDestroyOnLoad singleton
- Survives scene transitions
- Holds combat logic for both turrets
- Uses dependency injection pattern for scene references
-
๐ง Scene Setup - Dependency injection helper
- Runs on scene load
- Re-injects new turret references into persistent receiver
- Enables persistent receiver to control new scene objects
๐ฎ How to Interactโ
The Persistence Experimentโ
This demo proves that persistent events maintain their bindings across scene loads while non-persistent events are cleared.
Step 1: Enter Play Modeโ
Press the Play button in Unity.
Initial State:
- Two turrets (red and blue) idle in the scene
- HoloDisplay shows explanatory text
- Console is clear
Step 2: Initial Functionality Testโ
Click "Fire A":
- ๐ฏ Red turret (left) rotates toward target
- ๐ Projectile fires and travels
- ๐ฅ On impact:
- Orange floating text "CRIT! -500"
- Large explosion VFX
- Camera shake
- Target knocked back
- ๐ Console:
[Raiser] Broadcasting Command: Fire Turret A - ๐ Console:
[Receiver] Received Command A. Engaging...
Click "Fire B":
- ๐ฏ Blue turret (right) rotates toward target
- ๐ Projectile fires
- ๐ฅ On impact:
- White floating text "-200"
- Normal explosion VFX
- No camera shake (weaker attack)
- Target knocked back
- ๐ Console:
[Raiser] Broadcasting Command: Fire Turret B - ๐ Console:
[Receiver] Received Command B. Engaging...
Result: โ Both turrets work perfectly in the initial scene.
Step 3: The Scene Reload (The Purge)โ
Click "Load Scene 2":
What Happens Behind the Scenes:
-
๐ Unity's
SceneManager.LoadScene()is called -
๐ Scene Destruction Phase:
- All scene GameObjects are destroyed:
- โ Turret_A destroyed
- โ Turret_B destroyed
- โ TargetDummy destroyed
- โ PersistentEventRaiser destroyed
- ๐๏ธ GameEventManager cleans up non-persistent event listeners
OnTurretBlisteners clearedOnTurretAlisteners preserved (persistent flag)
- All scene GameObjects are destroyed:
-
๐๏ธ Scene Recreation Phase:
- New Turret_A spawned
- New Turret_B spawned
- New TargetDummy spawned
- New PersistentEventRaiser spawned
-
โจ Persistent Objects:
- โ
PersistentEventReceiversurvives (DontDestroyOnLoad) - โ
Its method bindings to
OnTurretAstill active
- โ
-
๐ง Dependency Injection:
PersistentEventSceneSetup.Start()runs- Calls
PersistentEventReceiver.UpdateSceneReferences() - Injects new scene turret references into persistent receiver
Visual Changes:
- Scene briefly goes black during reload
- Turrets respawn in same positions
- UI buttons remain functional
Step 4: Post-Reload Survival Testโ
Click "Fire A" (After Reload):
What Happens:
- ๐ฏ Red turret rotates and fires (works perfectly!)
- ๐ฅ Full combat sequence plays
- ๐ Console:
[Receiver] Received Command A. Engaging...
Why It Works:
Button โ fireAEvent.Raise()
โ GameEventManager finds persistent binding
โ PersistentEventReceiver.OnFireCommandA() executes
โ Uses newly injected turret reference
โ Turret fires
Result: โ Persistent event survived scene reload!
Click "Fire B" (After Reload):
What Happens:
- ๐ NOTHING
- ๐ Console:
[Raiser] Broadcasting Command: Fire Turret B - โ No receiver log
- Blue turret does not move or fire
Why It Failed:
๐ Input: Button Click
โ
๐ Event: fireBEvent.Raise()
โ
๐ Registry: [ GameEventManager Lookup ]
โ
โโโ Result: NONE Found
โ โโ ๐๏ธ Reason: Bindings cleared during Scene Reload
โ
๐ Outcome: Signal Dissipated
โ โโ ๐ป Result: "Lost in the void" (No receivers called)
โ
๐ Status: 0 Actions Executed | โ
System Safe (No NullRef)
Result: โ Non-persistent event binding was destroyed!
OnTurretB listener was cleared when the scene unloaded. The event asset still exists, but its connection to PersistentEventReceiver.OnFireCommandB() is permanently broken (unless you manually re-subscribe via code).
๐๏ธ Scene Architectureโ
The Scene Transition Problemโ
In standard Unity event systems:
๐ผ๏ธ Scene A: Loaded
โโ ๐ Listeners: Subscribed (Local Context)
โ
๐ [ Loading Scene B... ]
โ
๐งน Cleanup: Memory Purged
โโ โ Result: ALL listeners cleared from the registry
โ
๐ผ๏ธ Scene B: Active
โโ ๐ Status: Event is "Empty" (No receivers)
This breaks global systems that need to persist across scenes.
The Persistent Event Solutionโ
๐ผ๏ธ Scene A: Loaded
โโ ๐ก๏ธ Listeners: Subscribed (Global Context)
โ
๐ [ Loading Scene B... ]
โ
๐ Preservation: Handover Successful
โโ โ
Result: Bindings stored in the Global Persistent Registry
โ
๐ผ๏ธ Scene B: Active
โโ ๐ฅ Status: Event is "Hot" (Listeners remain ready to fire)
Persistent events behave like DontDestroyOnLoad for event logic.
Architectural Pattern: Dependency Injectionโ
This demo uses a sophisticated pattern to handle scene references:
The Challenge:
PersistentEventReceiversurvives (DontDestroyOnLoad)- But turrets are destroyed and recreated each scene load
- Receiver needs references to new turret instances
The Solution:
- Persistent Receiver holds combat logic
- Scene Setup Script runs on each scene load
- Setup injects new scene references into persistent receiver
- Receiver can now control new turrets
๐ก๏ธ Persistent Layer (The "Survivor")
โ โโ ๐ PersistentEventReceiver [Survives Scene Load]
โ โฒ
โ โ ๐ Dependency Injection (References Re-bound)
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
๐ผ๏ธ Scene Layer (The "Context") โ
โ โโ โ๏ธ PersistentEventSceneSetup [Recreated] โ
โ โ โ
โ โโโ ๐ Finds & Passes References โ โโโโโ
โ โ
โ โโโ ๐ค New Turret_A [Scene Instance]
โ โโโ ๐ค New Turret_B [Scene Instance]
Event Definitionsโ

| Event Name | Type | Persistent Flag |
|---|---|---|
OnTurretA | GameEvent (void) | โ Checked |
OnTurretB | GameEvent (void) | โ Unchecked |
Identical Events, Different Fate: Both are void events with the same configurationโexcept for one checkbox that determines their survival.
Behavior Configurationโ
Persistent Event (OnTurretA)โ
Click the (void) icon for OnTurretA to open the Behavior Window:

Critical Setting:
- ๐พ Persistent Event: โ CHECKED
Warning Message:
"Event will behave like DontDestroyOnLoad."
What This Means:
- Listener bindings stored in global persistent manager
- NOT cleared during scene transitions
- Survives until explicitly removed or game exit
- Essential for cross-scene systems
Non-Persistent Event (OnTurretB)โ
Same configuration except:
- ๐พ Persistent Event: โ UNCHECKED
Result:
- Standard Unity lifecycle
- Listeners cleared on scene unload
- Must re-subscribe if needed in new scene
Sender Setup (PersistentEventRaiser)โ
Select the PersistentEventRaiser GameObject:

Game Events:
Fire A Event:OnTurretA(Persistent)- Tooltip: "Checked 'Persistent Event' in Editor"
Fire B Event:OnTurretB(Non-Persistent)- Tooltip: "Unchecked 'Persistent Event' in Editor"
Lifecycle:
- โ Destroyed on scene reload
- โ Recreated with new scene
- Holds new event references (assets are persistent ScriptableObjects)
Receiver Setup (PersistentEventReceiver)โ
Select the PersistentEventReceiver GameObject:

Combat Resources:
Projectile Prefab: Projectile (Turret Projectile)Fire VFX: MuzzleFlashVFX (Particle System)
Feedback:
Hit Normal VFX: HitVFX_Normal (Particle System)Hit Crit VFX: HitVFX_Crit (Particle System)Floating Text Prefab: DamageFloatingText (Text Mesh Pro)Hit Clip: ExplosionSFX (Audio Clip)
Dynamic References (Hidden): These are injected at runtime by Scene Setup:
turretA,headA(Turret A references)turretB,headB(Turret B references)targetDummy,targetRigidbody(Target references)
Scene Setup Configurationโ
Select the Scene Setup GameObject:

Current Scene Objects:
Turret A: Turret_A (GameObject)Head A: Head (Transform) - rotation pivotTurret B: Turret_B (GameObject)Head B: Head (Transform)Target Dummy: TargetDummy (Transform)Target Rigidbody: TargetDummy (Rigidbody)
Purpose:
On Start(), this script finds the persistent receiver and injects these references, enabling it to control new scene objects.
๐ป Code Breakdownโ
๐ค PersistentEventRaiser.cs (Sender)โ
using UnityEngine;
using TinyGiants.GameEventSystem.Runtime;
public class PersistentEventRaiser : MonoBehaviour
{
[Header("Game Events")]
[Tooltip("Configuration: Checked 'Persistent Event' in Editor.")]
[GameEventDropdown] public GameEvent fireAEvent;
[Tooltip("Configuration: Unchecked 'Persistent Event' in Editor.")]
[GameEventDropdown] public GameEvent fireBEvent;
/// <summary>
/// UI Button: Commands Turret A to fire.
///
/// Since 'fireAEvent' is Persistent, this binding survives scene loads.
/// Even after reloading, the persistent receiver will still respond.
/// </summary>
public void FireTurretA()
{
if (fireAEvent == null) return;
fireAEvent.Raise();
Debug.Log("<color=cyan>[Raiser] Broadcasting Command: Fire Turret A</color>");
}
/// <summary>
/// UI Button: Commands Turret B to fire.
///
/// Since 'fireBEvent' is NOT Persistent, this binding BREAKS after scene load.
/// The event is raised, but no one is listening anymore.
/// </summary>
public void FireTurretB()
{
if (fireBEvent == null) return;
fireBEvent.Raise();
Debug.Log("<color=orange>[Raiser] Broadcasting Command: Fire Turret B</color>");
}
}
Key Points:
- ๐ฏ Standard Component - Not persistent, recreated each scene
- ๐ก Event References - ScriptableObject assets (persistent)
- ๐ No Lifecycle Awareness - Doesn't know if listeners survived
๐ฅ PersistentEventReceiver.cs (Listener - Singleton)โ
using UnityEngine;
using System.Collections;
public class PersistentEventReceiver : MonoBehaviour
{
[Header("Combat Resources")]
[SerializeField] private TurretProjectile projectilePrefab;
[SerializeField] private ParticleSystem fireVFX;
// ... other resources ...
// Runtime-injected scene references
[HideInInspector] public GameObject turretA;
[HideInInspector] public Transform headA;
[HideInInspector] public GameObject turretB;
[HideInInspector] public Transform headB;
[HideInInspector] public Transform targetDummy;
[HideInInspector] public Rigidbody targetRigidbody;
private bool _isFiringA;
private bool _isFiringB;
// Singleton pattern for persistence
private static PersistentEventReceiver _instance;
public static PersistentEventReceiver Instance => _instance;
private void Awake()
{
// CRITICAL: DontDestroyOnLoad makes this survive scene transitions
if (_instance == null)
{
_instance = this;
DontDestroyOnLoad(gameObject);
Debug.Log("[PersistentReceiver] Initialized with DontDestroyOnLoad.");
}
else if (_instance != this)
{
// Prevent duplicates if scene reloaded
Destroy(gameObject);
}
}
private void Update()
{
// Control turrets using injected references
HandleTurretRotation(turretA, headA, ref _isFiringA);
HandleTurretRotation(turretB, headB, ref _isFiringB);
}
/// <summary>
/// [Event Callback - Persistent Binding]
/// Bound to 'OnTurretA' with Persistent Event flag checked.
///
/// This method binding SURVIVES scene reload.
/// After reload, this will still be called when fireAEvent.Raise() executes.
/// </summary>
public void OnFireCommandA()
{
Debug.Log("<color=cyan>[Receiver] Received Command A. Engaging...</color>");
_isFiringA = true;
}
/// <summary>
/// [Event Callback - Non-Persistent Binding]
/// Bound to 'OnTurretB' with Persistent Event flag UNCHECKED.
///
/// This method binding is CLEARED on scene reload.
/// After reload, this will NEVER be called again (binding is lost).
/// </summary>
public void OnFireCommandB()
{
Debug.Log("<color=orange>[Receiver] Received Command B. Engaging...</color>");
_isFiringB = true;
}
/// <summary>
/// Called by PersistentEventSceneSetup on each scene load.
/// Injects new scene object references into persistent receiver.
/// </summary>
public void UpdateSceneReferences(
GameObject tA, Transform hA,
GameObject tB, Transform hB,
Transform target, Rigidbody rb)
{
this.turretA = tA;
this.headA = hA;
this.turretB = tB;
this.headB = hB;
this.targetDummy = target;
this.targetRigidbody = rb;
Debug.Log("[PersistentReceiver] Scene references updated.");
}
private void HandleTurretRotation(GameObject turret, Transform head, ref bool isFiring)
{
if (head == null || targetDummy == null) return;
// Idle sway or active targeting
Quaternion targetRot;
float speed = isFiring ? 10f : 2f;
if (isFiring)
{
// Aim at target
Vector3 dir = targetDummy.position - head.position;
dir.y = 0;
if (dir != Vector3.zero)
targetRot = Quaternion.LookRotation(dir);
else
targetRot = head.rotation;
}
else
{
// Idle patrol sweep
float angle = Mathf.Sin(Time.time * 0.5f) * 30f;
targetRot = Quaternion.Euler(0, 180 + angle, 0);
}
head.rotation = Quaternion.Slerp(head.rotation, targetRot, speed * Time.deltaTime);
// Fire when aimed
if (isFiring && Quaternion.Angle(head.rotation, targetRot) < 5f)
{
PerformFireSequence(turret);
isFiring = false;
}
}
private void PerformFireSequence(GameObject turret)
{
// Spawn muzzle flash, launch projectile, etc.
// ... (combat logic) ...
}
}
Key Points:
- ๐ฏ DontDestroyOnLoad - Survives scene transitions
- ๐ Singleton Pattern - Only one instance exists globally
- ๐ Dependency Injection - Scene references injected at runtime
- ๐ญ Dual Binding - Persistent (A) and non-persistent (B) methods
๐ง PersistentEventSceneSetup.cs (Dependency Injector)โ
using UnityEngine;
public class PersistentEventSceneSetup : MonoBehaviour
{
[Header("Current Scene Objects")]
public GameObject turretA;
public Transform headA;
public GameObject turretB;
public Transform headB;
public Transform targetDummy;
public Rigidbody targetRigidbody;
private void Start()
{
// Find the persistent receiver (lives in DontDestroyOnLoad scene)
var receiver = PersistentEventReceiver.Instance;
if (receiver != null)
{
// Inject this scene's object references
receiver.UpdateSceneReferences(
turretA, headA,
turretB, headB,
targetDummy, targetRigidbody
);
Debug.Log("[SceneSetup] Successfully injected scene references " +
"into persistent receiver.");
}
else
{
Debug.LogWarning("[SceneSetup] PersistentEventReceiver not found! " +
"Is the demo started correctly?");
}
}
}
Key Points:
- ๐ง Runs on Scene Load -
Start()executes when scene initializes - ๐ Finds Singleton - Accesses persistent receiver via static instance
- ๐ Injects References - Passes new scene objects to persistent logic
- ๐๏ธ Enables Cross-Scene Control - Bridges persistent logic with transient objects
๐ Key Takeawaysโ
| Concept | Implementation |
|---|---|
| ๐พ Persistent Event | Checkbox in Behavior Window preserves bindings across scenes |
| ๐๏ธ Cleanup Behavior | Non-persistent events cleared on scene unload |
| ๐ DontDestroyOnLoad | Receiver must survive for persistent events to work |
| ๐ Dependency Injection | Pattern for connecting persistent logic with scene objects |
| ๐ฏ Single Checkbox | One setting determines cross-scene survival |
Persistent events are perfect for:
- Music systems - Background music controller that spans multiple levels
- Inventory managers - Player inventory persists across scene transitions
- Achievement trackers - Global achievement listeners that monitor all scenes
- Analytics systems - Event logging that never gets interrupted
- UI systems - Persistent HUD controllers for health, score, etc.
Architecture Pattern:
[Persistent Layer - DontDestroyOnLoad]
- Global managers
- Event receivers
- Cross-scene logic
[Scene Layer - Recreated]
- Level-specific objects
- Scene setup scripts (dependency injection)
- UI buttons and raisers
This separation enables clean cross-scene architecture without manual re-subscription.
- Receiver Must Be Persistent: Checking "Persistent Event" only preserves the binding. The receiver GameObject must use
DontDestroyOnLoadto survive. - Scene References Break: Even though bindings persist, references to destroyed scene objects become null. Use dependency injection to update them.
- Memory Management: Persistent events stay active until game exit. Be mindful of accumulating bindings in long-running games.
- Initial Scene Requirement: The persistent receiver must be present in the first loaded scene. If Scene B loads first without the receiver, persistent events won't work.
๐ฏ What's Next?โ
You've mastered persistent events for cross-scene systems. Now let's explore trigger events for collision-based interactions.
Next Chapter: Learn about collision triggers in 10 Trigger Event
๐ Related Documentationโ
- Game Event Behavior - Complete guide to persistence configuration
- Best Practices - Patterns for cross-scene event architecture