2025-11-10 15:52:53 +01:00
using System ;
2025-09-01 22:51:52 +02:00
using System.Collections.Generic ;
using System.Threading.Tasks ;
2025-10-28 14:31:17 +01:00
using AppleHills.Core.Settings ;
2025-11-07 15:38:31 +00:00
using Core.Lifecycle ;
using Core.SaveLoad ;
2025-10-13 10:41:58 +02:00
using UI ;
2025-09-01 22:51:52 +02:00
using UnityEngine ;
using UnityEngine.SceneManagement ;
2025-10-13 10:41:58 +02:00
namespace Core
2025-09-01 22:51:52 +02:00
{
2025-09-06 21:01:54 +02:00
/// <summary>
2025-10-13 10:41:58 +02:00
/// Singleton service for loading and unloading Unity scenes asynchronously, with events for progress and completion.
2025-09-06 21:01:54 +02:00
/// </summary>
2025-11-07 15:38:31 +00:00
public class SceneManagerService : ManagedBehaviour
2025-09-04 00:00:46 +02:00
{
2025-10-13 14:25:11 +02:00
private LoadingScreenController _loadingScreen ;
2025-10-13 10:41:58 +02:00
private static SceneManagerService _instance ;
2025-10-16 19:43:19 +02:00
2025-10-13 10:41:58 +02:00
/// <summary>
2025-10-16 19:43:19 +02:00
/// Singleton instance of the SceneManagerService. No longer creates an instance if one doesn't exist.
2025-10-13 10:41:58 +02:00
/// </summary>
2025-10-16 19:43:19 +02:00
public static SceneManagerService Instance = > _instance ;
2025-09-01 22:51:52 +02:00
2025-10-13 10:41:58 +02:00
// Events for scene lifecycle
2025-11-07 15:38:31 +00:00
// NOTE: Most components should use lifecycle hooks (OnSceneReady, OnSceneUnloading)
// instead of subscribing to these events. Events are primarily for orchestration.
/// <summary>
/// Fired when a scene starts loading. Used by loading screen orchestration.
/// </summary>
2025-10-13 10:41:58 +02:00
public event Action < string > SceneLoadStarted ;
2025-11-07 15:38:31 +00:00
/// <summary>
/// Fired when a scene finishes loading.
/// Used by loading screen orchestration and cross-scene components (e.g., PauseMenu).
/// For component initialization, use OnSceneReady() lifecycle hook instead.
/// </summary>
2025-10-13 10:41:58 +02:00
public event Action < string > SceneLoadCompleted ;
2025-09-01 22:51:52 +02:00
2025-10-13 10:41:58 +02:00
private readonly Dictionary < string , AsyncOperation > _activeLoads = new ( ) ;
private readonly Dictionary < string , AsyncOperation > _activeUnloads = new ( ) ;
2025-10-28 14:31:17 +01:00
private LogVerbosity _logVerbosity = LogVerbosity . Debug ;
2025-10-13 10:41:58 +02:00
private const string BootstrapSceneName = "BootstrapScene" ;
2025-09-01 22:51:52 +02:00
2025-11-07 15:38:31 +00:00
// ManagedBehaviour configuration
public override int ManagedAwakePriority = > 15 ; // Core infrastructure, after GameManager
2025-11-10 15:52:53 +01:00
protected override void OnManagedAwake ( )
2025-10-13 14:25:11 +02:00
{
2025-11-10 15:52:53 +01:00
// Set instance immediately (early initialization)
2025-10-16 19:43:19 +02:00
_instance = this ;
2025-10-13 14:25:11 +02:00
2025-11-07 15:38:31 +00:00
// Initialize current scene tracking - critical for scene management
2025-10-16 19:43:19 +02:00
InitializeCurrentSceneTracking ( ) ;
// Ensure BootstrapScene is loaded at startup
var bootstrap = SceneManager . GetSceneByName ( BootstrapSceneName ) ;
if ( ! bootstrap . isLoaded )
{
SceneManager . LoadScene ( BootstrapSceneName , LoadSceneMode . Additive ) ;
}
2025-10-13 14:25:11 +02:00
}
2025-10-28 14:31:17 +01:00
2025-11-10 15:52:53 +01:00
protected override void OnManagedStart ( )
2025-10-28 14:31:17 +01:00
{
2025-11-07 15:38:31 +00:00
// Set up loading screen reference and events
2025-11-10 15:52:53 +01:00
// This must happen in ManagedStart because LoadingScreenController instance needs to be set first
2025-11-07 15:38:31 +00:00
_loadingScreen = LoadingScreenController . Instance ;
SetupLoadingScreenEvents ( ) ;
// Load verbosity settings
2025-10-28 14:31:17 +01:00
_logVerbosity = DeveloperSettingsProvider . Instance . GetSettings < DebugSettings > ( ) . sceneLogVerbosity ;
2025-11-07 15:38:31 +00:00
LogDebugMessage ( $"SceneManagerService initialized, current scene is: {CurrentGameplayScene}" ) ;
2025-10-28 14:31:17 +01:00
}
2025-10-16 19:43:19 +02:00
/// <summary>
/// Initialize current scene tracking immediately in Awake
/// This ensures scene management works correctly regardless of boot timing
/// </summary>
private void InitializeCurrentSceneTracking ( )
2025-09-12 15:37:26 +02:00
{
2025-10-16 19:43:19 +02:00
// Get the active scene and use it as the current gameplay scene
Scene activeScene = SceneManager . GetActiveScene ( ) ;
if ( activeScene . IsValid ( ) )
2025-09-12 15:37:26 +02:00
{
2025-10-16 19:43:19 +02:00
// If this is the MainMenu or another gameplay scene, track it
if ( activeScene . name ! = BootstrapSceneName )
2025-10-13 10:41:58 +02:00
{
CurrentGameplayScene = activeScene . name ;
2025-10-28 14:31:17 +01:00
LogDebugMessage ( $"Initialized with current scene: {CurrentGameplayScene}" ) ;
2025-10-16 19:43:19 +02:00
}
// Otherwise default to MainMenu
else
{
2025-10-17 14:03:27 +02:00
CurrentGameplayScene = "AppleHillsOverworld" ;
2025-10-28 14:31:17 +01:00
LogDebugMessage ( $"Initialized with default scene: {CurrentGameplayScene}" ) ;
2025-10-13 10:41:58 +02:00
}
2025-09-12 15:37:26 +02:00
}
2025-10-16 19:43:19 +02:00
else
2025-10-13 10:41:58 +02:00
{
2025-10-17 14:03:27 +02:00
CurrentGameplayScene = "AppleHillsOverworld" ;
2025-10-28 14:31:17 +01:00
LogDebugMessage ( $"No valid active scene, defaulting to: {CurrentGameplayScene}" ) ;
2025-10-13 10:41:58 +02:00
}
2025-09-12 15:37:26 +02:00
}
2025-10-13 10:41:58 +02:00
private void SetupLoadingScreenEvents ( )
2025-09-01 22:51:52 +02:00
{
2025-10-13 14:25:11 +02:00
if ( _loadingScreen = = null ) return ;
SceneLoadStarted + = _ = > _loadingScreen . ShowLoadingScreen ( ( ) = > GetAggregateLoadProgress ( ) ) ;
SceneLoadCompleted + = _ = > _loadingScreen . HideLoadingScreen ( ) ;
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
/// <summary>
/// Load a single scene asynchronously (additive).
/// </summary>
/// <param name="sceneName">Name of the scene to load.</param>
/// <param name="progress">Optional progress reporter.</param>
public async Task LoadSceneAsync ( string sceneName , IProgress < float > progress = null )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
SceneLoadStarted ? . Invoke ( sceneName ) ;
var op = SceneManager . LoadSceneAsync ( sceneName , LoadSceneMode . Additive ) ;
_activeLoads [ sceneName ] = op ;
while ( ! op . isDone )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
progress ? . Report ( op . progress ) ;
await Task . Yield ( ) ;
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
_activeLoads . Remove ( sceneName ) ;
SceneLoadCompleted ? . Invoke ( sceneName ) ;
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
/// <summary>
/// Unload a single scene asynchronously.
/// </summary>
/// <param name="sceneName">Name of the scene to unload.</param>
/// <param name="progress">Optional progress reporter.</param>
public async Task UnloadSceneAsync ( string sceneName , IProgress < float > progress = null )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
var scene = SceneManager . GetSceneByName ( sceneName ) ;
if ( ! scene . isLoaded )
{
2025-10-28 14:31:17 +01:00
Logging . Warning ( $"[SceneManagerService] Attempted to unload scene '{sceneName}', but it is not loaded." ) ;
2025-10-13 10:41:58 +02:00
return ;
}
2025-11-07 15:38:31 +00:00
2025-10-13 10:41:58 +02:00
var op = SceneManager . UnloadSceneAsync ( sceneName ) ;
_activeUnloads [ sceneName ] = op ;
while ( ! op . isDone )
{
progress ? . Report ( op . progress ) ;
await Task . Yield ( ) ;
}
_activeUnloads . Remove ( sceneName ) ;
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
/// <summary>
/// Load multiple scenes asynchronously.
/// </summary>
/// <param name="sceneNames">Enumerable of scene names to load.</param>
/// <param name="progress">Optional progress reporter.</param>
public async Task LoadScenesAsync ( IEnumerable < string > sceneNames , IProgress < float > progress = null )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
// Show loading screen at the start of multiple scene loading
2025-10-13 14:25:11 +02:00
if ( _loadingScreen ! = null )
2025-10-13 10:41:58 +02:00
{
2025-10-13 14:25:11 +02:00
_loadingScreen . ShowLoadingScreen ( ) ;
2025-10-13 10:41:58 +02:00
}
int total = 0 ;
int done = 0 ;
var ops = new List < AsyncOperation > ( ) ;
foreach ( var name in sceneNames )
{
total + + ;
var op = SceneManager . LoadSceneAsync ( name , LoadSceneMode . Additive ) ;
_activeLoads [ name ] = op ;
ops . Add ( op ) ;
SceneLoadStarted ? . Invoke ( name ) ;
}
while ( done < total )
{
done = 0 ;
float aggregate = 0f ;
foreach ( var op in ops )
{
if ( op . isDone ) done + + ;
aggregate + = op . progress ;
}
float avgProgress = aggregate / total ;
progress ? . Report ( avgProgress ) ;
await Task . Yield ( ) ;
}
foreach ( var name in sceneNames )
{
_activeLoads . Remove ( name ) ;
SceneLoadCompleted ? . Invoke ( name ) ;
}
// Hide loading screen after all scenes are loaded
2025-10-13 14:25:11 +02:00
if ( _loadingScreen ! = null )
2025-10-13 10:41:58 +02:00
{
2025-10-13 14:25:11 +02:00
_loadingScreen . HideLoadingScreen ( ) ;
2025-10-13 10:41:58 +02:00
}
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
/// <summary>
/// Unload multiple scenes asynchronously.
/// </summary>
/// <param name="sceneNames">Enumerable of scene names to unload.</param>
/// <param name="progress">Optional progress reporter.</param>
public async Task UnloadScenesAsync ( IEnumerable < string > sceneNames , IProgress < float > progress = null )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
// Show loading screen at the start of multiple scene unloading
2025-10-13 14:25:11 +02:00
if ( _loadingScreen ! = null )
2025-09-01 22:51:52 +02:00
{
2025-10-13 14:25:11 +02:00
_loadingScreen . ShowLoadingScreen ( ) ;
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
int total = 0 ;
int done = 0 ;
var ops = new List < AsyncOperation > ( ) ;
2025-09-01 22:51:52 +02:00
foreach ( var name in sceneNames )
2025-10-13 10:41:58 +02:00
{
total + + ;
var op = SceneManager . UnloadSceneAsync ( name ) ;
_activeUnloads [ name ] = op ;
ops . Add ( op ) ;
}
while ( done < total )
{
done = 0 ;
float aggregate = 0f ;
foreach ( var op in ops )
{
aggregate + = op . progress ;
if ( op . isDone ) done + + ;
}
float avg = aggregate / total ;
progress ? . Report ( avg ) ;
await Task . Yield ( ) ;
}
foreach ( var name in sceneNames )
{
_activeUnloads . Remove ( name ) ;
}
// Hide loading screen after all scenes are unloaded
2025-10-13 14:25:11 +02:00
if ( _loadingScreen ! = null )
2025-10-13 10:41:58 +02:00
{
2025-10-13 14:25:11 +02:00
_loadingScreen . HideLoadingScreen ( ) ;
2025-10-13 10:41:58 +02:00
}
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
// Optionally: expose current progress for all active operations
public float GetAggregateLoadProgress ( )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
if ( _activeLoads . Count = = 0 ) return 1f ;
float sum = 0f ;
foreach ( var op in _activeLoads . Values ) sum + = op . progress ;
return sum / _activeLoads . Count ;
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
public float GetAggregateUnloadProgress ( )
2025-09-08 14:25:50 +02:00
{
2025-10-13 10:41:58 +02:00
if ( _activeUnloads . Count = = 0 ) return 1f ;
float sum = 0f ;
foreach ( var op in _activeUnloads . Values ) sum + = op . progress ;
return sum / _activeUnloads . Count ;
2025-09-08 14:25:50 +02:00
}
2025-10-13 10:41:58 +02:00
// Tracks the currently loaded gameplay scene (not persistent/bootstrapper)
2025-10-17 14:03:27 +02:00
public string CurrentGameplayScene { get ; set ; } = "AppleHillsOverworld" ;
2025-10-13 10:41:58 +02:00
2025-11-07 15:38:31 +00:00
public async Task ReloadCurrentScene ( IProgress < float > progress = null , bool autoHideLoadingScreen = true , bool skipSave = false )
2025-10-14 15:53:58 +02:00
{
2025-11-07 15:38:31 +00:00
await SwitchSceneAsync ( CurrentGameplayScene , progress , autoHideLoadingScreen , skipSave ) ;
2025-10-14 15:53:58 +02:00
}
2025-10-16 19:43:19 +02:00
/// <summary>
/// Switches from current gameplay scene to a new one
/// </summary>
/// <param name="newSceneName">Name of the scene to load</param>
/// <param name="progress">Optional progress reporter</param>
/// <param name="autoHideLoadingScreen">Whether to automatically hide the loading screen when complete. If false, caller must hide it manually.</param>
2025-11-07 15:38:31 +00:00
/// <param name="skipSave">If true, skips saving scene data during transition. Useful for level restart to prevent re-saving cleared data.</param>
public async Task SwitchSceneAsync ( string newSceneName , IProgress < float > progress = null , bool autoHideLoadingScreen = true , bool skipSave = false )
2025-09-01 22:51:52 +02:00
{
2025-11-07 15:38:31 +00:00
string oldSceneName = CurrentGameplayScene ;
// PHASE 1: Show loading screen at the start
// Use explicit progress provider to combine unload + load progress
if ( _loadingScreen ! = null )
2025-10-16 19:43:19 +02:00
{
2025-11-07 15:38:31 +00:00
_loadingScreen . ShowLoadingScreen ( ( ) = > GetAggregateLoadProgress ( ) ) ;
2025-10-16 19:43:19 +02:00
}
2025-11-07 15:38:31 +00:00
// PHASE 2: Broadcast scene unloading - notify components to cleanup
LogDebugMessage ( $"Broadcasting OnSceneUnloading for: {oldSceneName}" ) ;
LifecycleManager . Instance ? . BroadcastSceneUnloading ( oldSceneName ) ;
// PHASE 3: Save scene-specific data via SaveLoadManager (unless skipSave is true)
if ( ! skipSave & & SaveLoadManager . Instance ! = null )
{
var debugSettings = DeveloperSettingsProvider . Instance . GetSettings < DebugSettings > ( ) ;
if ( debugSettings . useSaveLoadSystem )
{
LogDebugMessage ( $"Saving scene data for: {oldSceneName}" ) ;
SaveLoadManager . Instance . SaveSceneData ( ) ;
}
}
else if ( skipSave )
{
LogDebugMessage ( $"Skipping save for: {oldSceneName} (skipSave=true)" ) ;
}
// PHASE 4: Clear PuzzleManager state before scene transition
if ( PuzzleS . PuzzleManager . Instance ! = null )
{
LogDebugMessage ( $"Clearing puzzle state before scene transition" ) ;
PuzzleS . PuzzleManager . Instance . ClearPuzzleState ( ) ;
}
// PHASE 5: Remove all AstarPath (A* Pathfinder) singletons before loading the new scene
2025-10-13 10:41:58 +02:00
var astarPaths = FindObjectsByType < AstarPath > ( FindObjectsSortMode . None ) ;
foreach ( var astar in astarPaths )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
if ( Application . isPlaying )
Destroy ( astar . gameObject ) ;
else
DestroyImmediate ( astar . gameObject ) ;
2025-09-01 22:51:52 +02:00
}
2025-11-07 15:38:31 +00:00
// PHASE 6: Unload previous gameplay scene (Unity will call OnDestroy → OnManagedDestroy)
if ( ! string . IsNullOrEmpty ( oldSceneName ) & & oldSceneName ! = BootstrapSceneName )
2025-09-01 22:51:52 +02:00
{
2025-11-07 15:38:31 +00:00
var prevScene = SceneManager . GetSceneByName ( oldSceneName ) ;
2025-10-13 10:41:58 +02:00
if ( prevScene . isLoaded )
{
2025-11-07 15:38:31 +00:00
await UnloadSceneAsync ( oldSceneName ) ;
2025-10-13 10:41:58 +02:00
}
else
{
2025-11-07 15:38:31 +00:00
Logging . Warning ( $"[SceneManagerService] Previous scene '{oldSceneName}' is not loaded, skipping unload." ) ;
2025-10-13 10:41:58 +02:00
}
2025-09-01 22:51:52 +02:00
}
2025-11-07 15:38:31 +00:00
// PHASE 7: Ensure BootstrapScene is loaded before loading new scene
2025-10-13 10:41:58 +02:00
var bootstrap = SceneManager . GetSceneByName ( BootstrapSceneName ) ;
if ( ! bootstrap . isLoaded )
{
SceneManager . LoadScene ( BootstrapSceneName , LoadSceneMode . Additive ) ;
}
2025-11-07 15:38:31 +00:00
// PHASE 8: Begin scene loading mode - enables priority-ordered component initialization
LogDebugMessage ( $"Beginning scene load for: {newSceneName}" ) ;
LifecycleManager . Instance ? . BeginSceneLoad ( newSceneName ) ;
// PHASE 9: Load new gameplay scene
2025-10-13 10:41:58 +02:00
await LoadSceneAsync ( newSceneName , progress ) ;
CurrentGameplayScene = newSceneName ;
2025-10-16 19:43:19 +02:00
2025-11-07 15:38:31 +00:00
// PHASE 10: Broadcast scene ready - processes batched components in priority order, then calls OnSceneReady
LogDebugMessage ( $"Broadcasting OnSceneReady for: {newSceneName}" ) ;
LifecycleManager . Instance ? . BroadcastSceneReady ( newSceneName ) ;
// PHASE 11: Restore scene-specific data via SaveLoadManager
if ( ! skipSave & & SaveLoadManager . Instance ! = null )
{
var debugSettings = DeveloperSettingsProvider . Instance . GetSettings < DebugSettings > ( ) ;
if ( debugSettings . useSaveLoadSystem )
{
LogDebugMessage ( $"Restoring scene data for: {newSceneName}" ) ;
SaveLoadManager . Instance . RestoreSceneData ( ) ;
}
}
else if ( skipSave )
{
2025-11-10 14:11:02 +01:00
SaveLoadManager . Instance . RestoreSceneData ( ) ;
2025-11-07 15:38:31 +00:00
LogDebugMessage ( $"Skipping restore for: {newSceneName} (skipSave=true)" ) ;
}
// PHASE 12: Only hide the loading screen if autoHideLoadingScreen is true
2025-10-16 19:43:19 +02:00
if ( autoHideLoadingScreen & & _loadingScreen ! = null )
{
_loadingScreen . HideLoadingScreen ( ) ;
}
2025-09-01 22:51:52 +02:00
}
2025-10-28 14:31:17 +01:00
private void LogDebugMessage ( string message )
{
if ( _logVerbosity < = LogVerbosity . Debug )
{
Logging . Debug ( $"[SceneManagerService] {message}" ) ;
}
}
2025-09-01 22:51:52 +02:00
}
}