Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51)
# Lifecycle Management & Save System Revamp
## Overview
Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems.
## Core Architecture
### New Lifecycle System
- **`LifecycleManager`**: Centralized coordinator for all managed objects
- **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns
- `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier)
- `OnSceneReady()`: Scene-specific setup after managers ready
- Replaces `BootCompletionService` (deleted)
- **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100)
- **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode
### Unified SaveID System
- Consistent format: `{ParentName}_{ComponentType}`
- Auto-registration via `AutoRegisterForSave = true`
- New `DebugSaveIds` editor tool for inspection
## Save/Load Improvements
### Enhanced State Management
- **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy
- **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring
- **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state
- **Fixed race conditions**: Proper initialization ordering prevents data corruption
## Interactable & Pickup System
- Migrated to `OnManagedAwake()` for consistent initialization
- Template method pattern for state restoration (`RestoreInteractionState()`)
- Fixed combination item save/load bugs (items in slots vs. follower hand)
- Dynamic spawning support for combined items on load
- **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead
## UI System Changes
- **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour`
- **Fixed menu persistence bug**: Menus no longer reappear after scene transitions
- **Pause Menu**: Now reacts to all scene loads (not just first scene)
- **Orientation Enforcer**: Enforces per-scene via `SceneManagementService`
- **Loading Screen**: Integrated with new lifecycle
## ⚠️ Breaking Changes
1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority
2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead
3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently
4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour`
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Reviewed-on: #51
This commit is contained in:
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AppleHills.Core.Settings;
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Bootstrap
|
||||
{
|
||||
/// <summary>
|
||||
/// Service that provides notification and management of boot completion status.
|
||||
/// Allows systems to subscribe to boot completion events, register initialization actions with priorities,
|
||||
/// or await boot completion asynchronously.
|
||||
/// </summary>
|
||||
public static class BootCompletionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the boot process has completed
|
||||
/// </summary>
|
||||
public static bool IsBootComplete { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when boot completes
|
||||
/// </summary>
|
||||
public static event Action OnBootComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an initialization action with priority
|
||||
/// </summary>
|
||||
private class InitializationAction
|
||||
{
|
||||
public Action Action { get; }
|
||||
public int Priority { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public InitializationAction(Action action, int priority, string name)
|
||||
{
|
||||
Action = action;
|
||||
Priority = priority;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// List of initialization actions to be executed once boot completes
|
||||
private static List<InitializationAction> _initializationActions = new List<InitializationAction>();
|
||||
|
||||
// TaskCompletionSource for async await pattern
|
||||
private static TaskCompletionSource<bool> _bootCompletionTask = new TaskCompletionSource<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Called by CustomBoot when the boot process is complete
|
||||
/// </summary>
|
||||
internal static void HandleBootCompleted()
|
||||
{
|
||||
if (IsBootComplete)
|
||||
return;
|
||||
|
||||
IsBootComplete = true;
|
||||
|
||||
LogDebugMessage("Boot process completed, executing initialization actions");
|
||||
|
||||
// Execute initialization actions in priority order (lower number = higher priority)
|
||||
ExecuteInitializationActions();
|
||||
|
||||
// Trigger the event
|
||||
OnBootComplete?.Invoke();
|
||||
|
||||
// Complete the task for async waiters
|
||||
_bootCompletionTask.TrySetResult(true);
|
||||
|
||||
LogDebugMessage("All boot completion handlers executed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register an action to be executed when boot completes.
|
||||
/// Lower priority numbers run first.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute</param>
|
||||
/// <param name="priority">Priority (lower numbers run first)</param>
|
||||
/// <param name="name">Name for debugging</param>
|
||||
public static void RegisterInitAction(Action action, int priority = 100, string name = null)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
name = $"Action_{_initializationActions.Count}";
|
||||
|
||||
var initAction = new InitializationAction(action, priority, name);
|
||||
|
||||
if (IsBootComplete)
|
||||
{
|
||||
// If boot is already complete, execute immediately
|
||||
LogDebugMessage($"Executing late registration: {name} (Priority: {priority})");
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogDebugMessage($"Error executing init action '{name}': {ex}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise add to the queue
|
||||
_initializationActions.Add(initAction);
|
||||
LogDebugMessage($"Registered init action: {name} (Priority: {priority})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait asynchronously for boot completion
|
||||
/// </summary>
|
||||
/// <returns>Task that completes when boot is complete</returns>
|
||||
public static Task WaitForBootCompletionAsync()
|
||||
{
|
||||
if (IsBootComplete)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return _bootCompletionTask.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute all registered initialization actions in priority order
|
||||
/// </summary>
|
||||
private static void ExecuteInitializationActions()
|
||||
{
|
||||
// Sort by priority (lowest first)
|
||||
var sortedActions = _initializationActions
|
||||
.OrderBy(a => a.Priority)
|
||||
.ToList();
|
||||
|
||||
foreach (var action in sortedActions)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogDebugMessage($"Executing: {action.Name} (Priority: {action.Priority})");
|
||||
action.Action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogDebugMessage($"Error executing init action '{action.Name}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the list after execution
|
||||
_initializationActions.Clear();
|
||||
}
|
||||
|
||||
private static void LogDebugMessage(string message)
|
||||
{
|
||||
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().bootstrapLogVerbosity <=
|
||||
LogVerbosity.Debug)
|
||||
{
|
||||
Logging.Debug($"[BootCompletionService] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa0228cf33a64515bc166b7a9bc8c0b9
|
||||
timeCreated: 1760606319
|
||||
@@ -1,18 +1,18 @@
|
||||
using System;
|
||||
using System;
|
||||
using AppleHills.Core.Settings;
|
||||
using UnityEngine;
|
||||
using UI;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Cinematics;
|
||||
using UnityEngine.Serialization;
|
||||
using Core.SaveLoad;
|
||||
|
||||
namespace Bootstrap
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for the boot scene that coordinates bootstrap initialization with loading screen
|
||||
/// </summary>
|
||||
public class BootSceneController : MonoBehaviour
|
||||
public class BootSceneController : ManagedBehaviour
|
||||
{
|
||||
[SerializeField] private string mainSceneName = "AppleHillsOverworld";
|
||||
[SerializeField] private float minDelayAfterBoot = 0.5f; // Small delay after boot to ensure smooth transition
|
||||
@@ -30,35 +30,32 @@ namespace Bootstrap
|
||||
private float _sceneLoadingProgress = 0f;
|
||||
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
|
||||
|
||||
private void Start()
|
||||
// Run very early - need to set up loading screen before other systems initialize
|
||||
public override int ManagedAwakePriority => 5;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
LogDebugMessage("Boot scene started");
|
||||
base.Awake(); // Register with LifecycleManager
|
||||
|
||||
// Ensure the initial loading screen exists
|
||||
LogDebugMessage("BootSceneController.Awake() - Initializing loading screen DURING bootstrap");
|
||||
|
||||
// Validate loading screen exists
|
||||
if (initialLoadingScreen == null)
|
||||
{
|
||||
Debug.LogError("[BootSceneController] No InitialLoadingScreen assigned! Please assign it in the inspector.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to the loading screen completion event
|
||||
initialLoadingScreen.OnLoadingScreenFullyHidden += OnInitialLoadingComplete;
|
||||
|
||||
// Show the loading screen immediately with our combined progress provider
|
||||
// This needs to happen DURING bootstrap to show progress
|
||||
initialLoadingScreen.ShowLoadingScreen(GetCombinedProgress);
|
||||
|
||||
// Subscribe to boot progress events
|
||||
// Subscribe to loading screen completion event
|
||||
initialLoadingScreen.OnLoadingScreenFullyHidden += OnInitialLoadingComplete;
|
||||
|
||||
// Subscribe to boot progress for real-time updates during bootstrap
|
||||
CustomBoot.OnBootProgressChanged += OnBootProgressChanged;
|
||||
|
||||
// Register our boot completion handler with the BootCompletionService
|
||||
// This will execute either immediately if boot is already complete,
|
||||
// or when the boot process completes
|
||||
BootCompletionService.RegisterInitAction(
|
||||
OnBootCompleted,
|
||||
50, // Higher priority (lower number)
|
||||
"BootSceneController.OnBootCompleted"
|
||||
);
|
||||
|
||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().bootstrapLogVerbosity;
|
||||
|
||||
// In debug mode, log additional information
|
||||
@@ -67,6 +64,32 @@ namespace Bootstrap
|
||||
InvokeRepeating(nameof(LogDebugInfo), 0.1f, 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
LogDebugMessage("BootSceneController.OnManagedAwake() - Boot is GUARANTEED complete, starting scene loading");
|
||||
|
||||
// Boot is GUARANTEED complete at this point - that's the whole point of OnManagedAwake!
|
||||
// No need to subscribe to OnBootCompleted or check CustomBoot.Initialised
|
||||
_bootComplete = true;
|
||||
_currentPhase = LoadingPhase.SceneLoading;
|
||||
|
||||
// Start loading the main scene after a small delay
|
||||
// This prevents jerky transitions if boot happens very quickly
|
||||
Invoke(nameof(StartLoadingMainMenu), minDelayAfterBoot);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
// Manual cleanup for events
|
||||
if (initialLoadingScreen != null)
|
||||
{
|
||||
initialLoadingScreen.OnLoadingScreenFullyHidden -= OnInitialLoadingComplete;
|
||||
}
|
||||
CustomBoot.OnBootProgressChanged -= OnBootProgressChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the initial loading screen is fully hidden
|
||||
@@ -94,23 +117,6 @@ namespace Bootstrap
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Clean up event subscriptions
|
||||
CustomBoot.OnBootCompleted -= OnBootCompleted;
|
||||
CustomBoot.OnBootProgressChanged -= OnBootProgressChanged;
|
||||
|
||||
if (initialLoadingScreen != null)
|
||||
{
|
||||
initialLoadingScreen.OnLoadingScreenFullyHidden -= OnInitialLoadingComplete;
|
||||
}
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
CancelInvoke(nameof(LogDebugInfo));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Progress provider that combines bootstrap and scene loading progress
|
||||
/// </summary>
|
||||
@@ -145,19 +151,7 @@ namespace Bootstrap
|
||||
$"Scene: {_sceneLoadingProgress:P0}, Combined: {GetCombinedProgress():P0}, Boot Complete: {_bootComplete}");
|
||||
}
|
||||
|
||||
private void OnBootCompleted()
|
||||
{
|
||||
// Unsubscribe to prevent duplicate calls
|
||||
CustomBoot.OnBootCompleted -= OnBootCompleted;
|
||||
|
||||
LogDebugMessage("Boot process completed");
|
||||
_bootComplete = true;
|
||||
|
||||
// After a small delay, start loading the main menu
|
||||
// This prevents jerky transitions if boot happens very quickly
|
||||
Invoke(nameof(StartLoadingMainMenu), minDelayAfterBoot);
|
||||
}
|
||||
|
||||
|
||||
private void StartLoadingMainMenu()
|
||||
{
|
||||
if (_hasStartedLoading)
|
||||
@@ -207,6 +201,17 @@ namespace Bootstrap
|
||||
// Ensure progress is complete
|
||||
_sceneLoadingProgress = 1f;
|
||||
|
||||
// CRITICAL: Broadcast lifecycle events so components get their OnSceneReady callbacks
|
||||
LogDebugMessage($"Broadcasting OnSceneReady for: {mainSceneName}");
|
||||
LifecycleManager.Instance?.BroadcastSceneReady(mainSceneName);
|
||||
|
||||
// Restore scene data for the main menu
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
LogDebugMessage($"Restoring scene data for: {mainSceneName}");
|
||||
SaveLoadManager.Instance.RestoreSceneData();
|
||||
}
|
||||
|
||||
// Step 2: Scene is fully loaded, now hide the loading screen
|
||||
// This will trigger OnInitialLoadingComplete via the event when animation completes
|
||||
initialLoadingScreen.HideLoadingScreen();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using AppleHills.Core.Settings;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
@@ -39,6 +40,10 @@ namespace Bootstrap
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
|
||||
private static void Initialise()
|
||||
{
|
||||
// Create LifecycleManager FIRST - before any bootstrap logic
|
||||
// This ensures it exists when boot completes
|
||||
LifecycleManager.CreateInstance();
|
||||
|
||||
//We should always clean up after Addressables, so let's take care of that immediately
|
||||
Application.quitting += ApplicationOnUnloading;
|
||||
|
||||
@@ -97,12 +102,14 @@ namespace Bootstrap
|
||||
OnBootProgressChanged?.Invoke(1f);
|
||||
OnBootCompleted?.Invoke();
|
||||
|
||||
// Notify the BootCompletionService that boot is complete
|
||||
// Notify the LifecycleManager that boot is complete
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
// Direct call to boot completion service
|
||||
LogDebugMessage("Calling BootCompletionService.HandleBootCompleted()");
|
||||
BootCompletionService.HandleBootCompleted();
|
||||
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
|
||||
if (LifecycleManager.Instance != null)
|
||||
{
|
||||
LifecycleManager.Instance.OnBootCompletionTriggered();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,12 +124,14 @@ namespace Bootstrap
|
||||
OnBootProgressChanged?.Invoke(1f);
|
||||
OnBootCompleted?.Invoke();
|
||||
|
||||
// Notify the BootCompletionService that boot is complete
|
||||
// Notify the LifecycleManager that boot is complete
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
// Direct call to boot completion service
|
||||
LogDebugMessage("Calling BootCompletionService.HandleBootCompleted()");
|
||||
BootCompletionService.HandleBootCompleted();
|
||||
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
|
||||
if (LifecycleManager.Instance != null)
|
||||
{
|
||||
LifecycleManager.Instance.OnBootCompletionTriggered();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user