🌑

コンテクスト・オーバーレイの使い方

2022/07/21に公開

コンテクストに応じたオーバーレイを表示する

内容

  • オーバーレイに関するまとまった例を書いてみる

モチベーション

ProBuilderではツールを切り替えるとコンテクスト(選択したツール)に応じたOverlayが表示される. DrawShapeToolの場合は以下のようなオーバーレイが表示されます.

シーン上に描画する形状を選択したり, 形状を確定するまでパラメータを弄ったりできます.

こういうのいいよなぁと思うわけですが, 内部ではSceneViewOverlayというのを使って表示が行われています. これは筆者の環境(UnityEditor 2021.3.4f1)ではレガシーとなっているようです.

// Deprecated. Use `TransientSceneViewOverlay` instead.
class SceneViewOverlay
{

またUnity2021.2のOverlay機能を使うで紹介されている方法と同じものがLegacyOverlayとして定義されています.

Overlays Developer Guideによると

If you are converting some existing UI code to use the overlay system, chances are it is written in IMGUI. We strongly advise to take this opportunity to convert your whole UI to UITK. In the case where this is not possible you can use the IMGUIOverlay class.

と説明されており, IMGUIOverlayよりはUI Toolkit(UITK)を使う方法を押しているようです. 個人的にもそちらの方がいいかなぁという感じはします.

Overlayクラスの特徴

Overlayクラスの特徴としてはOverlayAttributeを付けるだけで簡単にSceneView上に表示されることです. すごく簡単です.

一方定義したオーバーレイはすべてオーバーレイ・メニューに追加されてしまいます.

コンテクストによる切替ができないのならEditorToolごとのメニューのような用途では不便です. またOverlayAttributeを使うしかオーバーレイの追加ができません.

Overlays are not directly instantiated in code.

基本はdisplayedで表示/非表示を切り替えるだけです. たくさんのツールを定義したい場合, 非常に多くのオーバーレイが登録されることになります.

SceneViewにOverlayを表示

using UnityEditor;
using UnityEditor.Overlays;
using UnityEngine.UIElements;

[Overlay(typeof(SceneView), "My Custom Toolbar", true)]
public class MyToolButtonOverlay : Overlay {
    public override VisualElement CreatePanelContent() {
        var root = new VisualElement() { name = "My Toolbar Root" };
        root.Add(new Label() { text = "Hello Overlay" });
        return root;
    }
}

Panel Overlaysより

特定のEditorWindowにオーバーレイを表示する

まずオーバーレイを定義しましょう. ISupportsOverlaysの例をそのまま使います.

ComiXOverlay.cs
using UnityEditor.Overlays;
using UnityEngine.UIElements;

using ComiX.UI;

[Overlay(typeof(ComiXEditor), "comix", "ComiX/Overlay", true)]
class ComiXOverlay : Overlay {
    Label m_MouseLabel;

    public override VisualElement CreatePanelContent() {
        m_MouseLabel = new Label();
        m_MouseLabel.style.minHeight = 40;
        m_MouseLabel.RegisterCallback<MouseEnterEvent>(evt => m_MouseLabel.text = "Mouse is hovering this Overlay content!");
        m_MouseLabel.RegisterCallback<MouseLeaveEvent>(evt => m_MouseLabel.text = "Mouse is not hovering this Overlay content.");
        return m_MouseLabel;
    }
}

次にこれを表示するEditorWindowを定義します.

ComiXEditor.cs
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEditor.Overlays;

namespace ComiX.UI {
    public class ComiXEditor : EditorWindow, ISupportsOverlays {
        [MenuItem("Tools/ComiX/ComiXEditor")]
        public static void ShowExample() {
            ComiXEditor wnd = GetWindow<ComiXEditor>();
            wnd.titleContent = new GUIContent("ComiXEditor");
        }

        public void CreateGUI() {
            VisualElement root = rootVisualElement;

            VisualElement label = new Label("Overlay Example");
            root.Add(label);
        }
    }
}

特に実装しなければいけないメソッドはないです. さてタブを右クリックするとコンテクストメニューにOverlaysというのが追加されています.

これを押すとオーバーレイが表示されます.

利便性は分かりませんがこういうこともできます.

コードからオーバーレイを表示する

EditorWindow.TryGetOverlayを使うとオーバーレイの可視性をスクリプトからコントロールできます.

同じオーバーレイを使います. typeof(ComiXEditor)をtypeof(SceneView)に変更しておきましょう. また二番目の引数にはcomixと指定します.

ComiXOverlay.cs
using UnityEditor;
using UnityEditor.Overlays;
using UnityEngine.UIElements;

using ComiX.UI;

[Overlay(typeof(SceneView), "comix", "ComiX/Overlay", true)]
class ComiXOverlay : Overlay {
    Label m_MouseLabel;

