From db10eef525c0905c1f83dea768db05cac6f87a95 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Mon, 29 Jun 2026 12:17:25 +0200 Subject: [PATCH] Add Play Scene Tool --- Assets/DefaultParentForEditor.cs | 45 ++++ Assets/DefaultParentForEditor.cs.meta | 11 + Assets/Editor.meta | 8 + Assets/Editor/PlaySceneWindow.cs | 354 ++++++++++++++++++++++++++ Assets/Editor/PlaySceneWindow.cs.meta | 11 + Assets/Editor/SceneCollection.cs | 42 +++ Assets/Editor/SceneCollection.cs.meta | 2 + 7 files changed, 473 insertions(+) create mode 100644 Assets/DefaultParentForEditor.cs create mode 100644 Assets/DefaultParentForEditor.cs.meta create mode 100644 Assets/Editor.meta create mode 100644 Assets/Editor/PlaySceneWindow.cs create mode 100644 Assets/Editor/PlaySceneWindow.cs.meta create mode 100644 Assets/Editor/SceneCollection.cs create mode 100644 Assets/Editor/SceneCollection.cs.meta diff --git a/Assets/DefaultParentForEditor.cs b/Assets/DefaultParentForEditor.cs new file mode 100644 index 0000000..6678f6e --- /dev/null +++ b/Assets/DefaultParentForEditor.cs @@ -0,0 +1,45 @@ +#if UNITY_EDITOR +using UnityEditor; +#endif + +using UnityEngine; + +namespace Chameleon +{ + [ExecuteAlways] + [AddComponentMenu("Tools/Chameleon/Default Parent (Editor)")] + public class DefaultParentForEditor : MonoBehaviour + { +#if UNITY_EDITOR + static DefaultParentForEditor selection; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + static void Init() => selection = null; + + void Update() + { + if (Application.isPlaying) + return; + + var activeGameObject = Selection.activeGameObject; + if (activeGameObject != gameObject) + return; + + if (!activeGameObject.TryGetComponent(out DefaultParentForEditor defaultParent)) + return; + + if (selection && selection != defaultParent) + { + selection = null; + EditorUtility.ClearDefaultParentObject(); + } + + if (!selection) + { + selection = this; + EditorUtility.SetDefaultParentObject(gameObject); + } + } +#endif + } +} diff --git a/Assets/DefaultParentForEditor.cs.meta b/Assets/DefaultParentForEditor.cs.meta new file mode 100644 index 0000000..15eab20 --- /dev/null +++ b/Assets/DefaultParentForEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00d9c432a5ead1540b39d22553c41f76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: a360cdd2cf64e48469885fdcb0737dd1, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 0000000..a48392c --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dde47c6dc3068974989b83b6a1aad5bb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/PlaySceneWindow.cs b/Assets/Editor/PlaySceneWindow.cs new file mode 100644 index 0000000..00995b0 --- /dev/null +++ b/Assets/Editor/PlaySceneWindow.cs @@ -0,0 +1,354 @@ +using UnityEditor; +using UnityEditor.SceneManagement; + +using UnityEngine; +using UnityEngine.SceneManagement; + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using F = System.Reflection.BindingFlags; + +namespace Chameleon.Editor +{ + [ExecuteAlways] + public class PlaySceneWindow : EditorWindow + { + [MenuItem("Tools/Chameleon/Play Scene/Open Window")] + public static void Open() + => _ = GetWindow("Play Scene"); + + [SerializeField] bool enabled = true; + + [SerializeField] string lastActiveScene; + [SerializeField] List lastScenePaths = new(); + + [SerializeField] string scenePath; + [SerializeField] int sceneIndex; + + static List<(string path, SceneCollection collection)> sceneCollections; + static string[] scenePaths; + static string[] sceneNames; + + static string activeScenePath + => SceneManager.GetActiveScene().path; + + void OnEnable() + { + OnDisable(); + + RefreshSceneCollections(); + + scenePaths = EditorBuildSettings.scenes.Select(x => x.path).ToArray(); + sceneNames = scenePaths.Select(Path.GetFileNameWithoutExtension).ToArray(); + + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + } + + void RefreshSceneCollections() + { + sceneCollections = AssetDatabase.FindAssets($"t:{nameof(SceneCollection)}") + .Select(AssetDatabase.GUIDToAssetPath) + .Select(AssetDatabase.LoadAssetAtPath) + .Where(x => x) + .Select(x => ( + string.Join(";", x.sceneAssets.Select(AssetDatabase.GetAssetPath)), + x + )) + .ToList(); + } + + void OnDisable() + { + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + } + + void OnPlayModeStateChanged(PlayModeStateChange state) + { + if (!enabled) + return; + + if (state != PlayModeStateChange.ExitingEditMode) + return; + + if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) + EditorApplication.ExitPlaymode(); + + lastActiveScene = SceneManager.GetActiveScene().path; + lastScenePaths = Enumerable + .Range(0, SceneManager.sceneCount) + .Select(i => SceneManager.GetSceneAt(i).path) + .ToList(); + + scenePath = scenePaths[sceneIndex]; + EditorSceneManager.OpenScene(scenePath); + } + + void Update() + { + if (!enabled) + return; + + if (EditorApplication.isPlaying) + return; + + if (lastScenePaths.Count == 0) + return; + + if (scenePath is not null) + return; + + var scene = lastScenePaths.First(); + if (scene == string.Empty) + return; + + EditorSceneManager.OpenScene(scene); + foreach (var scenePath in lastScenePaths.Skip(1)) + EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); + + if (!string.IsNullOrEmpty(lastActiveScene)) + { + var activeScene = SceneManager.GetSceneByPath(lastActiveScene); + if (activeScene.IsValid()) + SceneManager.SetActiveScene(activeScene); + lastActiveScene = string.Empty; + } + + lastScenePaths.Clear(); + } + + static GUIStyle blockStyle1; + static GUIStyle blockStyle2; + static GUIStyle buttonStyle; + + void OnGUI() + { + blockStyle1 ??= new() { padding = new(6, 6, 6, 3) }; + blockStyle2 ??= new() { padding = new(6, 6, 3, 6) }; + buttonStyle ??= new(GUI.skin.button) { padding = new(3, 3, 1, 1) }; + + System.Action action = null; + + var isPlaying = EditorApplication.isPlaying; + + GUI.FocusControl(null); + + GUILayout.BeginHorizontal(blockStyle1); + { + GUI.enabled = !isPlaying; + + var path = string.Join(";", Enumerable + .Range(0, SceneManager.sceneCount) + .Select(i => SceneManager.GetSceneAt(i).path) + ); + + var options = sceneCollections.Select(x => x.collection.name); + + var index = sceneCollections.FindIndex(x => x.path == path); + if (index == -1) + { + index = 0; + options = options.Prepend("(New Scene Collection)"); + var newIndex = EditorGUILayout.Popup(index, options.ToArray()); + if (newIndex != index) + sceneCollections[newIndex - 1].collection.Open(); + if (GUILayout.Button("Create", buttonStyle, GUILayout.ExpandWidth(false))) + { + var sceneCollection = CreateInstance(); + sceneCollection.sceneAssets.AddRange( + Enumerable + .Range(0, SceneManager.sceneCount) + .Select(i => SceneManager.GetSceneAt(i).path) + .Select(AssetDatabase.LoadAssetAtPath) + ); + var assetPath = AssetDatabase.GenerateUniqueAssetPath($"Assets/Untitled Scene Collection.asset"); + AssetDatabase.CreateAsset(sceneCollection, assetPath); + AssetDatabase.Refresh(); + RefreshSceneCollections(); + } + } + else + { + var newIndex = EditorGUILayout.Popup(index, options.ToArray()); + if (newIndex != index) + sceneCollections[newIndex].collection.Open(); + } + + if (GUILayout.Button(EditorGUIUtility.IconContent("d_Refresh"), buttonStyle, GUILayout.ExpandWidth(false))) + { + action = OnEnable; + } + } + GUILayout.EndHorizontal(); + + if (enabled) + { + if (isPlaying) + { + GUI.color = new(.5f, 1, .5f); + GUI.enabled = false; + if (scenePath is not null && scenePath == activeScenePath) + scenePath = null; + } + else + { + if (enabled) + GUI.color = new(.75f, 1, .75f); + + if (scenePath == activeScenePath) + EditorApplication.EnterPlaymode(); + } + } + + GUILayout.BeginHorizontal(blockStyle2); + { + enabled = EditorGUILayout.Toggle(enabled, GUILayout.Width(16)); + GUI.enabled = enabled && !isPlaying; + sceneIndex = EditorGUILayout.Popup(sceneIndex, sceneNames); + + GUI.enabled = !isPlaying; + var disabled = !EditorSettings.enterPlayModeOptionsEnabled; + GUI.color = disabled ? new(1, .75f, .5f) : new(.825f, .825f, .825f); + disabled = EditorGUILayout.Toggle(disabled, GUILayout.Width(14)); + EditorGUILayout.LabelField("Reload Domain", GUILayout.Width(86)); + EditorSettings.enterPlayModeOptionsEnabled = !disabled; + EditorSettings.enterPlayModeOptions + = EnterPlayModeOptions.DisableDomainReload + | EnterPlayModeOptions.DisableSceneReload; + } + GUILayout.EndHorizontal(); + + action?.Invoke(); + } + + [MenuItem("Tools/Chameleon/Play Scene/Focus Selection")] + static void FocusSelection() + { + Collapse(); + + var gameObject = Selection.activeGameObject; + if (gameObject) + { + EditorGUIUtility.PingObject(gameObject); + SceneManager.SetActiveScene(Selection.activeGameObject.scene); + } + + var rootGameObjects = SceneManager + .GetActiveScene() + .GetRootGameObjects(); + + var parent = rootGameObjects.FirstOrDefault( + x => x.TryGetComponent(out DefaultParentForEditor _) + ); + + if (parent) + { + EditorUtility.SetDefaultParentObject(parent); + if (!gameObject) + gameObject = parent; + } + + if (!gameObject) + { + gameObject = rootGameObjects.FirstOrDefault(); + if (!gameObject) + return; + } + + if (parent) + { + var transform = gameObject.transform; + if (transform.childCount > 0) + gameObject = transform.GetChild(0).gameObject; + } + + EditorGUIUtility.PingObject(gameObject); + } + + [MenuItem("Tools/Chameleon/Play Scene/Focus Scene")] + static void FocusScene() + { + Collapse(); + + var gameObject = Selection.activeGameObject; + if (gameObject) + SceneManager.SetActiveScene(Selection.activeGameObject.scene); + + var rootGameObjects = SceneManager + .GetActiveScene() + .GetRootGameObjects(); + + gameObject = rootGameObjects.FirstOrDefault( + x => x.TryGetComponent(out DefaultParentForEditor _) + ); + + if (gameObject) + { + EditorUtility.SetDefaultParentObject(gameObject); + var transform = gameObject.transform; + if (transform.childCount > 0) + gameObject = transform.GetChild(0).gameObject; + } + else + { + gameObject = rootGameObjects.FirstOrDefault(); + if (!gameObject) + return; + } + + EditorGUIUtility.PingObject(gameObject); + Selection.activeObject = null; + } + + static void Collapse() + { + var activeObject = Selection.activeObject; + Collapse(repaint: false); + Collapse(repaint: true); + Selection.activeObject = activeObject; + } + + static void Collapse(bool repaint) + { + Selection.activeObject = null; + + var scopeFlags = F.Public | F.NonPublic; + var instanceFlags = scopeFlags | F.Instance; + + var window = typeof(EditorWindow).Assembly + .GetType("UnityEditor.SceneHierarchyWindow") + .GetField("s_LastInteractedHierarchy", scopeFlags | F.Static) + .GetValue(null) as EditorWindow; + + if (!window) + return; + + var treeOwner = window + .GetType() + .GetField("m_SceneHierarchy", instanceFlags) + .GetValue(window); + + var tree = treeOwner + .GetType() + .GetField("m_TreeView", instanceFlags) + .GetValue(treeOwner); + + if (tree is null) + return; + + var source = tree + .GetType() + .GetProperty("data", instanceFlags) + .GetValue(tree, null); + + source + .GetType() + .GetMethod("SetExpandedIDs", instanceFlags) + .Invoke(source, new[] { new int[0] }); + + if (repaint) + window.Repaint(); + } + } +} diff --git a/Assets/Editor/PlaySceneWindow.cs.meta b/Assets/Editor/PlaySceneWindow.cs.meta new file mode 100644 index 0000000..0cef724 --- /dev/null +++ b/Assets/Editor/PlaySceneWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e362fe89183ef0c458ec2a3a64894ba0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/SceneCollection.cs b/Assets/Editor/SceneCollection.cs new file mode 100644 index 0000000..a572cfd --- /dev/null +++ b/Assets/Editor/SceneCollection.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; + +using UnityEditor; +using UnityEditor.SceneManagement; + +using UnityEngine; + +namespace Chameleon.Editor +{ + [CreateAssetMenu(menuName = "Chameleon/Scene Collection (Editor)")] + public class SceneCollection : UnityEngine.ScriptableObject + { + public List sceneAssets = new(); + + [UnityEditor.Callbacks.OnOpenAsset] + public static bool OnOpenAsset(int instanceID, int _) + { + var @object = EditorUtility.EntityIdToObject(instanceID); + if (@object is SceneCollection sceneCollection) + sceneCollection.Open(); + return false; + } + + public void Open() + { + if (sceneAssets.Count == 0) + return; + + OpenScene(sceneAssets.First(), OpenSceneMode.Single); + + foreach (var sceneAsset in sceneAssets.Skip(1)) + OpenScene(sceneAsset, OpenSceneMode.Additive); + } + + static void OpenScene(SceneAsset sceneAsset, OpenSceneMode mode) + { + var scenePath = AssetDatabase.GetAssetPath(sceneAsset); + EditorSceneManager.OpenScene(scenePath, mode); + } + } +} diff --git a/Assets/Editor/SceneCollection.cs.meta b/Assets/Editor/SceneCollection.cs.meta new file mode 100644 index 0000000..969ebc1 --- /dev/null +++ b/Assets/Editor/SceneCollection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0d51f5e57ffbddc4dab2f2d0f5d8e693 \ No newline at end of file