2025-09-01 22:51:52 +02:00
using System ;
using System.Collections.Generic ;
using System.Threading.Tasks ;
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-16 19:43:19 +02:00
using Bootstrap ;
2025-09-01 22:51:52 +02:00
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-10-13 10:41:58 +02:00
public class SceneManagerService : MonoBehaviour
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 ;
private static bool _isQuitting = false ;
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
public event Action < string > SceneLoadStarted ;
public event Action < string , float > SceneLoadProgress ;
public event Action < string > SceneLoadCompleted ;
public event Action < string > SceneUnloadStarted ;
public event Action < string , float > SceneUnloadProgress ;
public event Action < string > SceneUnloadCompleted ;
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 ( ) ;
private const string BootstrapSceneName = "BootstrapScene" ;
2025-09-01 22:51:52 +02:00
2025-10-16 19:43:19 +02:00
void Awake ( )
2025-10-13 14:25:11 +02:00
{
2025-10-16 19:43:19 +02:00
_instance = this ;
// DontDestroyOnLoad(gameObject);
2025-10-13 14:25:11 +02:00
2025-10-16 19:43:19 +02:00
// Initialize current scene tracking immediately in Awake
InitializeCurrentSceneTracking ( ) ;
// Register for post-boot initialization
BootCompletionService . RegisterInitAction ( InitializePostBoot ) ;
// 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-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-16 19:43:19 +02:00
Logging . Debug ( $"[SceneManagerService] Initialized with current scene: {CurrentGameplayScene}" ) ;
}
// Otherwise default to MainMenu
else
{
2025-10-17 14:03:27 +02:00
CurrentGameplayScene = "AppleHillsOverworld" ;
2025-10-16 19:43:19 +02:00
Logging . Debug ( $"[SceneManagerService] 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-16 19:43:19 +02:00
Logging . Debug ( $"[SceneManagerService] 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-16 19:43:19 +02:00
private void InitializePostBoot ( )
{
// Set up loading screen reference and events after boot is complete
_loadingScreen = LoadingScreenController . Instance ;
// Set up loading screen event handlers if available
SetupLoadingScreenEvents ( ) ;
Logging . Debug ( $"[SceneManagerService] Post-boot initialization complete, current scene is: {CurrentGameplayScene}" ) ;
}
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
void OnApplicationQuit ( )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
_isQuitting = true ;
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 ) ;
SceneLoadProgress ? . Invoke ( sceneName , 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-14 15:53:58 +02:00
Logging . Warning ( $"SceneManagerService: Attempted to unload scene '{sceneName}', but it is not loaded." ) ;
2025-10-13 10:41:58 +02:00
return ;
}
SceneUnloadStarted ? . Invoke ( sceneName ) ;
var op = SceneManager . UnloadSceneAsync ( sceneName ) ;
_activeUnloads [ sceneName ] = op ;
while ( ! op . isDone )
{
progress ? . Report ( op . progress ) ;
SceneUnloadProgress ? . Invoke ( sceneName , op . progress ) ;
await Task . Yield ( ) ;
}
_activeUnloads . Remove ( sceneName ) ;
SceneUnloadCompleted ? . Invoke ( 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 ) ;
SceneUnloadStarted ? . Invoke ( name ) ;
}
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 ) ;
SceneUnloadCompleted ? . Invoke ( 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-10-16 19:43:19 +02:00
public async Task ReloadCurrentScene ( IProgress < float > progress = null , bool autoHideLoadingScreen = true )
2025-10-14 15:53:58 +02:00
{
2025-10-16 19:43:19 +02:00
await SwitchSceneAsync ( CurrentGameplayScene , progress , autoHideLoadingScreen ) ;
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>
public async Task SwitchSceneAsync ( string newSceneName , IProgress < float > progress = null , bool autoHideLoadingScreen = true )
2025-09-01 22:51:52 +02:00
{
2025-10-16 19:43:19 +02:00
// Show loading screen at the start (whether using auto-hide or not)
if ( _loadingScreen ! = null & & ! _loadingScreen . IsActive )
{
_loadingScreen . ShowLoadingScreen ( ) ;
}
2025-10-13 10:41:58 +02:00
// Remove all AstarPath (A* Pathfinder) singletons before loading the new scene
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-10-13 10:41:58 +02:00
// Unload previous gameplay scene (if not BootstrapScene and not same as new)
2025-10-14 15:53:58 +02:00
if ( ! string . IsNullOrEmpty ( CurrentGameplayScene ) & & CurrentGameplayScene ! = BootstrapSceneName )
2025-09-01 22:51:52 +02:00
{
2025-10-13 10:41:58 +02:00
var prevScene = SceneManager . GetSceneByName ( CurrentGameplayScene ) ;
if ( prevScene . isLoaded )
{
await UnloadSceneAsync ( CurrentGameplayScene ) ;
}
else
{
2025-10-14 15:53:58 +02:00
Logging . Warning ( $"SceneManagerService: Previous scene '{CurrentGameplayScene}' is not loaded, skipping unload." ) ;
2025-10-13 10:41:58 +02:00
}
2025-09-01 22:51:52 +02:00
}
2025-10-13 10:41:58 +02:00
// Ensure BootstrapScene is loaded before loading new scene
var bootstrap = SceneManager . GetSceneByName ( BootstrapSceneName ) ;
if ( ! bootstrap . isLoaded )
{
SceneManager . LoadScene ( BootstrapSceneName , LoadSceneMode . Additive ) ;
}
// Load new gameplay scene
await LoadSceneAsync ( newSceneName , progress ) ;
// Update tracker
CurrentGameplayScene = newSceneName ;
2025-10-16 19:43:19 +02:00
// Only hide the loading screen if autoHideLoadingScreen is true
if ( autoHideLoadingScreen & & _loadingScreen ! = null )
{
_loadingScreen . HideLoadingScreen ( ) ;
}
2025-09-01 22:51:52 +02:00
}
}
}