SaveLoad using managed lifecycle

This commit is contained in:
Michal Pikulski
2025-11-04 20:01:27 +01:00
committed by Michal Pikulski
parent 3e835ed3b8
commit b932be2232
19 changed files with 1042 additions and 627 deletions

View File

@@ -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

View File

@@ -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>