Play intro audio only once

This commit is contained in:
Michal Pikulski
2025-11-10 11:09:06 +01:00
parent 252cb99884
commit 75cd70a18a
5 changed files with 141 additions and 12 deletions

View File

@@ -453761,6 +453761,10 @@ PrefabInstance:
propertyPath: m_PlayOnAwake propertyPath: m_PlayOnAwake
value: 0 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 8545106365577783398, guid: ead4e790fa3a1924ebd1586c93cd5479, type: 3}
propertyPath: isOneTime
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: [] m_RemovedComponents: []
m_RemovedGameObjects: [] m_RemovedGameObjects: []
m_AddedGameObjects: [] m_AddedGameObjects: []

View File

@@ -469,6 +469,34 @@ namespace Core.Lifecycle
LogDebug($"Restored scene data to {restoredCount} components"); LogDebug($"Restored scene data to {restoredCount} components");
} }
/// <summary>
/// Broadcasts scene restore completed event to all registered components.
/// Called AFTER all OnSceneRestoreRequested calls complete.
/// </summary>
public void BroadcastSceneRestoreCompleted()
{
LogDebug("Broadcasting SceneRestoreCompleted");
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(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");
}
/// <summary> /// <summary>
/// Broadcasts global restore request to all registered components that opt-in. /// Broadcasts global restore request to all registered components that opt-in.
/// Distributes serialized data to matching components by SaveId. /// Distributes serialized data to matching components by SaveId.

View File

@@ -89,6 +89,7 @@ namespace Core.Lifecycle
public void InvokeSceneReady() => OnSceneReady(); public void InvokeSceneReady() => OnSceneReady();
public string InvokeSceneSaveRequested() => OnSceneSaveRequested(); public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data); public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
public void InvokeSceneRestoreCompleted() => OnSceneRestoreCompleted();
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested(); public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data); public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
public void InvokeManagedDestroy() => OnManagedDestroy(); public void InvokeManagedDestroy() => OnManagedDestroy();
@@ -202,6 +203,10 @@ namespace Core.Lifecycle
/// Called during scene transitions to restore scene-specific state. /// Called during scene transitions to restore scene-specific state.
/// Receives previously serialized data (from OnSceneSaveRequested). /// 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: /// TIMING:
/// - Called AFTER scene load, during OnSceneReady phase /// - Called AFTER scene load, during OnSceneReady phase
/// - Frequency: Every scene transition /// - Frequency: Every scene transition
@@ -212,6 +217,29 @@ namespace Core.Lifecycle
// Default: no-op // Default: no-op
} }
/// <summary>
/// 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).
/// </summary>
protected virtual void OnSceneRestoreCompleted()
{
// Default: no-op
}
/// <summary> /// <summary>
/// Called once on game boot to restore global persistent state. /// Called once on game boot to restore global persistent state.
/// Receives data that was saved via OnGlobalSaveRequested. /// Receives data that was saved via OnGlobalSaveRequested.

View File

@@ -504,9 +504,19 @@ namespace Core.SaveLoad
/// </summary> /// </summary>
public void RestoreSceneData() public void RestoreSceneData()
{ {
if (Lifecycle.LifecycleManager.Instance == null)
{
Logging.Warning("[SaveLoadManager] LifecycleManager not available for scene restore");
return;
}
if (currentSaveData == null || currentSaveData.participantStates == null) 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; return;
} }
@@ -520,11 +530,12 @@ namespace Core.SaveLoad
} }
// Restore scene data via LifecycleManager // 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");
} }
/// <summary> /// <summary>

View File

@@ -1,22 +1,80 @@
using UnityEngine; using UnityEngine;
using UnityEngine.Audio; using UnityEngine.Audio;
using System; 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 AppleAudioSource narratorAudioSource;
public AudioResource firstNarration; 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 private bool _hasPlayed;
void Start()
#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<LevelAudioObjectSaveData>(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.audioSource.resource = firstNarration;
narratorAudioSource.Play(0); narratorAudioSource.Play(0);
_hasPlayed = true;
} }
} }