using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; /// /// Singleton service for loading and unloading Unity scenes asynchronously, with events for progress and completion. /// public class SceneManagerService : MonoBehaviour { private static SceneManagerService _instance; private static bool _isQuitting = false; /// /// Singleton instance of the SceneManagerService. /// public static SceneManagerService Instance { get { if (_instance == null && Application.isPlaying && !_isQuitting) { _instance = FindAnyObjectByType(); if (_instance == null) { var go = new GameObject("SceneManagerService"); _instance = go.AddComponent(); // DontDestroyOnLoad(go); } } return _instance; } } // Events for scene lifecycle public event Action SceneLoadStarted; public event Action SceneLoadProgress; public event Action SceneLoadCompleted; public event Action SceneUnloadStarted; public event Action SceneUnloadProgress; public event Action SceneUnloadCompleted; private readonly Dictionary _activeLoads = new(); private readonly Dictionary _activeUnloads = new(); private const string BootstrapSceneName = "BootstrapScene"; void Awake() { _instance = this; // DontDestroyOnLoad(gameObject); #if UNITY_EDITOR // In Editor, set CurrentGameplayScene to the currently open scene at play start if (Application.isPlaying) { var activeScene = SceneManager.GetActiveScene(); if (activeScene.IsValid()) { CurrentGameplayScene = activeScene.name; } } #endif // Ensure BootstrapScene is loaded at startup var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName); if (!bootstrap.isLoaded) { SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive); } } void OnApplicationQuit() { _isQuitting = true; } /// /// Load a single scene asynchronously (additive). /// /// Name of the scene to load. /// Optional progress reporter. public async Task LoadSceneAsync(string sceneName, IProgress progress = null) { SceneLoadStarted?.Invoke(sceneName); var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); _activeLoads[sceneName] = op; while (!op.isDone) { progress?.Report(op.progress); SceneLoadProgress?.Invoke(sceneName, op.progress); await Task.Yield(); } _activeLoads.Remove(sceneName); SceneLoadCompleted?.Invoke(sceneName); } /// /// Unload a single scene asynchronously. /// /// Name of the scene to unload. /// Optional progress reporter. public async Task UnloadSceneAsync(string sceneName, IProgress progress = null) { var scene = SceneManager.GetSceneByName(sceneName); if (!scene.isLoaded) { Debug.LogWarning($"SceneManagerService: Attempted to unload scene '{sceneName}', but it is not loaded."); 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); } /// /// Load multiple scenes asynchronously. /// /// Enumerable of scene names to load. /// Optional progress reporter. public async Task LoadScenesAsync(IEnumerable sceneNames, IProgress progress = null) { int total = 0; int done = 0; var ops = new List(); 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); // Optionally, could invoke SceneLoadProgress for each scene await Task.Yield(); } foreach (var name in sceneNames) { _activeLoads.Remove(name); SceneLoadCompleted?.Invoke(name); } } /// /// Unload multiple scenes asynchronously. /// /// Enumerable of scene names to unload. /// Optional progress reporter. public async Task UnloadScenesAsync(IEnumerable sceneNames, IProgress progress = null) { int total = 0; int done = 0; var ops = new List(); foreach (var name in sceneNames) { 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); foreach (var name in sceneNames) SceneUnloadProgress?.Invoke(name, avg); await Task.Yield(); } foreach (var name in sceneNames) { _activeUnloads.Remove(name); SceneUnloadCompleted?.Invoke(name); } } // 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; private set; } = "MainMenu"; // Switches from current gameplay scene to a new one public async Task SwitchSceneAsync(string newSceneName, IProgress progress = null) { // Remove all AstarPath (A* Pathfinder) singletons before loading the new scene var astarPaths = FindObjectsByType(FindObjectsSortMode.None); foreach (var astar in astarPaths) { if (Application.isPlaying) Destroy(astar.gameObject); else DestroyImmediate(astar.gameObject); } // Unload previous gameplay scene (if not BootstrapScene and not same as new) if (!string.IsNullOrEmpty(CurrentGameplayScene) && CurrentGameplayScene != newSceneName && CurrentGameplayScene != BootstrapSceneName) { var prevScene = SceneManager.GetSceneByName(CurrentGameplayScene); if (prevScene.isLoaded) { await UnloadSceneAsync(CurrentGameplayScene); } else { Debug.LogWarning($"SceneManagerService: Previous scene '{CurrentGameplayScene}' is not loaded, skipping unload."); } } // 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; } }