SaveLoad using managed lifecycle
This commit is contained in:
committed by
Michal Pikulski
parent
3e835ed3b8
commit
b932be2232
@@ -1,6 +1,4 @@
|
||||
using Core.SaveLoad;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
@@ -8,21 +6,13 @@ namespace Interactions
|
||||
/// Base class for interactables that participate in the save/load system.
|
||||
/// Provides common save ID generation and serialization infrastructure.
|
||||
/// </summary>
|
||||
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
|
||||
public abstract class SaveableInteractable : InteractableBase
|
||||
{
|
||||
[Header("Save System")]
|
||||
[SerializeField]
|
||||
[Tooltip("Optional custom save ID. If empty, will auto-generate from hierarchy path.")]
|
||||
private string customSaveId = "";
|
||||
|
||||
/// <summary>
|
||||
/// Sets a custom save ID for this interactable.
|
||||
/// Used when spawning dynamic objects that need stable save IDs.
|
||||
/// </summary>
|
||||
public void SetCustomSaveId(string saveId)
|
||||
{
|
||||
customSaveId = saveId;
|
||||
}
|
||||
|
||||
// Save system configuration
|
||||
public override bool AutoRegisterForSave => true;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to indicate we're currently restoring from save data.
|
||||
@@ -30,99 +20,10 @@ namespace Interactions
|
||||
/// </summary>
|
||||
protected bool IsRestoringFromSave { get; private set; }
|
||||
|
||||
private bool hasRegistered;
|
||||
private bool hasRestoredState;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this participant has already had its state restored.
|
||||
/// </summary>
|
||||
public bool HasBeenRestored => hasRestoredState;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// Register early in Awake so even disabled objects are tracked
|
||||
RegisterWithSaveSystem();
|
||||
}
|
||||
#region Save/Load Lifecycle Hooks
|
||||
|
||||
protected virtual void Start()
|
||||
{
|
||||
// If we didn't register in Awake (shouldn't happen), register now
|
||||
if (!hasRegistered)
|
||||
{
|
||||
RegisterWithSaveSystem();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
UnregisterFromSaveSystem();
|
||||
}
|
||||
|
||||
private void RegisterWithSaveSystem()
|
||||
{
|
||||
if (hasRegistered) return;
|
||||
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.RegisterParticipant(this);
|
||||
hasRegistered = true;
|
||||
|
||||
// Check if save data was already loaded before we registered
|
||||
// If so, we need to subscribe to the next load event
|
||||
if (!SaveLoadManager.Instance.IsSaveDataLoaded && !hasRestoredState)
|
||||
{
|
||||
SaveLoadManager.Instance.OnLoadCompleted += OnSaveDataLoadedHandler;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[SaveableInteractable] SaveLoadManager not found for {gameObject.name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterFromSaveSystem()
|
||||
{
|
||||
if (!hasRegistered) return;
|
||||
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
|
||||
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
||||
hasRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event handler for when save data finishes loading.
|
||||
/// Called if the object registered before save data was loaded.
|
||||
/// </summary>
|
||||
private void OnSaveDataLoadedHandler(string slot)
|
||||
{
|
||||
// The SaveLoadManager will automatically call RestoreState on us
|
||||
// We just need to unsubscribe from the event
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
#region ISaveParticipant Implementation
|
||||
|
||||
public string GetSaveId()
|
||||
{
|
||||
string sceneName = GetSceneName();
|
||||
|
||||
if (!string.IsNullOrEmpty(customSaveId))
|
||||
{
|
||||
return $"{sceneName}/{customSaveId}";
|
||||
}
|
||||
|
||||
// Auto-generate from hierarchy path
|
||||
string hierarchyPath = GetHierarchyPath();
|
||||
return $"{sceneName}/{hierarchyPath}";
|
||||
}
|
||||
|
||||
public string SerializeState()
|
||||
protected override string OnSceneSaveRequested()
|
||||
{
|
||||
object stateData = GetSerializableState();
|
||||
if (stateData == null)
|
||||
@@ -133,28 +34,17 @@ namespace Interactions
|
||||
return JsonUtility.ToJson(stateData);
|
||||
}
|
||||
|
||||
public void RestoreState(string serializedData)
|
||||
protected override void OnSceneRestoreRequested(string serializedData)
|
||||
{
|
||||
if (string.IsNullOrEmpty(serializedData))
|
||||
{
|
||||
Debug.LogWarning($"[SaveableInteractable] Empty save data for {GetSaveId()}");
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Only restore state if we're actually in a restoration context
|
||||
// This prevents state machines from teleporting objects when they enable them mid-gameplay
|
||||
if (SaveLoadManager.Instance != null && !SaveLoadManager.Instance.IsRestoringState)
|
||||
{
|
||||
// If we're not in an active restoration cycle, this is probably a late registration
|
||||
// (object was disabled during initial load and just got enabled)
|
||||
// Skip restoration to avoid mid-gameplay teleportation
|
||||
Debug.Log($"[SaveableInteractable] Skipping late restoration for {GetSaveId()} - object enabled after initial load");
|
||||
hasRestoredState = true; // Mark as restored to prevent future attempts
|
||||
Debug.LogWarning($"[SaveableInteractable] Empty save data for {SaveId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// OnSceneRestoreRequested is guaranteed by the lifecycle system to only fire during actual restoration
|
||||
// No need to check IsRestoringState - the lifecycle manager handles timing deterministically
|
||||
IsRestoringFromSave = true;
|
||||
hasRestoredState = true;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -162,7 +52,7 @@ namespace Interactions
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"[SaveableInteractable] Failed to restore state for {GetSaveId()}: {e.Message}");
|
||||
Debug.LogError($"[SaveableInteractable] Failed to restore state for {SaveId}: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -189,61 +79,22 @@ namespace Interactions
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private string GetSceneName()
|
||||
{
|
||||
Scene scene = gameObject.scene;
|
||||
if (!scene.IsValid())
|
||||
{
|
||||
Debug.LogWarning($"[SaveableInteractable] GameObject {gameObject.name} has invalid scene");
|
||||
return "UnknownScene";
|
||||
}
|
||||
|
||||
return scene.name;
|
||||
}
|
||||
|
||||
private string GetHierarchyPath()
|
||||
{
|
||||
// Build path from scene root to this object
|
||||
// Format: ParentName/ChildName/ObjectName_SiblingIndex
|
||||
string path = gameObject.name;
|
||||
Transform current = transform.parent;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
path = $"{current.name}/{path}";
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
// Add sibling index for uniqueness among same-named objects
|
||||
int siblingIndex = transform.GetSiblingIndex();
|
||||
if (siblingIndex > 0)
|
||||
{
|
||||
path = $"{path}_{siblingIndex}";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Editor Helpers
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[ContextMenu("Log Save ID")]
|
||||
private void LogSaveId()
|
||||
{
|
||||
Debug.Log($"Save ID: {GetSaveId()}");
|
||||
Debug.Log($"Save ID: {SaveId}");
|
||||
}
|
||||
|
||||
[ContextMenu("Test Serialize/Deserialize")]
|
||||
private void TestSerializeDeserialize()
|
||||
{
|
||||
string serialized = SerializeState();
|
||||
string serialized = OnSceneSaveRequested();
|
||||
Debug.Log($"Serialized state: {serialized}");
|
||||
|
||||
RestoreState(serialized);
|
||||
OnSceneRestoreRequested(serialized);
|
||||
Debug.Log("Deserialization test complete");
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user