// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Helper { using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Extension; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; using Utils; using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif /// /// Utilities for scene discovery, loading, and object retrieval in editor and runtime. /// public static class SceneHelper { /// /// Returns true if a scene with the given name or path is currently loaded. /// public static bool IsSceneLoaded(string sceneNameOrPath) { for (int i = 0; i < SceneManager.sceneCount; ++i) { Scene scene = SceneManager.GetSceneAt(i); if ( string.Equals(scene.name, sceneNameOrPath, StringComparison.Ordinal) || string.Equals(scene.path, sceneNameOrPath, StringComparison.Ordinal) ) { return true; } } return false; } /// /// Finds all scene asset paths under the specified search folders (Editor only). /// public static string[] GetAllScenePaths(string[] searchFolders = null) { #if UNITY_EDITOR searchFolders ??= Array.Empty(); string[] guids = AssetDatabase.FindAssets("t:Scene", searchFolders); if (guids.Length == 0) { return Array.Empty(); } using PooledResource> lease = Buffers.GetList( guids.Length, out List paths ); for (int i = 0; i < guids.Length; i++) { paths.Add(AssetDatabase.GUIDToAssetPath(guids[i])); } return paths.ToArray(); #else return Array.Empty(); #endif } /// /// Returns all enabled scenes included in Build Settings (Editor only). /// public static string[] GetScenesInBuild() { #if UNITY_EDITOR EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes; if (scenes.Length == 0) { return Array.Empty(); } using PooledResource> lease = Buffers.GetList( scenes.Length, out List paths ); for (int i = 0; i < scenes.Length; i++) { EditorBuildSettingsScene scene = scenes[i]; if (scene.enabled) { paths.Add(scene.path); } } return paths.ToArray(); #else return Array.Empty(); #endif } /// /// Loads a scene additively if needed and returns the first object of type along with a disposal callback to unload the scene. /// public static async ValueTask> GetObjectOfTypeInScene( string scenePath ) where T : Object { if (!SceneAssetExists(scenePath)) { return new DeferredDisposalResult(default, () => new ValueTask()); } DeferredDisposalResult result = await GetAllObjectsOfTypeInScene(scenePath); T value = result.result.Length == 0 ? default : result.result[0]; return new DeferredDisposalResult(value, result.DisposeAsync); } /// /// Loads a scene additively if needed and returns all objects of type along with a disposal callback to unload the scene. /// public static async ValueTask> GetAllObjectsOfTypeInScene( string scenePath ) where T : Object { if (!SceneAssetExists(scenePath)) { return new DeferredDisposalResult(Array.Empty(), () => new ValueTask()); } // Ensure singleton is created _ = UnityMainThreadDispatcher.Instance; TaskCompletionSource taskCompletionSource = new(); SceneLoadScope sceneScope = new(scenePath, OnSceneLoaded); T[] result = await taskCompletionSource.Task; return new DeferredDisposalResult( result, async () => { if ( !UnityMainThreadDispatcher.TryGetInstance( out UnityMainThreadDispatcher dispatcher ) ) { await sceneScope.DisposeAsync(); return; } TaskCompletionSource disposalComplete = new(); dispatcher.RunOnMainThread(() => _ = sceneScope .DisposeAsync() .WithContinuation(() => disposalComplete.SetResult(true)) ); await disposalComplete.Task; } ); void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (!string.Equals(scene.path, scenePath, StringComparison.Ordinal)) { return; } T[] allObjects = Object.FindObjectsByType( FindObjectsInactive.Include, FindObjectsSortMode.None ); if (allObjects.Length == 0) { taskCompletionSource.SetResult(Array.Empty()); return; } using PooledResource> lease = Buffers.GetList( allObjects.Length, out List filtered ); for (int i = 0; i < allObjects.Length; i++) { T obj = allObjects[i]; GameObject go = obj.GetGameObject(); if (go != null && go.scene == scene) { filtered.Add(obj); } } T[] foundObjects = filtered.ToArray(); taskCompletionSource.SetResult(foundObjects); } } /// /// A helper scope that ensures a target scene is loaded and provides an async disposal to unload it. /// public sealed class SceneLoadScope { private #if UNITY_EDITOR readonly #endif Scene? _openedScene; private readonly UnityAction _onSceneLoaded; private readonly bool _eventAdded; /// /// Creates the scope and ensures the target scene is loaded. If the active scene already matches, no loading occurs. /// public SceneLoadScope(string scenePath, UnityAction onSceneLoaded) { _onSceneLoaded = onSceneLoaded; _eventAdded = false; Scene activeScene = SceneManager.GetActiveScene(); if ( !activeScene.IsValid() || !activeScene.isLoaded || !string.Equals(activeScene.path, scenePath, StringComparison.Ordinal) ) { #if UNITY_EDITOR if (Application.isPlaying) { SceneManager.sceneLoaded += onSceneLoaded; _eventAdded = true; _openedScene = EditorSceneManager.LoadSceneInPlayMode( scenePath, new LoadSceneParameters(LoadSceneMode.Additive, LocalPhysicsMode.None) ); } else { _openedScene = EditorSceneManager.OpenScene( scenePath, OpenSceneMode.Additive ); onSceneLoaded?.Invoke(_openedScene.Value, LoadSceneMode.Additive); } #else SceneManager.sceneLoaded += onSceneLoaded; _eventAdded = true; SceneManager.sceneLoaded += LocalSceneLoaded; SceneManager.LoadScene(scenePath, LoadSceneMode.Additive); _openedScene = SceneManager.GetSceneByPath(scenePath); void LocalSceneLoaded(Scene scene, LoadSceneMode mode) { if (!string.Equals(scene.path, scenePath, StringComparison.Ordinal)) { return; } _openedScene = scene; SceneManager.sceneLoaded -= LocalSceneLoaded; } #endif } else { onSceneLoaded?.Invoke(activeScene, LoadSceneMode.Single); _openedScene = null; } } /// /// Unloads the scene if it was opened by this scope; otherwise no-ops. /// public async ValueTask DisposeAsync() { if (_eventAdded) { SceneManager.sceneLoaded -= _onSceneLoaded; } if (_openedScene == null) { return; } Scene openedScene = _openedScene.Value; if (!openedScene.IsValid()) { return; } if (!openedScene.isLoaded) { return; } #if UNITY_EDITOR if (Application.isPlaying) { await SceneManager.UnloadSceneAsync(openedScene, UnloadSceneOptions.None); } else { EditorSceneManager.CloseScene(openedScene, true); } #else await SceneManager.UnloadSceneAsync(openedScene, UnloadSceneOptions.None); #endif } } private static bool SceneAssetExists(string scenePath) { if (string.IsNullOrWhiteSpace(scenePath)) { return false; } string normalized = scenePath.SanitizePath(); if (Path.IsPathRooted(normalized)) { return File.Exists(normalized); } string projectRoot = Path.GetDirectoryName(Application.dataPath); if (string.IsNullOrEmpty(projectRoot)) { return false; } string absolutePath = Path.Combine(projectRoot, normalized); return File.Exists(absolutePath); } } }