Last active
April 17, 2025 02:17
-
-
Save alexanderameye/c1f99c6b84162697beedc8606027ed9c to your computer and use it in GitHub Desktop.
A small scene switcher utility for Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.IO; | |
using UnityEditor; | |
using UnityEditor.Overlays; | |
using UnityEditor.SceneManagement; | |
using UnityEditor.Toolbars; | |
using UnityEngine; | |
using UnityEngine.SceneManagement; | |
using UnityEngine.UIElements; | |
public static class EditorSceneSwitcher | |
{ | |
public static bool AutoEnterPlaymode = false; | |
public static readonly List<string> ScenePaths = new(); | |
public static void OpenScene(string scenePath) | |
{ | |
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) | |
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); | |
if (AutoEnterPlaymode) EditorApplication.EnterPlaymode(); | |
} | |
public static void LoadScenes() | |
{ | |
// clear scenes | |
ScenePaths.Clear(); | |
// find all scenes in the Assets folder | |
var sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] {"Assets"}); | |
foreach (var sceneGuid in sceneGuids) | |
{ | |
var scenePath = AssetDatabase.GUIDToAssetPath(sceneGuid); | |
var sceneAsset = AssetDatabase.LoadAssetAtPath(scenePath, typeof(SceneAsset)); | |
ScenePaths.Add(scenePath); | |
} | |
} | |
} | |
[Icon("d_SceneAsset Icon")] | |
[Overlay(typeof(SceneView), OverlayID, "Scene Switcher Creator Overlay")] | |
public class SceneSwitcherToolbarOverlay : ToolbarOverlay | |
{ | |
public const string OverlayID = "scene-switcher-overlay"; | |
private SceneSwitcherToolbarOverlay() : base( | |
SceneDropdown.ID, | |
AutoEnterPlayModeToggle.ID | |
) | |
{ | |
} | |
public override void OnCreated() | |
{ | |
// load the scenes when the toolbar overlay is initially created | |
EditorSceneSwitcher.LoadScenes(); | |
// subscribe to the event where scene assets were potentially modified | |
EditorApplication.projectChanged += OnProjectChanged; | |
} | |
// Called when an Overlay is about to be destroyed. | |
// Usually this corresponds to the EditorWindow in which this Overlay resides closing. (Scene View in this case) | |
public override void OnWillBeDestroyed() | |
{ | |
// unsubscribe from the event where scene assets were potentially modified | |
EditorApplication.projectChanged -= OnProjectChanged; | |
} | |
private void OnProjectChanged() | |
{ | |
// reload the scenes whenever scene assets were potentially modified | |
EditorSceneSwitcher.LoadScenes(); | |
} | |
} | |
[EditorToolbarElement(ID, typeof(SceneView))] | |
public class SceneDropdown : EditorToolbarDropdown | |
{ | |
public const string ID = SceneSwitcherToolbarOverlay.OverlayID + "/scene-dropdown"; | |
private const string Tooltip = "Switch scene."; | |
public SceneDropdown() | |
{ | |
var content = | |
EditorGUIUtility.TrTextContentWithIcon(SceneManager.GetActiveScene().name, Tooltip, | |
"d_SceneAsset Icon"); | |
text = content.text; | |
tooltip = content.tooltip; | |
icon = content.image as Texture2D; | |
// hacky: the text element is the second one here so we can set the padding | |
// but this is not really robust I think | |
ElementAt(1).style.paddingLeft = 5; | |
ElementAt(1).style.paddingRight = 5; | |
clicked += ToggleDropdown; | |
// keep track of panel events | |
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel); | |
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel); | |
} | |
protected virtual void OnAttachToPanel(AttachToPanelEvent evt) | |
{ | |
// subscribe to the event where the play mode has changed | |
EditorApplication.playModeStateChanged += OnPlayModeStateChanged; | |
// subscribe to the event where scene assets were potentially modified | |
EditorApplication.projectChanged += OnProjectChanged; | |
// subscribe to the event where a scene has been opened | |
EditorSceneManager.sceneOpened += OnSceneOpened; | |
} | |
protected virtual void OnDetachFromPanel(DetachFromPanelEvent evt) | |
{ | |
// unsubscribe from the event where the play mode has changed | |
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; | |
// unsubscribe from the event where scene assets were potentially modified | |
EditorApplication.projectChanged -= OnProjectChanged; | |
// unsubscribe from the event where a scene has been opened | |
EditorSceneManager.sceneOpened -= OnSceneOpened; | |
} | |
private void OnPlayModeStateChanged(PlayModeStateChange stateChange) | |
{ | |
switch (stateChange) | |
{ | |
case PlayModeStateChange.EnteredEditMode: | |
SetEnabled(true); | |
break; | |
case PlayModeStateChange.EnteredPlayMode: | |
// don't allow switching scenes while in play mode | |
SetEnabled(false); | |
break; | |
} | |
} | |
private void OnProjectChanged() | |
{ | |
// update the dropdown label whenever the active scene has potentially be renamed | |
text = SceneManager.GetActiveScene().name; | |
} | |
private void OnSceneOpened(Scene scene, OpenSceneMode mode) | |
{ | |
// update the dropdown label whenever a scene has been opened | |
text = scene.name; | |
} | |
private void ToggleDropdown() | |
{ | |
var menu = new GenericMenu(); | |
foreach (var scenePath in EditorSceneSwitcher.ScenePaths) | |
{ | |
var sceneName = Path.GetFileNameWithoutExtension(scenePath); | |
menu.AddItem(new GUIContent(sceneName), text == sceneName, | |
() => OnDropdownItemSelected(sceneName, scenePath)); | |
} | |
menu.DropDown(worldBound); | |
} | |
private void OnDropdownItemSelected(string sceneName, string scenePath) | |
{ | |
text = sceneName; | |
EditorSceneSwitcher.OpenScene(scenePath); | |
} | |
} | |
[EditorToolbarElement(ID, typeof(SceneView))] | |
public class AutoEnterPlayModeToggle : EditorToolbarToggle | |
{ | |
public const string ID = SceneSwitcherToolbarOverlay.OverlayID + "/auto-enter-playmode-toggle"; | |
private const string Tooltip = "Auto enter playmode."; | |
public AutoEnterPlayModeToggle() | |
{ | |
var content = EditorGUIUtility.TrTextContentWithIcon("", Tooltip, "d_preAudioAutoPlayOff"); | |
text = content.text; | |
tooltip = content.tooltip; | |
icon = content.image as Texture2D; | |
value = EditorSceneSwitcher.AutoEnterPlaymode; | |
this.RegisterValueChangedCallback(Toggle); | |
} | |
private void Toggle(ChangeEvent<bool> evt) | |
{ | |
EditorSceneSwitcher.AutoEnterPlaymode = evt.newValue; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Those changes look good to me! It's nice to know I wasn't way off base with the attach/detach handlers. Thanks for the com.unity.xr.arfoundation link.
OnCreated
andOnWillBeDestroyed
are almost certainly also called when enabling/disabling the overlay/toolbar completely. Using them here makes sense to me!For the
ToolbarElements
, in my case I was (perhaps foolishly) trying to use aSceneView.duringSceneGui +=
hook to have a simpleToolbarToggle
draw stuff in the scene view. My initial approach was nearly identical to the way theDropdownToggleExample
works on docs page for ToolbarOverlay. If you change that example to use the AttachToPanel/DetachFromPanel pattern, it does fix the leak issue, but the overlay collapsed state becomes a more obvious/real problem. The button'sduringSceneGui
handler gets removed and the stuff being drawn stops getting drawn.In that specific case, separating the draw logic from the toggle button is probably the way to go? Instead it'd notify some other object when the toggle state changed, and that other object would manage its own
duringSceneGui
hook and draw stuff into the scene view.But, my whole ramble is basically; it's real easy to accidentally cross the complexity threshold where jamming functionality directly into the button starts causing hard-to-spot problems. (the example in the docs leads you directly off that cliff imo) It'd be great to have better sample code to reference, or for the API to make it more clear how that kind of complexity should be dealt with.
Either way... you certainly don't need my permission to update your gist (haha), but for this specific case those changes look good to me! Thanks for responding!