【Unity】実機と同じシーケンスで再生開始するボタンをプレイボタンの横に追加【エディタ拡張】

# 動作
<img width="149" alt="スクリーンショット 2019-03-23 16.12.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/27863/bc270942-cf51-b843-aa5b-708b3a937981.png">
![3月-23-2019 16-12-41.gif](https://qiita-image-store.s3.amazonaws.com/0/27863/e1560b27-31e4-2507-d028-e5a12707e574.gif)

`Initialize`シーンがすぐ`Title`シーンをロードしているのでわかりづらいが、[0]をクリックしたら現在アクティブな`Game`シーンからではなく`Initialize`シーンから再生が開始されている。

上記gifだと最初からHierarchyに`Initialize`シーンが置いてあるけど、なくても大丈夫。

# モチベーション
普段は [Zenject](https://github.com/modesttree/Zenject) でDIしているので各シーンを直接再生しながら開発しているけど、たまに実機と同じシーケンスで再生したいことがあった。

それまでは

* Hierarchyに"Scenes In Build"で0にしてある`Initialize`シーンを配置＆`Unload`状態に
* 必要なときに`Initialize`シーンだけアクティブにして再生

をやってたけど何度もやってて切り替えが面倒になったので、押しやすい場所にやりたいことを一発でやれるボタンを置こうと思った。

# 導入方法
再生ボタンの両サイドにボタンを配置できるエディタ拡張「[unity-toolbar-extender](https://github.com/marijnz/unity-toolbar-extender)」を入れて、以下のコードを適当な`Editor/`以下に配置する。

```PlayFromScene0Button.cs
#if UNITY_EDITOR
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace UnityToolbarExtender
{
    internal static class ToolbarStyles
    {
        public static readonly GUIStyle CommandButtonStyle;

        static ToolbarStyles()
        {
            CommandButtonStyle = new GUIStyle("Command")
            {
                fontSize = 16,
                alignment = TextAnchor.MiddleCenter,
                imagePosition = ImagePosition.ImageAbove,
            };
        }
    }

    [InitializeOnLoad]
    public class PlayFromScene0Button
    {
        private const string Scene0HierarchyIndexKey = "Scene0HierarchyIndex";
        private const string ShouldResetPlayModeStartSceneKey = "ShouldResetPlayModeStartScene";
        private const string ButtonLabel = "0";
        private const string ButtonTooltip = "Play from first scene of \"Scenes In Build\"";

        static PlayFromScene0Button()
        {
            ToolbarExtender.LeftToolbarGUI.Add(OnToolbarGUI);
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
        }

        private static string GetConfigValue(string name)
        {
            return EditorUserSettings.GetConfigValue(name);
        }

        private static void SetConfigValue(string name, string value)
        {
            EditorUserSettings.SetConfigValue(name, value);
        }

        private static void ResetConfigValue(string name)
        {
            EditorUserSettings.SetConfigValue(name, null);
        }

        private static void OnToolbarGUI()
        {
            if (Validate() == false) return;

            GUILayout.FlexibleSpace();

            // ボタン表示
            if (GUILayout.Button(new GUIContent(ButtonLabel, ButtonTooltip), ToolbarStyles.CommandButtonStyle))
            {
                PlayFromScene0();
            }
        }

        [MenuItem("Edit/Play from scene 0 %@", true)]
        private static bool Validate()
        {
            // "Scenes In Build"にシーンが登録されていなければinvalid
            if (EditorBuildSettings.scenes.Length == 0) return false;

            // プレイモード時はinvalid
            if (EditorApplication.isPlayingOrWillChangePlaymode) return false;

            return true;
        }

        [MenuItem("Edit/Play from scene 0 %@", false, 149)]
        private static void PlayFromScene0()
        {
            // シーンを保存するかどうか尋ねる
            if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
            {
                var scene0Path = EditorBuildSettings.scenes[0].path;
                var scene0Asset = AssetDatabase.LoadAssetAtPath<SceneAsset>(scene0Path);
                var sceneSetups = EditorSceneManager.GetSceneManagerSetup();
                var scene0Setup = sceneSetups.FirstOrDefault(s => s.path == scene0Path);

                // シーン0がHierarchyに存在＆ロードされていない場合は閉じる（その状態でHierarchyに存在しているとうまく動かない）
                if (scene0Setup != null && scene0Setup.isLoaded == false)
                {
                    var scene = SceneManager.GetSceneByPath(scene0Path);
                    EditorSceneManager.CloseScene(scene, true);
                    var scene0HierarchyIndex = sceneSetups.ToList().IndexOf(scene0Setup).ToString();

                    // シーン0をあとでHierarchyに復元するためにインデックスを保存
                    SetConfigValue(Scene0HierarchyIndexKey, scene0HierarchyIndex);
                }

                SetConfigValue(ShouldResetPlayModeStartSceneKey, bool.TrueString);
                EditorSceneManager.playModeStartScene = scene0Asset;
                EditorApplication.isPlaying = true;
            }
        }

        private static void OnPlayModeStateChanged(PlayModeStateChange state)
        {
            if (state == PlayModeStateChange.EnteredPlayMode && GetConfigValue(ShouldResetPlayModeStartSceneKey) == bool.TrueString)
            {
                ResetConfigValue(ShouldResetPlayModeStartSceneKey);
                EditorSceneManager.playModeStartScene = null;
            }

            if (state == PlayModeStateChange.EnteredEditMode)
            {
                int scene0HierarchyIndex;

                // 必要であればシーン0をHierarchyに復元（特別な場合を除き自動で復元される）
                if (int.TryParse(GetConfigValue(Scene0HierarchyIndexKey), out scene0HierarchyIndex) && -1 < scene0HierarchyIndex)
                {
                    ResetConfigValue(Scene0HierarchyIndexKey);

                    var scene0Path = EditorBuildSettings.scenes[0].path;
                    var scene0 = EditorSceneManager.OpenScene(scene0Path, OpenSceneMode.AdditiveWithoutLoading);
                    var dstScenePath = EditorSceneManager.GetSceneManagerSetup()[scene0HierarchyIndex].path;
                    var dstScene = SceneManager.GetSceneByPath(dstScenePath);

                    EditorSceneManager.MoveSceneBefore(scene0, dstScene);
                }
            }
        }
    }
}
#endif
```

コメントにちょっと書いてるけど"Scenes In Build"で0のシーンがUnload状態でHierarchyに置いてある場合だけ挙動が変だったので、その場合は一旦シーンをHierarchyから消して後で復元している。

# 仕様
* 動作確認済み
  * macOS High Sierra（10.13.6）
  * Unity 2018.2.20f1
  * シーン0が
     * Hierarchyにない
     * Hierarchyにある
         * ロード状態
         * アンロード状態
  * 他のシーンを(複数)開いているとき
* ショートカットキー：`⌘@`
* "Scenes In Build"にシーンが登録されていなければボタン非表示
* 再生中もボタン非表示
* 再生前に必要であればシーンを保存するか確認
  * "Don't Save"なら保存せずに再生
  * "Cancel"なら再生しない
* 再生後に再生前のHierarchy復元

[コガネブログ](http://baba-s.hatenablog.com/entry/2018/08/27/090000)さんによると
>
自分が「unity-toolbar-extender」を試してみたところ、
MacBook Pro の Unity では正常に動作しましたが、
Windows 7 や Windows 10 の Unity ではボタンが表示されない現象に遭遇しました
>
もしかしたら「unity-toolbar-extender」は Windows では使用できないかもしれません
>

とのことなのでWindowsなどでは動かないかも。

# 所感
Unityエディターのデフォルト機能としてほしいくらいには便利。

# 参考
* [SceneSwitcher.cs](https://github.com/marijnz/unity-toolbar-extender/blob/master/Assets/ToolbarExtender/Example/Scripts/Editor/SceneSwitcher.cs)
  * [unity-toolbar-extender](https://github.com/marijnz/unity-toolbar-extender)のサンプル
* [【Unity】エディタ拡張でstaticなフィールドに置いたインスタンスが、コンパイル/再生時に初期化される問題の対策について](http://tsubakit1.hateblo.jp/entry/2017/01/31/235730)
  * 最初staticフィールドに復元情報入れてたら再生時にnullになって困った
* [EditorUserSettings.Set/GetConfigValue](https://anchan828.github.io/editor-manual/web/data_storage.html#h3-2)
  * `EditorUserSettings`に情報を保存しておけば再生時にリセットされない
* [第8章　MenuItem](https://anchan828.github.io/editor-manual/web/part1-menuitem.html)
  * ショートカットキー周り