408 lines
16 KiB
C#
408 lines
16 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Threading.Tasks;
|
||
using AppleHills.Core.Settings;
|
||
using Core.Lifecycle;
|
||
using Core.SaveLoad;
|
||
using UI;
|
||
using UnityEngine;
|
||
using UnityEngine.SceneManagement;
|
||
|
||
namespace Core
|
||
{
|
||
/// <summary>
|
||
/// Singleton service for loading and unloading Unity scenes asynchronously, with events for progress and completion.
|
||
/// </summary>
|
||
public class SceneManagerService : ManagedBehaviour
|
||
{
|
||
private LoadingScreenController _loadingScreen;
|
||
private static SceneManagerService _instance;
|
||
|
||
/// <summary>
|
||
/// Singleton instance of the SceneManagerService. No longer creates an instance if one doesn't exist.
|
||
/// </summary>
|
||
public static SceneManagerService Instance => _instance;
|
||
|
||
// Events for scene lifecycle
|
||
// 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>
|
||
public event Action<string> SceneLoadStarted;
|
||
|
||
/// <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>
|
||
public event Action<string> SceneLoadCompleted;
|
||
|
||
private readonly Dictionary<string, AsyncOperation> _activeLoads = new();
|
||
private readonly Dictionary<string, AsyncOperation> _activeUnloads = new();
|
||
private LogVerbosity _logVerbosity = LogVerbosity.Debug;
|
||
private const string BootstrapSceneName = "BootstrapScene";
|
||
|
||
// ManagedBehaviour configuration
|
||
public override int ManagedAwakePriority => 15; // Core infrastructure, after GameManager
|
||
|
||
protected override void OnManagedAwake()
|
||
{
|
||
// Set instance immediately (early initialization)
|
||
_instance = this;
|
||
|
||
// Initialize current scene tracking - critical for scene management
|
||
InitializeCurrentSceneTracking();
|
||
|
||
// Ensure BootstrapScene is loaded at startup
|
||
var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName);
|
||
if (!bootstrap.isLoaded)
|
||
{
|
||
SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive);
|
||
}
|
||
}
|
||
|
||
protected override void OnManagedStart()
|
||
{
|
||
// Set up loading screen reference and events
|
||
// This must happen in ManagedStart because LoadingScreenController instance needs to be set first
|
||
_loadingScreen = LoadingScreenController.Instance;
|
||
SetupLoadingScreenEvents();
|
||
|
||
// Load verbosity settings
|
||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
|
||
|
||
LogDebugMessage($"SceneManagerService initialized, current scene is: {CurrentGameplayScene}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Initialize current scene tracking immediately in Awake
|
||
/// This ensures scene management works correctly regardless of boot timing
|
||
/// </summary>
|
||
private void InitializeCurrentSceneTracking()
|
||
{
|
||
// Get the active scene and use it as the current gameplay scene
|
||
Scene activeScene = SceneManager.GetActiveScene();
|
||
|
||
if (activeScene.IsValid())
|
||
{
|
||
// If this is the MainMenu or another gameplay scene, track it
|
||
if (activeScene.name != BootstrapSceneName)
|
||
{
|
||
CurrentGameplayScene = activeScene.name;
|
||
LogDebugMessage($"Initialized with current scene: {CurrentGameplayScene}");
|
||
}
|
||
// Otherwise default to MainMenu
|
||
else
|
||
{
|
||
CurrentGameplayScene = "AppleHillsOverworld";
|
||
LogDebugMessage($"Initialized with default scene: {CurrentGameplayScene}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
CurrentGameplayScene = "AppleHillsOverworld";
|
||
LogDebugMessage($"No valid active scene, defaulting to: {CurrentGameplayScene}");
|
||
}
|
||
}
|
||
|
||
private void SetupLoadingScreenEvents()
|
||
{
|
||
if (_loadingScreen == null) return;
|
||
|
||
SceneLoadStarted += _ => _loadingScreen.ShowLoadingScreen(() => GetAggregateLoadProgress());
|
||
SceneLoadCompleted += _ => _loadingScreen.HideLoadingScreen();
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
SceneLoadStarted?.Invoke(sceneName);
|
||
var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
|
||
_activeLoads[sceneName] = op;
|
||
while (!op.isDone)
|
||
{
|
||
progress?.Report(op.progress);
|
||
await Task.Yield();
|
||
}
|
||
_activeLoads.Remove(sceneName);
|
||
SceneLoadCompleted?.Invoke(sceneName);
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
var scene = SceneManager.GetSceneByName(sceneName);
|
||
if (!scene.isLoaded)
|
||
{
|
||
Logging.Warning($"[SceneManagerService] Attempted to unload scene '{sceneName}', but it is not loaded.");
|
||
return;
|
||
}
|
||
|
||
var op = SceneManager.UnloadSceneAsync(sceneName);
|
||
_activeUnloads[sceneName] = op;
|
||
while (!op.isDone)
|
||
{
|
||
progress?.Report(op.progress);
|
||
await Task.Yield();
|
||
}
|
||
_activeUnloads.Remove(sceneName);
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
// Show loading screen at the start of multiple scene loading
|
||
if (_loadingScreen != null)
|
||
{
|
||
_loadingScreen.ShowLoadingScreen();
|
||
}
|
||
|
||
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
|
||
if (_loadingScreen != null)
|
||
{
|
||
_loadingScreen.HideLoadingScreen();
|
||
}
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
// Show loading screen at the start of multiple scene unloading
|
||
if (_loadingScreen != null)
|
||
{
|
||
_loadingScreen.ShowLoadingScreen();
|
||
}
|
||
|
||
int total = 0;
|
||
int done = 0;
|
||
var ops = new List<AsyncOperation>();
|
||
foreach (var name in sceneNames)
|
||
{
|
||
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
|
||
if (_loadingScreen != null)
|
||
{
|
||
_loadingScreen.HideLoadingScreen();
|
||
}
|
||
}
|
||
|
||
// Optionally: expose current progress for all active operations
|
||
public float GetAggregateLoadProgress()
|
||
{
|
||
if (_activeLoads.Count == 0) return 1f;
|
||
float sum = 0f;
|
||
foreach (var op in _activeLoads.Values) sum += op.progress;
|
||
return sum / _activeLoads.Count;
|
||
}
|
||
public float GetAggregateUnloadProgress()
|
||
{
|
||
if (_activeUnloads.Count == 0) return 1f;
|
||
float sum = 0f;
|
||
foreach (var op in _activeUnloads.Values) sum += op.progress;
|
||
return sum / _activeUnloads.Count;
|
||
}
|
||
|
||
// Tracks the currently loaded gameplay scene (not persistent/bootstrapper)
|
||
public string CurrentGameplayScene { get; set; } = "AppleHillsOverworld";
|
||
|
||
public async Task ReloadCurrentScene(IProgress<float> progress = null, bool autoHideLoadingScreen = true, bool skipSave = false)
|
||
{
|
||
await SwitchSceneAsync(CurrentGameplayScene, progress, autoHideLoadingScreen, skipSave);
|
||
}
|
||
|
||
/// <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>
|
||
/// <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)
|
||
{
|
||
string oldSceneName = CurrentGameplayScene;
|
||
|
||
// PHASE 1: Show loading screen at the start
|
||
// Use explicit progress provider to combine unload + load progress
|
||
if (_loadingScreen != null)
|
||
{
|
||
_loadingScreen.ShowLoadingScreen(() => GetAggregateLoadProgress());
|
||
}
|
||
|
||
// 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
|
||
var astarPaths = FindObjectsByType<AstarPath>(FindObjectsSortMode.None);
|
||
foreach (var astar in astarPaths)
|
||
{
|
||
if (Application.isPlaying)
|
||
Destroy(astar.gameObject);
|
||
else
|
||
DestroyImmediate(astar.gameObject);
|
||
}
|
||
|
||
// PHASE 6: Unload previous gameplay scene (Unity will call OnDestroy → OnManagedDestroy)
|
||
if (!string.IsNullOrEmpty(oldSceneName) && oldSceneName != BootstrapSceneName)
|
||
{
|
||
var prevScene = SceneManager.GetSceneByName(oldSceneName);
|
||
if (prevScene.isLoaded)
|
||
{
|
||
await UnloadSceneAsync(oldSceneName);
|
||
}
|
||
else
|
||
{
|
||
Logging.Warning($"[SceneManagerService] Previous scene '{oldSceneName}' is not loaded, skipping unload.");
|
||
}
|
||
}
|
||
|
||
// PHASE 7: Ensure BootstrapScene is loaded before loading new scene
|
||
var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName);
|
||
if (!bootstrap.isLoaded)
|
||
{
|
||
SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive);
|
||
}
|
||
|
||
// 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
|
||
await LoadSceneAsync(newSceneName, progress);
|
||
CurrentGameplayScene = newSceneName;
|
||
|
||
// 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)
|
||
{
|
||
SaveLoadManager.Instance.RestoreSceneData();
|
||
LogDebugMessage($"Skipping restore for: {newSceneName} (skipSave=true)");
|
||
}
|
||
|
||
// PHASE 12: Only hide the loading screen if autoHideLoadingScreen is true
|
||
if (autoHideLoadingScreen && _loadingScreen != null)
|
||
{
|
||
_loadingScreen.HideLoadingScreen();
|
||
}
|
||
}
|
||
|
||
private void LogDebugMessage(string message)
|
||
{
|
||
if (_logVerbosity <= LogVerbosity.Debug)
|
||
{
|
||
Logging.Debug($"[SceneManagerService] {message}");
|
||
}
|
||
}
|
||
}
|
||
}
|