はじめに
uGUIを使っていて自動でHierarchyにEventSystem
が生成される挙動が邪魔だったので、自動で生成されないようにできないか調べたメモ。
uGUIとEventSystem
uGUIではその動作に必要なEventSystem
をシーン毎に配置するというのがUnityの想定しているやり方なわけですが、これはプロジェクト全体でEventSystem
をまとめて変更したくなったときに面倒で、例えば全てのEventSystem
の値を同期するコンポーネントを作ったりが必要です。
Zenjectとシーン
Zenject を使っているとシーンのペアレンティング/デコレート機能によりシーンを分割した設計・運用にしやすくなります。
また、ManagerクラスのコンポーネントなどはProjectContext
プレハブ内に置いてバインドすることでシーンを単体で再生しても読み込み時に展開・利用でき、シーン毎に配置する必要もないので開発時に便利です。
コンポーネントをプロジェクト全体で一つに集約できるので、変更があっても一箇所で済みます。
EventSystem
の挙動
EventSystem
も再生時に常に一つだけあればいいので先述の運用にすると楽なのですが、EventSystem
はUnityが勝手にHierarchyに配置することがあり、プロジェクト全体で一つだけにしてシーンから消してもいつの間にか復活・シーンに保存されている場合があります。
Zenjectで機能ごとに細かくシーン分割していると気づけばいろんなシーンに雨後の筍のようにEventSystem
が生成・保存されていて、いちいち削除するのも面倒です。
では自動でEventSystem
が生成されないようにする設定があるかというと、そんなものはありません。以下はUnity2019.1がEventSystem
を自動生成している処理です。
[MenuOptions.cs](https://bitbucket.org/Unity-Technologies/ui/src/31cbc456efd5ed74cba398ec1a101a31f66716db/UnityEditor.UI/UI/MenuOptions.cs)(Bitbucket)
...
[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;
}
}
どうやら右クリックメニューからのuGUIコンポーネント生成時にCanvas
がなければEventSystem
と一緒に生成しているみたいです。
自作関数に[MenuItem]
を指定してUIの生成処理を上書きできないかも試しましたが、
Cannot add menu item 'GameObject/UI/Image' for method 'MenuOptions.AddImage' because a menu item with the same name already exists.
という警告が出て上書きできませんでした。
EventSystem
が追加されたら削除するクラスの実装
そもそも生成されないようにするのはできなさそうだったので、生成されたら即消すクラスを実装してみました。適当なEditor/
以下のディレクトリに配置すると動作します(Unity2017.3.1f1で動作確認済み)。
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個まとめて紹介
(コガネブログ)