// 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);
}
}
}