SaveLoad using managed lifecycle
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -62,6 +62,11 @@ namespace Core.Lifecycle
|
||||
private bool isBootComplete = false;
|
||||
private string currentSceneReady = "";
|
||||
|
||||
// Scene loading state tracking
|
||||
private bool isLoadingScene = false;
|
||||
private string sceneBeingLoaded = "";
|
||||
private List<ManagedBehaviour> pendingSceneComponents = new List<ManagedBehaviour>();
|
||||
|
||||
[SerializeField] private bool enableDebugLogging = true;
|
||||
|
||||
#endregion
|
||||
@@ -115,26 +120,36 @@ namespace Core.Lifecycle
|
||||
// Track which scene this component belongs to
|
||||
componentScenes[component] = sceneName;
|
||||
|
||||
// Register for ManagedAwake
|
||||
// ALWAYS add to managedAwakeList - this is the master list used for save/load
|
||||
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
|
||||
|
||||
// Handle ManagedAwake timing based on boot state
|
||||
if (isBootComplete)
|
||||
{
|
||||
// Boot already complete - call OnManagedAwake immediately
|
||||
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
|
||||
try
|
||||
// Check if we're currently loading a scene
|
||||
if (isLoadingScene && sceneName == sceneBeingLoaded)
|
||||
{
|
||||
component.InvokeManagedAwake();
|
||||
HandleAutoRegistrations(component);
|
||||
// Batch this component - will be processed in priority order when scene load completes
|
||||
pendingSceneComponents.Add(component);
|
||||
LogDebug($"Batched component for scene load: {component.gameObject.name} (Scene: {sceneName})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
|
||||
// Truly late registration (component enabled after scene is ready)
|
||||
// Call OnManagedAwake immediately since boot already completed
|
||||
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
|
||||
try
|
||||
{
|
||||
component.InvokeManagedAwake();
|
||||
HandleAutoRegistrations(component);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Boot not complete yet - add to list for broadcast
|
||||
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
|
||||
}
|
||||
// If boot not complete, component stays in list and will be processed by BroadcastManagedAwake()
|
||||
|
||||
// Register for all scene lifecycle hooks
|
||||
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
|
||||
@@ -143,20 +158,8 @@ namespace Core.Lifecycle
|
||||
InsertSorted(restoreRequestedList, component, component.RestorePriority);
|
||||
InsertSorted(destroyList, component, component.DestroyPriority);
|
||||
|
||||
// If this scene is already ready, call OnSceneReady immediately
|
||||
// Check both currentSceneReady AND if the Unity scene is actually loaded
|
||||
// (during scene loading, components Awake before BroadcastSceneReady is called)
|
||||
bool sceneIsReady = currentSceneReady == sceneName;
|
||||
|
||||
// Also check if this is happening during boot and the scene is the active scene
|
||||
// This handles components that register during initial scene load
|
||||
if (!sceneIsReady && isBootComplete && sceneName != "DontDestroyOnLoad")
|
||||
{
|
||||
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneName);
|
||||
sceneIsReady = scene.isLoaded;
|
||||
}
|
||||
|
||||
if (sceneIsReady)
|
||||
// If this scene is already ready (and we're not in loading mode), call OnSceneReady immediately
|
||||
if (!isLoadingScene && currentSceneReady == sceneName)
|
||||
{
|
||||
LogDebug($"Late registration: Calling OnSceneReady immediately for {component.gameObject.name}");
|
||||
try
|
||||
@@ -233,8 +236,63 @@ namespace Core.Lifecycle
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the list - components already initialized
|
||||
managedAwakeList.Clear();
|
||||
// NOTE: We do NOT clear managedAwakeList here!
|
||||
// This list is reused for save/load broadcasts and must persist for the lifetime of the game.
|
||||
// Components are added during registration and removed during Unregister (OnDestroy).
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins scene loading mode for the specified scene.
|
||||
/// Components that register during this time will be batched and processed in priority order.
|
||||
/// Call this BEFORE starting to load a scene.
|
||||
/// </summary>
|
||||
public void BeginSceneLoad(string sceneName)
|
||||
{
|
||||
isLoadingScene = true;
|
||||
sceneBeingLoaded = sceneName;
|
||||
pendingSceneComponents.Clear();
|
||||
LogDebug($"Began scene loading mode for: {sceneName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes all batched components from the scene load in priority order.
|
||||
/// Called automatically by BroadcastSceneReady.
|
||||
/// </summary>
|
||||
private void ProcessBatchedSceneComponents()
|
||||
{
|
||||
if (pendingSceneComponents.Count == 0)
|
||||
{
|
||||
isLoadingScene = false;
|
||||
sceneBeingLoaded = "";
|
||||
return;
|
||||
}
|
||||
|
||||
LogDebug($"Processing {pendingSceneComponents.Count} batched components for scene: {sceneBeingLoaded}");
|
||||
|
||||
// Sort by ManagedAwake priority (lower values first)
|
||||
pendingSceneComponents.Sort((a, b) => a.ManagedAwakePriority.CompareTo(b.ManagedAwakePriority));
|
||||
|
||||
// Call OnManagedAwake in priority order
|
||||
foreach (var component in pendingSceneComponents)
|
||||
{
|
||||
if (component == null) continue;
|
||||
|
||||
try
|
||||
{
|
||||
component.InvokeManagedAwake();
|
||||
HandleAutoRegistrations(component);
|
||||
LogDebug($"Processed batched component: {component.gameObject.name} (Priority: {component.ManagedAwakePriority})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for batched component {component.gameObject.name}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// Clear state
|
||||
pendingSceneComponents.Clear();
|
||||
isLoadingScene = false;
|
||||
sceneBeingLoaded = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -264,41 +322,21 @@ namespace Core.Lifecycle
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast OnSaveRequested to components in the specified scene (reverse priority order).
|
||||
/// </summary>
|
||||
public void BroadcastSaveRequested(string sceneName)
|
||||
{
|
||||
LogDebug($"Broadcasting SaveRequested for scene: {sceneName}");
|
||||
|
||||
// Iterate backwards (high priority → low priority)
|
||||
for (int i = saveRequestedList.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var component = saveRequestedList[i];
|
||||
if (component == null) continue;
|
||||
|
||||
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
|
||||
{
|
||||
try
|
||||
{
|
||||
component.InvokeSaveRequested();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Error in OnSaveRequested for {component.gameObject.name}: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast OnSceneReady to components in the specified scene (priority order).
|
||||
/// If scene loading mode is active, processes batched components first.
|
||||
/// </summary>
|
||||
public void BroadcastSceneReady(string sceneName)
|
||||
{
|
||||
LogDebug($"Broadcasting SceneReady for scene: {sceneName}");
|
||||
currentSceneReady = sceneName;
|
||||
|
||||
// If we were in scene loading mode for this scene, process batched components first
|
||||
if (isLoadingScene && sceneBeingLoaded == sceneName)
|
||||
{
|
||||
ProcessBatchedSceneComponents();
|
||||
}
|
||||
|
||||
foreach (var component in sceneReadyList)
|
||||
{
|
||||
if (component == null) continue;
|
||||
@@ -318,28 +356,179 @@ namespace Core.Lifecycle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast OnRestoreRequested to components in the specified scene (priority order).
|
||||
/// Broadcasts scene save request to all registered components that opt-in.
|
||||
/// Collects and returns serialized data from components that return non-null values.
|
||||
/// Called by SaveLoadManager during scene transitions.
|
||||
/// </summary>
|
||||
public void BroadcastRestoreRequested(string sceneName)
|
||||
public Dictionary<string, string> BroadcastSceneSaveRequested()
|
||||
{
|
||||
LogDebug($"Broadcasting RestoreRequested for scene: {sceneName}");
|
||||
|
||||
foreach (var component in restoreRequestedList)
|
||||
var saveData = new Dictionary<string, string>();
|
||||
|
||||
foreach (var component in managedAwakeList)
|
||||
{
|
||||
if (component == null) continue;
|
||||
if (component == null || !component.AutoRegisterForSave) continue;
|
||||
|
||||
try
|
||||
{
|
||||
string serializedData = component.InvokeSceneSaveRequested();
|
||||
if (!string.IsNullOrEmpty(serializedData))
|
||||
{
|
||||
saveData[component.SaveId] = serializedData;
|
||||
LogDebug($"Collected scene save data from: {component.SaveId}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Exception during scene save for {component.SaveId}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
LogDebug($"Collected scene save data from {saveData.Count} components");
|
||||
return saveData;
|
||||
}
|
||||
|
||||
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
|
||||
/// <summary>
|
||||
/// Broadcasts global save request to all registered components that opt-in.
|
||||
/// Collects and returns serialized data from components that return non-null values.
|
||||
/// Called by SaveLoadManager when writing save file to disk (quit, manual save).
|
||||
/// </summary>
|
||||
public Dictionary<string, string> BroadcastGlobalSaveRequested()
|
||||
{
|
||||
var saveData = new Dictionary<string, string>();
|
||||
|
||||
foreach (var component in managedAwakeList)
|
||||
{
|
||||
if (component == null || !component.AutoRegisterForSave) continue;
|
||||
|
||||
try
|
||||
{
|
||||
string serializedData = component.InvokeGlobalSaveRequested();
|
||||
if (!string.IsNullOrEmpty(serializedData))
|
||||
{
|
||||
saveData[component.SaveId] = serializedData;
|
||||
LogDebug($"Collected global save data from: {component.SaveId}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Exception during global save for {component.SaveId}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
LogDebug($"Collected global save data from {saveData.Count} components");
|
||||
return saveData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts scene restore request to all registered components that opt-in.
|
||||
/// Distributes serialized data to matching components by SaveId.
|
||||
/// Called by SaveLoadManager during scene load.
|
||||
/// </summary>
|
||||
public void BroadcastSceneRestoreRequested(Dictionary<string, string> saveData)
|
||||
{
|
||||
if (saveData == null) return;
|
||||
|
||||
int restoredCount = 0;
|
||||
foreach (var component in managedAwakeList)
|
||||
{
|
||||
if (component == null || !component.AutoRegisterForSave) continue;
|
||||
|
||||
if (saveData.TryGetValue(component.SaveId, out string serializedData))
|
||||
{
|
||||
try
|
||||
{
|
||||
component.InvokeRestoreRequested();
|
||||
component.InvokeSceneRestoreRequested(serializedData);
|
||||
restoredCount++;
|
||||
LogDebug($"Restored scene data to: {component.SaveId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Error in OnRestoreRequested for {component.gameObject.name}: {ex}");
|
||||
Debug.LogError($"[LifecycleManager] Exception during scene restore for {component.SaveId}: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogDebug($"Restored scene data to {restoredCount} components");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts global restore request to all registered components that opt-in.
|
||||
/// Distributes serialized data to matching components by SaveId.
|
||||
/// Called by SaveLoadManager during initial boot load.
|
||||
/// </summary>
|
||||
public void BroadcastGlobalRestoreRequested(Dictionary<string, string> saveData)
|
||||
{
|
||||
if (saveData == null) return;
|
||||
|
||||
int restoredCount = 0;
|
||||
foreach (var component in managedAwakeList)
|
||||
{
|
||||
if (component == null || !component.AutoRegisterForSave) continue;
|
||||
|
||||
if (saveData.TryGetValue(component.SaveId, out string serializedData))
|
||||
{
|
||||
try
|
||||
{
|
||||
component.InvokeGlobalRestoreRequested(serializedData);
|
||||
restoredCount++;
|
||||
LogDebug($"Restored global data to: {component.SaveId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Exception during global restore for {component.SaveId}: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogDebug($"Restored global data to {restoredCount} components");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts global load completed event to all registered components that opt-in.
|
||||
/// Called ONCE after save file is successfully loaded on game boot.
|
||||
/// NOT called during scene transitions.
|
||||
/// </summary>
|
||||
public void BroadcastGlobalLoadCompleted()
|
||||
{
|
||||
LogDebug("Broadcasting GlobalLoadCompleted");
|
||||
|
||||
foreach (var component in managedAwakeList)
|
||||
{
|
||||
if (component == null || !component.AutoRegisterForSave) continue;
|
||||
|
||||
try
|
||||
{
|
||||
component.InvokeGlobalLoadCompleted();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Exception during global load for {component.name}: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts global save started event to all registered components that opt-in.
|
||||
/// Called ONCE before save file is written to disk.
|
||||
/// NOT called during scene transitions.
|
||||
/// </summary>
|
||||
public void BroadcastGlobalSaveStarted()
|
||||
{
|
||||
LogDebug("Broadcasting GlobalSaveStarted");
|
||||
|
||||
foreach (var component in managedAwakeList)
|
||||
{
|
||||
if (component == null || !component.AutoRegisterForSave) continue;
|
||||
|
||||
try
|
||||
{
|
||||
component.InvokeGlobalSaveStarted();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LifecycleManager] Exception during global save for {component.name}: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -57,6 +57,27 @@ namespace Core.Lifecycle
|
||||
/// </summary>
|
||||
public virtual bool AutoRegisterPausable => false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, this component participates in the save/load system.
|
||||
/// Components should override OnSaveRequested() and OnRestoreRequested().
|
||||
/// Default: false
|
||||
/// </summary>
|
||||
public virtual bool AutoRegisterForSave => false;
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for this component in the save system.
|
||||
/// Default: "SceneName/GameObjectName"
|
||||
/// Override ONLY for special cases (e.g., singletons like "PlayerController", or custom IDs).
|
||||
/// </summary>
|
||||
public virtual string SaveId
|
||||
{
|
||||
get
|
||||
{
|
||||
string sceneName = gameObject.scene.IsValid() ? gameObject.scene.name : "UnknownScene";
|
||||
return $"{sceneName}/{gameObject.name}";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Accessors (for LifecycleManager)
|
||||
@@ -65,9 +86,13 @@ namespace Core.Lifecycle
|
||||
public void InvokeManagedAwake() => OnManagedAwake();
|
||||
public void InvokeSceneUnloading() => OnSceneUnloading();
|
||||
public void InvokeSceneReady() => OnSceneReady();
|
||||
public void InvokeSaveRequested() => OnSaveRequested();
|
||||
public void InvokeRestoreRequested() => OnRestoreRequested();
|
||||
public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
|
||||
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
|
||||
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
|
||||
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
|
||||
public void InvokeManagedDestroy() => OnManagedDestroy();
|
||||
public void InvokeGlobalLoadCompleted() => OnGlobalLoadCompleted();
|
||||
public void InvokeGlobalSaveStarted() => OnGlobalSaveStarted();
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -158,25 +183,91 @@ namespace Core.Lifecycle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before scene unloads to save data via SaveLoadManager.
|
||||
/// Called in REVERSE priority order (higher values execute first).
|
||||
/// Integrates with existing SaveLoadManager save system.
|
||||
/// Return serialized state string (e.g., JsonUtility.ToJson(myData)).
|
||||
/// Called during scene transitions to save scene-specific state.
|
||||
/// Return serialized data (e.g., JsonUtility.ToJson(myData)).
|
||||
/// Return null if component has no scene-specific state to save.
|
||||
///
|
||||
/// TIMING:
|
||||
/// - Called BEFORE scene unload during scene transitions
|
||||
/// - Frequency: Every scene transition
|
||||
/// - Use for: Level progress, object positions, puzzle states
|
||||
/// </summary>
|
||||
protected virtual void OnSaveRequested()
|
||||
protected virtual string OnSceneSaveRequested()
|
||||
{
|
||||
// Override in derived classes
|
||||
return null; // Default: no data to save
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after scene loads to restore data via SaveLoadManager.
|
||||
/// Called in priority order (lower values execute first).
|
||||
/// Integrates with existing SaveLoadManager restore system.
|
||||
/// Receives serialized state string to restore from.
|
||||
/// Called during scene transitions to restore scene-specific state.
|
||||
/// Receives previously serialized data (from OnSceneSaveRequested).
|
||||
///
|
||||
/// TIMING:
|
||||
/// - Called AFTER scene load, during OnSceneReady phase
|
||||
/// - Frequency: Every scene transition
|
||||
/// - Use for: Restoring level progress, object positions, puzzle states
|
||||
/// </summary>
|
||||
protected virtual void OnRestoreRequested()
|
||||
protected virtual void OnSceneRestoreRequested(string serializedData)
|
||||
{
|
||||
// Override in derived classes
|
||||
// Default: no-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once on game boot to restore global persistent state.
|
||||
/// Receives data that was saved via OnGlobalSaveRequested.
|
||||
///
|
||||
/// TIMING:
|
||||
/// - Called ONCE on game boot after save file is read
|
||||
/// - NOT called during scene transitions
|
||||
/// - Use for: Player inventory, unlocked features, card collections
|
||||
/// </summary>
|
||||
protected virtual void OnGlobalRestoreRequested(string serializedData)
|
||||
{
|
||||
// Default: no-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once before game save file is written to disk.
|
||||
/// Return serialized data for global persistent state.
|
||||
/// Return null if component has no global state to save.
|
||||
///
|
||||
/// TIMING:
|
||||
/// - Called ONCE before save file is written (on quit, manual save, etc.)
|
||||
/// - NOT called during scene transitions
|
||||
/// - Use for: Player inventory, unlocked features, card collections
|
||||
/// </summary>
|
||||
protected virtual string OnGlobalSaveRequested()
|
||||
{
|
||||
return null; // Default: no data to save
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once when game save data is initially loaded from disk.
|
||||
/// Use for global managers that need to react to load completion.
|
||||
/// Does NOT receive data - use OnGlobalRestoreRequested for that.
|
||||
///
|
||||
/// TIMING:
|
||||
/// - Called ONCE on game boot after all restore operations complete
|
||||
/// - NOT called during scene transitions
|
||||
/// - Use for: Triggering UI updates, broadcasting load events
|
||||
/// </summary>
|
||||
protected virtual void OnGlobalLoadCompleted()
|
||||
{
|
||||
// Default: no-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once before save file is written to disk.
|
||||
/// Use for global managers that need to perform cleanup before save.
|
||||
/// Does NOT return data - use OnGlobalSaveRequested for that.
|
||||
///
|
||||
/// TIMING:
|
||||
/// - Called ONCE before save file is written
|
||||
/// - NOT called during scene transitions
|
||||
/// - Use for: Final validation, cleanup operations
|
||||
/// </summary>
|
||||
protected virtual void OnGlobalSaveStarted()
|
||||
{
|
||||
// Default: no-op
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user