using System.IO; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; namespace NightclubArcadia.EditorTools { /// /// Makes the split-scene layout usable in the Editor. /// /// The game runs from Bootstrap, which loads Systems and a level additively AT /// RUNTIME. Unity discards runtime-loaded scenes when Play ends and restores /// whatever the Editor had open, which for someone who pressed Play in Bootstrap is /// Bootstrap alone. The Hierarchy comes back nearly empty and it looks as though the /// scenes were destroyed. They are not — nothing is written to disk — but it is an /// unpleasant way to work. /// /// Two fixes, both opt-outable: /// - Open Game Scenes puts all three in the Hierarchy for authoring. /// - The Editor's scene setup is captured before Play and restored after, so what /// you had open is what you get back. /// /// GameBootstrap skips any scene that is already loaded, so having all three open /// and pressing Play does the right thing rather than loading anything twice. /// [InitializeOnLoad] public static class GameSceneWorkflow { const string RestorePrefKey = "NightclubArcadia.RestoreSceneSetup"; const string RestoreMenu = "Tools/Nightclub Arcadia/Restore Scene Setup After Play"; static SceneSetup[] setupBeforePlay; static GameSceneWorkflow() { EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } [MenuItem("Tools/Nightclub Arcadia/Open Game Scenes")] public static void OpenGameScenes() { // The prompt cannot be shown in batch mode, where it returns false and would // make this menu a silent no-op. if (!Application.isBatchMode && !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { return; } foreach (var path in new[] { ScenePaths.Bootstrap, ScenePaths.Systems, ScenePaths.WorkingLevel }) { if (!File.Exists(path)) { Debug.LogError($"[Scenes] Missing: {path}"); return; } } EditorSceneManager.OpenScene(ScenePaths.Bootstrap, OpenSceneMode.Single); EditorSceneManager.OpenScene(ScenePaths.Systems, OpenSceneMode.Additive); var level = EditorSceneManager.OpenScene(ScenePaths.WorkingLevel, OpenSceneMode.Additive); // The level owns the lighting and is where new objects should land. EditorSceneManager.SetActiveScene(level); Debug.Log("[Scenes] Bootstrap + Systems + SC101_ConferenceHall open. Level is active."); } [MenuItem(RestoreMenu)] static void ToggleRestore() { var on = !EditorPrefs.GetBool(RestorePrefKey, true); EditorPrefs.SetBool(RestorePrefKey, on); Debug.Log($"[Scenes] Restore scene setup after Play: {(on ? "on" : "off")}"); } [MenuItem(RestoreMenu, true)] static bool ToggleRestoreValidate() { Menu.SetChecked(RestoreMenu, EditorPrefs.GetBool(RestorePrefKey, true)); return true; } static void OnPlayModeStateChanged(PlayModeStateChange state) { if (!EditorPrefs.GetBool(RestorePrefKey, true)) { return; } if (state == PlayModeStateChange.ExitingEditMode) { setupBeforePlay = EditorSceneManager.GetSceneManagerSetup(); return; } if (state != PlayModeStateChange.EnteredEditMode || setupBeforePlay == null) { return; } var setup = setupBeforePlay; setupBeforePlay = null; // A scene deleted or renamed while Play was running would throw here, and // an exception in this callback leaves the Editor in a confusing state. var usable = setup.Where(s => File.Exists(s.path)).ToArray(); if (usable.Length == 0) { return; } var current = EditorSceneManager.GetSceneManagerSetup(); if (current.Length == usable.Length && current.Select(s => s.path).SequenceEqual(usable.Select(s => s.path))) { return; // already what we wanted; do not disturb it } try { EditorSceneManager.RestoreSceneManagerSetup(usable); } catch (System.Exception e) { Debug.LogWarning($"[Scenes] Could not restore the pre-Play scene setup: {e.Message}"); } } } }