From 75cd70a18ad785702994f80ce96e533defb38517 Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Mon, 10 Nov 2025 11:09:06 +0100 Subject: [PATCH] Play intro audio only once --- Assets/Scenes/Levels/Quarry.unity | 4 ++ .../Core/Lifecycle/LifecycleManager.cs | 28 ++++++++ .../Core/Lifecycle/ManagedBehaviour.cs | 28 ++++++++ .../Scripts/Core/SaveLoad/SaveLoadManager.cs | 23 ++++-- Assets/Scripts/Sound/LevelAudioObject.cs | 70 +++++++++++++++++-- 5 files changed, 141 insertions(+), 12 deletions(-) diff --git a/Assets/Scenes/Levels/Quarry.unity b/Assets/Scenes/Levels/Quarry.unity index 7f1908bc..a9b002da 100644 --- a/Assets/Scenes/Levels/Quarry.unity +++ b/Assets/Scenes/Levels/Quarry.unity @@ -453761,6 +453761,10 @@ PrefabInstance: propertyPath: m_PlayOnAwake value: 0 objectReference: {fileID: 0} + - target: {fileID: 8545106365577783398, guid: ead4e790fa3a1924ebd1586c93cd5479, type: 3} + propertyPath: isOneTime + value: 1 + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] diff --git a/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs b/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs index 82bbd221..2ac71c73 100644 --- a/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs +++ b/Assets/Scripts/Core/Lifecycle/LifecycleManager.cs @@ -469,6 +469,34 @@ namespace Core.Lifecycle LogDebug($"Restored scene data to {restoredCount} components"); } + /// + /// Broadcasts scene restore completed event to all registered components. + /// Called AFTER all OnSceneRestoreRequested calls complete. + /// + public void BroadcastSceneRestoreCompleted() + { + LogDebug("Broadcasting SceneRestoreCompleted"); + + // Create a copy to avoid collection modification during iteration + var componentsCopy = new List(managedAwakeList); + + foreach (var component in componentsCopy) + { + if (component == null) continue; + + try + { + component.InvokeSceneRestoreCompleted(); + } + catch (Exception ex) + { + Debug.LogError($"[LifecycleManager] Exception during scene restore completed for {component.SaveId}: {ex}"); + } + } + + LogDebug("SceneRestoreCompleted broadcast complete"); + } + /// /// Broadcasts global restore request to all registered components that opt-in. /// Distributes serialized data to matching components by SaveId. diff --git a/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs b/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs index 085797be..5281424c 100644 --- a/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs +++ b/Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs @@ -89,6 +89,7 @@ namespace Core.Lifecycle public void InvokeSceneReady() => OnSceneReady(); public string InvokeSceneSaveRequested() => OnSceneSaveRequested(); public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data); + public void InvokeSceneRestoreCompleted() => OnSceneRestoreCompleted(); public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested(); public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data); public void InvokeManagedDestroy() => OnManagedDestroy(); @@ -202,6 +203,10 @@ namespace Core.Lifecycle /// Called during scene transitions to restore scene-specific state. /// Receives previously serialized data (from OnSceneSaveRequested). /// + /// IMPORTANT: This method MUST be synchronous. Do not use coroutines or async/await. + /// OnSceneRestoreCompleted is called immediately after all restore calls complete, + /// so any async operations would still be running when it fires. + /// /// TIMING: /// - Called AFTER scene load, during OnSceneReady phase /// - Frequency: Every scene transition @@ -212,6 +217,29 @@ namespace Core.Lifecycle // Default: no-op } + /// + /// Called after all scene restore operations complete. + /// Does NOT receive data - use OnSceneRestoreRequested for that. + /// + /// GUARANTEE: + /// - ALWAYS called after scene load, whether there's save data or not + /// - All OnSceneRestoreRequested() calls have RETURNED (but async operations may still be running) + /// - Safe for synchronous restore operations (JSON deserialization, setting fields, etc.) + /// + /// TIMING: + /// - Called AFTER all OnSceneRestoreRequested calls complete (or immediately if no save data exists) + /// - Frequency: Every scene transition + /// - Use for: Post-restore initialization, first-time initialization, triggering events after state is restored + /// + /// COMMON PATTERN: + /// Use this to perform actions that depend on whether data was restored or not. + /// Example: Play one-time audio only if it hasn't been played before (_hasPlayed == false). + /// + protected virtual void OnSceneRestoreCompleted() + { + // Default: no-op + } + /// /// Called once on game boot to restore global persistent state. /// Receives data that was saved via OnGlobalSaveRequested. diff --git a/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs b/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs index ecbeb0ca..86a8ed76 100644 --- a/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs +++ b/Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs @@ -504,9 +504,19 @@ namespace Core.SaveLoad /// public void RestoreSceneData() { + if (Lifecycle.LifecycleManager.Instance == null) + { + Logging.Warning("[SaveLoadManager] LifecycleManager not available for scene restore"); + return; + } + if (currentSaveData == null || currentSaveData.participantStates == null) { - Logging.Debug("[SaveLoadManager] No scene data to restore"); + Logging.Debug("[SaveLoadManager] No scene data to restore (first visit or no save data)"); + + // Still broadcast restore completed so components can initialize properly + Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreCompleted(); + Logging.Debug($"[SaveLoadManager] Scene restore completed (no data)"); return; } @@ -520,11 +530,12 @@ namespace Core.SaveLoad } // Restore scene data via LifecycleManager - if (Lifecycle.LifecycleManager.Instance != null) - { - Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict); - Logging.Debug($"[SaveLoadManager] Broadcast scene restore to LifecycleManager"); - } + Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict); + Logging.Debug($"[SaveLoadManager] Broadcast scene restore to LifecycleManager"); + + // Broadcast scene restore completed - ALWAYS called, whether there's data or not + Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreCompleted(); + Logging.Debug($"[SaveLoadManager] Scene restore completed"); } /// diff --git a/Assets/Scripts/Sound/LevelAudioObject.cs b/Assets/Scripts/Sound/LevelAudioObject.cs index ed74b94d..23fab489 100644 --- a/Assets/Scripts/Sound/LevelAudioObject.cs +++ b/Assets/Scripts/Sound/LevelAudioObject.cs @@ -1,22 +1,80 @@ using UnityEngine; using UnityEngine.Audio; using System; +using Core.Lifecycle; -public class LevelAudioObject : MonoBehaviour +[Serializable] +public class LevelAudioObjectSaveData { + public bool hasPlayed; +} + +public class LevelAudioObject : ManagedBehaviour +{ + [Header("Audio Settings")] public AppleAudioSource narratorAudioSource; public AudioResource firstNarration; + + [Header("Playback Settings")] + [Tooltip("If true, the audio will only play once and never again after being played")] + public bool isOneTime; - // Start is called once before the first execution of Update after the MonoBehaviour is created - void Start() + private bool _hasPlayed; + + #region ManagedBehaviour Overrides + + public override bool AutoRegisterForSave => isOneTime; // Only save if one-time audio + + protected override string OnSceneSaveRequested() { - PlayNarrationAudio(); + if (!isOneTime) + return null; // No need to save if not one-time + + LevelAudioObjectSaveData saveData = new LevelAudioObjectSaveData + { + hasPlayed = _hasPlayed + }; + + return JsonUtility.ToJson(saveData); } - void PlayNarrationAudio() + protected override void OnSceneRestoreRequested(string serializedData) { + if (!isOneTime || string.IsNullOrEmpty(serializedData)) + return; + + try + { + LevelAudioObjectSaveData saveData = JsonUtility.FromJson(serializedData); + _hasPlayed = saveData.hasPlayed; + } + catch (Exception e) + { + Debug.LogWarning($"[LevelAudioObject] Failed to restore audio state: {e.Message}"); + } + } + + protected override void OnSceneRestoreCompleted() + { + if (isOneTime && !_hasPlayed) + { + PlayNarrationAudio(); + } + } + + #endregion + + private void PlayNarrationAudio() + { + if (narratorAudioSource == null || firstNarration == null) + { + Debug.LogWarning($"[LevelAudioObject] Missing audio source or narration resource on {gameObject.name}"); + return; + } + narratorAudioSource.audioSource.resource = firstNarration; narratorAudioSource.Play(0); + + _hasPlayed = true; } - }