Unity 2021.2のSceneViewで色々な操作を行う新機能、Overlayについて

この記事は、[Unity #1 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/unity) １４日目の記事です。

#はじめに

最近のUnityは、いくつかの操作をSceneViewから素早くアクセスするための機能や、SceneViewで色々な操作を行うためのAPI等が追加されたんですが、その辺り「コレ is 何」「どうやって使うの？」となったので、その辺りの機能と使い方について紹介していこうと思います。

#環境

本記事は `Unity 2021.2.6f1` を使用して作成しました。


#Overlays

Unity 2021.2でSceneViewまわりに色々とついてるのはコレです。今までPlayボタンの左やシーン画面の右上等にあったボタンがシーン内に配置された事により、シーンビューから素早く操作できるようになりました。

![スクリーンショット 2021-12-13 19.38.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/d865df76-2719-53aa-3b6a-a5c1671ad2ad.png)

必要に応じてドックに接続したり、UIを非表示にしたり、ボタンの位置を動かしたりできるようになったので、シーンで操作するときに邪魔にならない位置に動かしたりできます。基本的には初期設定から動かす人はあんまり居ないと思いますが…とにかく動かせるようになりました。

![スクリーンショット 2021-12-13 20.00.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/61024020-ab6e-5471-3360-2016148f341b.png)

https://docs.unity3d.com/2021.2/Documentation/Manual/overlays.html

Overlaysで便利なのは、独自のボタンや操作を作るケースです。今まではカスタムウインドウを開いたり、メニューアイテムを選択してカスタムコマンドを実行していた所を、シーンビューから離れず操作できるようになります。

例えば、下のようにシーンをシーンビュー上で素早く切り替えたり、PlayerやGameControllerといった、何度も操作することになるオブジェクトに素早くアクセスするといった事が可能になります。

![select player.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/17ae9ad5-0d1b-e33a-bd79-8b684ad11aab.gif)
![change scene set.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/e73b2b98-6bc3-5cbd-fc7f-0b1fc48390d9.gif)

## Overlaysを自作する 

上でも紹介したとおり、Overlaysは独自の機能を作成することが可能です。

まず、とりあえず動くコードはこんな感じです。下のコードではボタンを押したらPlayerオブジェクトが選択されるようになります。

```cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Overlays;
using UnityEditor.UIElements;
using UnityEditor.Toolbars;

// オーバーレイ本体
[Overlay(typeof(SceneView), MenuPath)]
public class SceneControlExample : ToolbarOverlay
{
    const string MenuPath = "Custom/SceneControl";

    SceneControlExample() : base(
        SelectPlayerButton.ID // IDを使ってUIを登録
        ){}
}

// ボタンの挙動 
[EditorToolbarElement(ID, typeof(SceneView))]
public class SelectPlayerButton : ToolbarButton
{
    public const string ID = "SeceneControlExample.SelectPlayerButton"; // ユニークなID

    SelectPlayerButton()
    {
        tooltip = "Playerタグが設定されているオブジェクトを選択します。";
        text = "プレイヤーを選択";
        clicked += OnClicked;
    }

    private void OnClicked()
    {
        Selection.activeGameObject = GameObject.FindWithTag("Player");
    }
}
```
コードを作成後、SceneViewのメニューから `Overlays/Custom/SceneControl` でボタンがついたメニューが表示され、あとはボタンを押したらPlayerタグが設定されてるオブジェクトが選択されます。

![スクリーンショット 2021-12-13 20.27.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/4f00741d-41d3-c928-6032-b7843da6beb4.png)

![スクリーンショット 2021-12-13 20.32.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/2ce12161-7aba-4276-bcef-c6dd8ebe9886.png)

この状態でメニュー上に結合したり、縦・横にボタンを並べるといった事もできます。余計な所に浮いてると邪魔なので、積極的に結合したいところです。ただし、その場合アイコンを用意しないと他のボタンと見分けがつかなくなる事があるので注意が必要です。

![スクリーンショット 2021-12-13 21.12.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/9314dff1-099e-c6a3-e2a2-9138288b4a0e.png)


ボタン以外にも、ON/OFFを切り替えるトグルや一覧から選択するのに便利なドロップダウン等があります。その辺りは [マニュアル](https://docs.unity3d.com/2021.2/Documentation/Manual/overlays-custom.html) に全部入りのサンプルがおいてあるので、それを試してみると良さげな感じがします。下のGIFアニメはソレを試した結果です。

![example.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/bb3cce2c-7566-c96b-2118-ea4552fdad24.gif)

https://docs.unity3d.com/2021.2/Documentation/Manual/overlays-custom.html

## UIBuilderで作成したメニューをOverlayで使用する

任意のボタンを登録する以外でもUIToolKit（UIElements）のUIを使用してオーバーレイのパネルを作成する事もできます。

例えば下のようなメニューをUIBuilderで作成します。レイアウトは超適当です。

![スクリーンショット 2021-12-13 21.00.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/73b0547f-6ce8-1136-fe20-20873c4e97a5.png)

```
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <ui:Label text="メニュー" display-tooltip-when-elided="true" style="font-size: 59px;" />
    <ui:Button text="何かする" display-tooltip-when-elided="true" name="Button1" />
    <ui:Button text="兄化する" display-tooltip-when-elided="true" name="Button2" />
</ui:UXML>
```

次にOverlayを作成して、上の `VisualElement` を登録します。

```cs
[Overlay(typeof(SceneView), "Custom/MyMenu")]
public class MyCustomToolbar : Overlay
{
    public override VisualElement CreatePanelContent()
    {
        var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UXML.uxml");
        var root = uxml.CloneTree();

        root.Q<Button>("Button1").clicked += () => { Debug.Log("何かした"); }; // ボタンを押したらログが出る
        root.Q<Button>("Button2").clicked += () => { Debug.Log("兄化した"); };

        return root;
    }
}
```
あとはメニューの `Overlay/Custom/MyMenu` からメニューを表示すれば、下のようなメニューが表示されます。

![スクリーンショット 2021-12-13 21.07.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/18ad16ae-caab-4ead-c6ad-6421d4e89d3b.png)

一つ注意すべき事は、 `Overlay` で作成したメニューは上に接続したときに内容が殆ど隠れてしまう点です。常時ボタンを表示したいケースでは `ToolbarOverlay` を使用、少し凝ったレイアウトで作りたい場合では `Overlay` を使用するといった使い分けになりそうです。

![スクリーンショット 2021-12-13 21.17.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/3151647d-4c1f-a7d0-dd1a-4d897a16e375.png)

# その他・ヒント

## Overlayが持っているパラメーターを残す

Overlayが保持するパラメータは、基本的にゲームを再生したりスクリプトを更新すると吹っ飛びます。もしゲームを再生しても値を残したい場合は、 `ScriptableSingleton` に格納しておけばゲーム再生後でもデータが吹っ飛ぶ事が避けられます。

```cs
[Overlay(typeof(SceneView), "Custom/MyMenu")]
public class MyCustomToolbar : Overlay
{
    public int count = 0;
    public override VisualElement CreatePanelContent()
    {
        var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UXML.uxml");
        var root = uxml.CloneTree();

        // ゲームを再生しても数が増え続ける
        root.Q<Button>("Button1").clicked += () => { MyData.instance.Count++; Debug.Log(MyData.instance.Count); };
        // ゲームを再生するとCountの値は０に戻る
        root.Q<Button>("Button2").clicked += () => { this.count++; Debug.Log(this.count); };

        return root;
    }
}

public class MyData : ScriptableSingleton<MyData>
{
    public int Count = 0;
}
```

またEditorToolsと連携する場合でもScriptableSingletonは便利です。

## Undoは大事

Overlay経由でオブジェクトを操作した場合、コンポーネントやTransformの値が変わっていても「シーン戦線異常なし」という事になる場合があります。またUndoが登録されていない変更はCtrl+Zで巻き戻ることができません。

なので、操作する可能性のあるオブジェクトは `Undo.RegisterCompleteObjectUndo` に登録してから変更を加えます。

```cs
        root.Q<Button>("Button1").clicked += () =>
        {
            Undo.RegisterCompleteObjectUndo(
                // 操作するオブジェクトをすべてUndoに突っ込む
                new Object[] { 
                    MyData.instance, 
                    Selection.activeGameObject.transform 
                },
                "(0,0,0)へ動かした後に数を増やす");

            // transformとMyData.Instance.Countを更新
            Selection.activeGameObject.transform.position = Vector3.zero;
            MyData.instance.Count++;

        };
```

実際にUndoが登録されているかは、Unityエディター右上のUndo Historyから確認できます。

![スクリーンショット 2021-12-13 21.46.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/d6571b9a-eb5d-e31c-8a81-555bcbf0ef2e.png)

## 特定のオブジェクトを編集する場合はEditorToolsが便利

特定のオブジェクトやコンポーネントを編集…例えばキャラクターが移動する場所をVector3で持つといった場合、Overlayで云々するよりもEditorToolsで操作した方が楽に操作できます。EditorToolsに登録するとハンドル向けのOverlayに新しい操作が追加されます。

![EditorTools.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12772/97f91271-9692-2419-0703-6457b82dfb26.gif)

```cs
public class MoveTo : MonoBehaviour
{
    public Vector3 Offset; // オブジェクトを動かせるように、現在地からの相対値で持つ

    public Vector3 TargetPosition
    {
        get => transform.position + Offset;
        set => Offset = value - transform.position;
    }
}
```
```cs
[EditorTool("Set Target Position", typeof(MoveTo))]
public class MoveToEditor : EditorTool, IDrawSelectedHandles
{
    // IDrawSelectedHandlesより。 オブジェクト選択中に常に表示されるハンドル
    public void OnDrawHandles()
    {
        var moveTo = target as MoveTo;
        var targetPosition = moveTo.TargetPosition;
        var currentPosition = moveTo.transform.position;

        Handles.DrawLine(currentPosition, targetPosition);
    }

    // EditorToolsがアクティブのときに表示されるハンドル
    public override void OnToolGUI(EditorWindow window)
    {
        Handles.BeginGUI();
        OnDrawGUI();
        Handles.EndGUI();

        OnDrawHandle();
    }

    void OnDrawHandle()
    {
        var moveTo = target as MoveTo;
        var newValue = Handles.PositionHandle(moveTo.TargetPosition, Quaternion.identity);

        if (EditorGUI.EndChangeCheck())
        {
            Undo.RegisterCompleteObjectUndo(moveTo, "Move target position");

            moveTo.TargetPosition = newValue;
        }
    }

    void OnDrawGUI()
    {
        var moveTo = target as MoveTo;

        GUIContent content = new GUIContent();
        GUIStyle style = EditorStyles.helpBox;
        var uiRect = HandleUtility.WorldPointToSizedRect(moveTo.TargetPosition, content, style); // World座標をUIの座標系に変換
        uiRect.width = 200;
        uiRect.height = 40;
        using (new GUILayout.AreaScope(uiRect))
        {
            GUILayout.Label(moveTo.TargetPosition.ToString());
        }
    }
}

```