    public override VisualElement CreatePanelContent() {
        m_MouseLabel = new Label();
        m_MouseLabel.style.minHeight = 40;
        m_MouseLabel.RegisterCallback<MouseEnterEvent>(evt => m_MouseLabel.text = "Mouse is hovering this Overlay content!");
        m_MouseLabel.RegisterCallback<MouseLeaveEvent>(evt => m_MouseLabel.text = "Mouse is not hovering this Overlay content.");
        return m_MouseLabel;
    }
}

次にEditorWindowにオーバーレイを表示するロジックを追加します. ISupportsOverlaysは不要なので消しておきます.

using UnityEngine.UIElements;
using UnityEngine;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEditor.Overlays;

namespace ComiX.UI {
    public class ComiXEditor : EditorWindow {
        [MenuItem("Tools/ComiX/ComiXEditor")]
        public static void ShowExample() {
            ComiXEditor wnd = GetWindow<ComiXEditor>();
            wnd.titleContent = new GUIContent("ComiXEditor");
        }

        public void CreateGUI() {

            // Each editor window contains a root VisualElement object
            VisualElement root = rootVisualElement;

            // VisualElements objects can contain other VisualElement following a tree hierarchy.
            VisualElement label = new Label("Overlay Example");
            root.Add(label);

            var button = new Button();
            button.name = "OverlayToggle";
            button.text = "Toggle Overlay";
            button.clicked += ButtonClicked;
            root.Add(button);
        }

        void ButtonClicked() {
            Overlay comixOverlay;
            if (SceneView.lastActiveSceneView.TryGetOverlay("comix", out comixOverlay)) {
                Debug.Log($"Found {comixOverlay.layout}");
                comixOverlay.displayed = !comixOverlay.displayed;
            } else {
                Debug.Log("Not Found");
            };
        }
    }
}

"Toggle Overlay"というボタンを押すとシーン上にオーバーレイが表示されます.

コンテクスト・オーバーレイ

OverlayのクラスにITransientOverlayを実装します.

using UnityEditor;
using UnityEngine;
using UnityEditor.Overlays;
using UnityEngine.UIElements;


[Overlay(typeof(SceneView), "Counter", "Selection Count", true)]
class SelectionCount : Overlay, ITransientOverlay {
    Label m_Label;

    public bool visible {
        get {
            return Selection.activeGameObject != null && Selection.activeGameObject.activeSelf;
        }
    }

    public override VisualElement CreatePanelContent() {
        Selection.selectionChanged += () =>
        {
            if (m_Label != null)
                m_Label.text = $"Selection Count {Selection.count}";
        };

        return m_Label = new Label($"Selection Count {Selection.count}");
    }
}

これは選択されているオブジェクトの個数を表示するオーバーレイです. 選択されているものがあるときだけ表示されればいいわけです.

実際選択されているゲーム・オブジェクトの個数を表示してくれます. そしてオーバーレイ・メニューにはこのオーバーレイの名前はありません.

課題

displayedChanged

displayedChangeイベントをサブスクライブすると, 表示/非表示の切り替えの時に実行される.

便利そうなのだがインスタンス化のタイミングがなくCreatePanelContentが表示のたびに呼び出される. ここが設定のポイントになる.

public override VisualElement CreatePanelContent() {
    displayedChanged += OnDisplayedChanged;
    // ...
}

問題はこれをアンサブスクライブする時で, OnDisable的なものはない. 仕方ないので以下のようにする. うーん・・・自然に呼び出してくれる良さが半減といった感じ.

void OnDisplayedChanged(bool displayed) {
    if (!displayed) {
        Debug.Log("Hide Overlay");
        displayedChanged -= OnDisplayedChanged;
        return;
    }

    Debug.Log("Show Overlay");
}

Overlay.OnWillBeDestroyedまで放置しておいてもいいのかもしれないが良く分からない.

まとめ

当たり前ですがどの機能を使うかは状況次第ですね. コンテクスト・オーバーレイはToolManagerと組み合わせると簡単にツール用のオーバーレイが表示できそうです.

Reference

Discussion