エディタのカスタムモードを作成する

この記事は、[Unity #2 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/unity) 6日目の記事になります。

# 概要
現在、Unityでは起動時にコンパイルエラーなどがあるとセーフモードで立ち上げることができるようになっています。
この際表示されるエディタは通常とは少し異なり、再生ボタンなどがあった場所にセーフモードである旨が表示され、メニューバーの内容も通常とは異なります。
今回はこのようなオリジナルのモードを作成する方法について少しだけわかったので記事にします。

※現状これらの方法についてはマニュアルに記載されていないため、今後変更される可能性があります。

# 準備
使用しているバージョン : [Unity 2021.2.5f1](https://unity3d.com/unity/whats-new/2021.2.5)

まずはパッケージを作ります。これはモード変更機能が以下の場所を読むようになっているためです。

* Unity.exeのパス/Data/Resources/default.mode (EditorApplication.applicationContentsPath + "/Resources/default.mode")
* パッケージフォルダ内

パッケージの作成方法については話が逸れるので割愛します。
[詳しくはこちら](https://docs.unity3d.com/Manual/CustomPackages.html)をご覧ください

今回はこのような構造にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133834/11203828-b52b-e95f-24b8-b9339f44bd44.png)
Modeフォルダ内には、カスタムモードのレイアウトファイルである.modeファイルを配置してください。

# モードを定義する
準備段階で作成したmodeファイルの中身を記述していきます。試しに今回はこのようなレイアウトにします。
また、メニューバーも内容を変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133834/02eff74d-f10c-1a1e-3330-8326af7ce4f2.png)

左側 : タブ移動不可のHierarchy, タブ移動不可のInspector
中央 : タブ移動可のSceneView,　タブ移動可のGameView
右側 : タブ移動不可のQuickSearch

<details><summary>custom_mode.mode</summary><div>

```json-doc
custom_mode = 
{
    label = "Custom Mode"
    version = "1.0.0"

    pane_types = []


    layout = 
    {
        vertical = true
        center_view = 
        {
            horizontal = true
            children = 
            [    
                {
                    size = 10
                    vertical = true
                    children = 
                    [
                        {
                            size = 20
                            class_name = "SceneHierarchyWindow"
                        }
                        {
                            size = 80
                            class_name = "InspectorWindow"
                        }
                    ]
                }       
                {
                    size = 70
					vertical = true
                    children = 
                    [
						{
							size = 80
		                    tabs = true
							children = 
							[
								{
									class_name = "SceneView"
								}
								{
									class_name = "GameView"
								}
							]
						}
                    ]
                }
                {
                    size = 20
                    vertical = true
                    children = 
                    [
                        { class_name = "QuickSearch" }
                    ]
                }
            ]
        }
    }

    menus = [
        {
            name = "File"
            children = [
                { name = "Exit" }
            ]
        }
        {
            name = "Custom Mode"
            children = [
                {
                    name = "Return to Unity"
                    command_id = "Commands/Return to Unity"
                }
            ]
        }
        {
            name = "Window"
            children = [
                {
                    name = "Return to Unity"
                    command_id = "Commands/Return to Unity"
                }
            ]
        }
    ]

    capabilities = {
        // Never save this mode as the current
        remember = false

        // Update the layout when switching to safe mode.
        layout_switching = true
        layout_window_menu = false
        status_bar_extra_features = false
        allow_asset_creation = false
        
        playbar = false
        layers = false
    }
}
```

</div></details>

この状態でImmediate Windowで確認すると読み込まれていることがわかります。
正常に読み込まれていない場合はアセンブリ更新時にパースエラーが出ているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133834/0773dcd0-535e-4dd4-b188-cecd62757f66.png)


# モードファイル各要素説明

## トップ階層
```json-doc
custom_mode = { }
```
トップの階層。``custom_mode``の部分が後ほどスクリプトから指定する際のIDになる。

## 表示名
```json-doc
label = "Custom Mode"
```
``ModeService.modeNames``で取得する際の名前

