Skip to content

Instantly share code, notes, and snippets.

@alexanderameye
Last active December 1, 2023 22:37
Show Gist options
  • Save alexanderameye/c1f99c6b84162697beedc8606027ed9c to your computer and use it in GitHub Desktop.
Save alexanderameye/c1f99c6b84162697beedc8606027ed9c to your computer and use it in GitHub Desktop.
A small scene switcher utility for Unity
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;
}
}
@onefifth
Copy link

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 and OnWillBeDestroyed 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 a SceneView.duringSceneGui += hook to have a simple ToolbarToggle draw stuff in the scene view. My initial approach was nearly identical to the way the DropdownToggleExample 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's duringSceneGui 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment