【Unity】シーンに自動で追加されるEventSystemを削除する方法【Zenject】

# はじめに
uGUIを使っていて自動でHierarchyに`EventSystem`が生成される挙動が邪魔だったので、自動で生成されないようにできないか調べたメモ。

# uGUIと`EventSystem`
uGUIではその動作に必要な`EventSystem`をシーン毎に配置するというのがUnityの想定しているやり方なわけですが、これはプロジェクト全体で`EventSystem`をまとめて変更したくなったときに面倒で、例えば全ての`EventSystem`の値を同期するコンポーネントを作ったりが必要です。

# Zenjectとシーン
[Zenject](https://github.com/modesttree/Zenject) を使っているとシーンのペアレンティング/デコレート機能によりシーンを分割した設計・運用にしやすくなります。

また、Managerクラスのコンポーネントなどは`ProjectContext`プレハブ内に置いてバインドすることでシーンを単体で再生しても読み込み時に展開・利用でき、シーン毎に配置する必要もないので開発時に便利です。

コンポーネントをプロジェクト全体で一つに集約できるので、変更があっても一箇所で済みます。

# `EventSystem`の挙動
`EventSystem`も再生時に常に一つだけあればいいので先述の運用にすると楽なのですが、`EventSystem`はUnityが勝手にHierarchyに配置することがあり、プロジェクト全体で一つだけにしてシーンから消してもいつの間にか復活・シーンに保存されている場合があります。

Zenjectで機能ごとに細かくシーン分割していると気づけばいろんなシーンに雨後の筍のように`EventSystem`が生成・保存されていて、いちいち削除するのも面倒です。

では自動で`EventSystem`が生成されないようにする設定があるかというと、そんなものはありません。以下はUnity2019.1が`EventSystem`を自動生成している処理です。

<details><summary>[MenuOptions.cs](https://bitbucket.org/Unity-Technologies/ui/src/31cbc456efd5ed74cba398ec1a101a31f66716db/UnityEditor.UI/UI/MenuOptions.cs)（Bitbucket）</summary><div>

```MenuOptions.cs
...
        [MenuItem("GameObject/UI/Text", false, 2000)]
        static public void AddText(MenuCommand menuCommand)
        {
            GameObject go = DefaultControls.CreateText(GetStandardResources());
            PlaceUIElementRoot(go, menuCommand);
        }
...
        private static void PlaceUIElementRoot(GameObject element, MenuCommand menuCommand)
        {
            GameObject parent = menuCommand.context as GameObject;
            bool explicitParentChoice = true;
            if (parent == null)
            {
                parent = GetOrCreateCanvasGameObject();
...
        static public GameObject GetOrCreateCanvasGameObject()
        {
            GameObject selectedGo = Selection.activeGameObject;

            // Try to find a gameobject that is the selected GO or one if its parents.
            Canvas canvas = (selectedGo != null) ? selectedGo.GetComponentInParent<Canvas>() : null;
            if (IsValidCanvas(canvas))
                return canvas.gameObject;

            // No canvas in selection or its parents? Then use any valid canvas.
            // We have to find all loaded Canvases, not just the ones in main scenes.
            Canvas[] canvasArray = StageUtility.GetCurrentStageHandle().FindComponentsOfType<Canvas>();
            for (int i = 0; i < canvasArray.Length; i++)
                if (IsValidCanvas(canvasArray[i]))
                    return canvasArray[i].gameObject;

            // No canvas in the scene at all? Then create a new one.
            return MenuOptions.CreateNewUI();
        }
...
        static public GameObject CreateNewUI()
        {
            // Root for the UI
            var root = new GameObject("Canvas");
            root.layer = LayerMask.NameToLayer(kUILayerName);
            Canvas canvas = root.AddComponent<Canvas>();
            canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            root.AddComponent<CanvasScaler>();
            root.AddComponent<GraphicRaycaster>();

            // Works for all stages.
            StageUtility.PlaceGameObjectInCurrentStage(root);
            bool customScene = false;
            PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
            if (prefabStage != null)
            {
                root.transform.SetParent(prefabStage.prefabContentsRoot.transform, false);
                customScene = true;
            }

            Undo.RegisterCreatedObjectUndo(root, "Create " + root.name);

            // If there is no event system add one...
            // No need to place event system in custom scene as these are temporary anyway.
            // It can be argued for or against placing it in the user scenes,
            // but let's not modify scene user is not currently looking at.
            if (!customScene)
                CreateEventSystem(false);
            return root;
        }
...
        [MenuItem("GameObject/UI/Event System", false, 2100)]
        public static void CreateEventSystem(MenuCommand menuCommand)
        {
            GameObject parent = menuCommand.context as GameObject;
            CreateEventSystem(true, parent);
        }

        private static void CreateEventSystem(bool select)
        {
            CreateEventSystem(select, null);
        }

        private static void CreateEventSystem(bool select, GameObject parent)
        {
            StageHandle stage = parent == null ? StageUtility.GetCurrentStageHandle() : StageUtility.GetStageHandle(parent);
            var esys = stage.FindComponentOfType<EventSystem>();
            if (esys == null)
            {
                var eventSystem = new GameObject("EventSystem");
                if (parent == null)
                    StageUtility.PlaceGameObjectInCurrentStage(eventSystem);
                else
                    GameObjectUtility.SetParentAndAlign(eventSystem, parent);
                esys = eventSystem.AddComponent<EventSystem>();
                eventSystem.AddComponent<StandaloneInputModule>();

                Undo.RegisterCreatedObjectUndo(eventSystem, "Create " + eventSystem.name);
            }

            if (select && esys != null)
            {
                Selection.activeGameObject = esys.gameObject;
            }
        }
```

</div></details>

どうやら右クリックメニューからのuGUIコンポーネント生成時に`Canvas`がなければ`EventSystem`と一緒に生成しているみたいです。

自作関数に`[MenuItem]`を指定してUIの生成処理を上書きできないかも試しましたが、

<pre>
Cannot add menu item 'GameObject/UI/Image' for method 'MenuOptions.AddImage' because a menu item with the same name already exists.
</pre>

という警告が出て上書きできませんでした。

# `EventSystem`が追加されたら削除するクラスの実装
そもそも生成されないようにするのはできなさそうだったので、生成されたら即消すクラスを実装してみました。適当な`Editor/`以下のディレクトリに配置すると動作します（Unity2017.3.1f1で動作確認済み）。

```Editor/EventSystemDestroyer.cs
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

public class EventSystemDestroyer
{
    [InitializeOnLoadMethod]
    private static void Initialize()
    {
        EditorApplication.hierarchyChanged += () =>
        {
            var activeScene = SceneManager.GetActiveScene();
            var go = activeScene.GetRootGameObjects().LastOrDefault();
            if (go == null || go.name != "EventSystem") return;

            GameObject.DestroyImmediate(go);
            Debug.unityLogger.LogWarning("GameObject with EventSystem in root was destroyed", activeScene.name);
        };
    }
}
```

Hierarchyに変更が起きたときに`EventSystem`がシーンのルートかつ一番最後に存在していれば削除します。

Unityに自動で生成されたかどうかは判断してないので、もっと賢くやるなら「Hierarchyに最初から`EventSystem`が存在していたかどうか」などを判断する必要がありそうです。必要に応じて実装してください。

# 参考
* [【Unity】エディタ拡張で使用できるコールバックを40個まとめて紹介
](http://baba-s.hatenablog.com/entry/2017/12/04/090000#%E3%82%B7%E3%83%BC%E3%83%B3%E5%86%85%E3%81%AE%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%8C%E5%A4%89%E6%9B%B4%E3%81%95%E3%82%8C%E3%81%9F%E6%99%82)（コガネブログ）