## レイアウト
```json-doc
layout = { }
```
エディタのウィンドウレイアウトを記述する階層

### 向き

```json-doc
// 縦
vertical = true

// 横
horizontal = true
```
これ以降の要素はその要素内に更にこの指定が無い限りはここで指定した向きで並べられる。
``EditorGUILayout.BeginVertical()``みたいなイメージ

### 場所
```json-doc
// 再生バーがある領域
top_view = { }

// 再生バー以下の領域
center_view = { }
```

### 子要素
```json-doc
children = [ ]
```
この中に複数表示ウィンドウを指定することができる。入れ子可能

### サイズ
```json-doc
// 比率指定
size = (0 ~ 100)

// 最低サイズをピクセル指定
min_width = 1024
min_height = 768
```
そのウィンドウや領域のサイズ。

### タブ移動可否設定
```json-doc
tabs = true
```

### 表示ウィンドウ指定
```json-doc
// シーンビューを表示
class_name = "SceneView"
```
表示対象のEditorWindow継承クラス名。
自作のWindowでもここに書けば表示できる。

## メニューバー
```json-doc
menus = { }
```
メニューバーの各要素を記述する階層

### メニュー名
```json-doc
name = "File"
```
メニューアイテムの名前

### 子要素
```json-doc
children = [ ]
```
この中に更にメニューの子要素を入れることができる。入れ子可能

### コマンド
```json-doc
command_id = "Commands/Return to Unity"
```
メニューアイテム実行時に呼び出すコマンドを指定する。後述

### セパレーター
```json-doc
{ name = "Undo" }
{ name = "Redo" }
null
{ name = "Copy" }
{ name = "Paste" }
```
要素の間に``null``を入れることによって、そこはセパレーターになる

## 機能設定
```json-doc
capabilities = { }
```
各機能の設定を記述する階層

### レイアウト切り替え
```json-doc
layout_switching = true
```
無効の場合はtop_viewとメニューバーのみ切り替わる

### PlayMode
```json-doc
playbar = false
```
再生・一時停止・ステップのボタン群の表示/非表示切り替え

### Layer
```json-doc
layers = false
```
レイヤドロップダウンの表示/非表示切り替え


# モードをスクリプトから変更する

CustomModeEditor.csファイルの中に例として以下のように記述する

```CustomModeEditor.cs
using UnityEditor;

// ReSharper disable once CheckNamespace
namespace CustomEditorMode.Editor
{
    public static class CustomModeEditor
    {

        [MenuItem("Custom Mode/Enable")]
        public static void EnableCustomMode()
        {
            ModeService.ChangeModeById("custom_mode");
        }

        [CommandHandler("Commands/Return to Unity", CommandHint.Menu | CommandHint.Validate)]
        private static void DisableCustomMode(CommandExecuteContext context)
        {
            ModeService.ChangeModeById("default");
        }
    }
}
```

## モード変更
``ModeService.ChangeModeById("mode id")``を使用し、前述した.modeファイルのidを指定する。
ここでは例としてメニューから呼び出すようにしている。

## コマンド
``CommandHandler``アトリビュートをコマンドで発火させたい関数に付与し、idを前述した.modeファイルのcommand_idにする。
この際、関数の引数にCommandExecuteContextを入れるのを忘れないように。(ただし使用されているところは見つからなかった)

# 余談
[ModeService](https://github.com/Unity-Technologies/UnityCsReference/blob/e740821767d2290238ea7954457333f06e952bad/Editor/Mono/EditorMode/ModeService.cs)の実装に関してはUnityCsReferenceにあるほか、色々とリポジトリ内探索をしていると新しい発見があるので面白いと思う。

最近は非エンジニアでもUnityで作業する場面が増えてきたように思うので、こういった機能を用いて専用の編集レイアウトを組んであげることで初手の情報量の多さを軽減できそう。
また、プロジェクト構造が決定されているのであればProjectWindowを表示させないことでプロジェクトが汚れるのを防ぐなど使い道はありそう。