「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化する

[PONOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ponos)の5日目の記事です。

昨日は@nimitsuさんの[GameLift RealTimeServerで遊んでみよう for Unity（AWS設定編）](https://qiita.com/nimitsu/items/61cf2b177c10b59ebd3f)でした。

## はじめに

本記事は[PONOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ponos)の2日目の記事である[「MonKey - Productivity Commands」のコマンド操作でUnity開発を効率化する](https://qiita.com/e73ryo/items/42758bb6c1f33de3adfa)の続編となります。
![assetstore.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/b2152d8c-ab90-e881-a6ab-c617bb91c923.png)
[MonKey - Productivity Commands - Asset Store](https://assetstore.unity.com/packages/tools/utilities/monkey-productivity-commands-119938)

前回はMonKeyを使用してどんな事ができるのか、導入について書かせてもらいました。
今回は少し発展して、カスタムコマンドの作り方についてまとめたいと思います。

[](https://sites.google.com/view/monkey-user-guide/command-creation)

MonKeyには予め130超もの豊富なコマンドが用意されており、様々な処理をコマンドから実行できますが、それらのコマンドだけではそのプロジェクト特有の処理まではサポートできません。
カスタムコマンドを利用することにより、プロジェクト独自の処理についてもコマンド化し操作時間を短縮することができるので、MonKeyを有効利用したいと思っている方は参考にしてみてください。

なお、

* Unity 2019.2.12f1
* MacOS 10.14.6

の環境で動作確認しています。

## カスタムコマンド

`MonKey.Command`属性をstaticメソッドに付与することで、そのメソッドをコマンド化することができます。
なお、スクリプトは`Editor`フォルダ以下に存在している必要がありますのでご注意ください。

```cs
using MonKey;

public static class SampleCommands
{
    [Command("Sample Command 01")] // 引数にMonKey上のコマンド名を指定する
    static void SampleCommand01()
    {
        Debug.Log($"コマンド\"SampleCommand01\"を実行したログです。");
    }
}
```
![SampleCommand01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/6c6f8ed2-a99e-d732-31e3-3406567aa726.png)
MonKeyのコマンドパレット上に表示されました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/87d35b2e-3f30-0de5-7967-3c83c2f08c52.png)
コマンドを実行すると、メソッド内に記述した処理が実行されています。
このように、メソッドをMonKeyのコマンド化するのは非常に簡単です。

## 使用例：選択中のGameObjectの階層パスをコンソールに出力する

さて、ここからはより実用的なカスタムコマンドを作成してみます。
今回作成するのは「選択中のGameObjectの階層パスをコンソールに出力する」コマンドです。
![Example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/8b08d33c-577b-0704-d528-369d015ae0f0.png)
例えば、上のようなHierarchyで実行した場合には「Parent/SecondSon/Grandchild/Great-grandson」とコンソールにログ出力する挙動となります。

『SecondSon』の下にある『Grand-grandson』のことを他の人に伝えたいとき、Hierarchyの階層のパスを提示できれば、『EldestSon』の下にいるGrand-grandsonを誤って参照しまうことを防止できます。

### コマンドの基本情報を設定する

前項で作成した`Sample Command 01`ですが、他のコマンドと異なり「ヘルプ」や「短縮されたコマンド名」が表示されておらず、実際に使うには不便でした。
今回はそれらコマンドに関する基本情報をちゃんと設定してみましょう。

`MonKey.Command`属性には上記の「ヘルプ」や「短縮されたコマンド名」のようなコマンドの基本情報を設定するためのプロパティが用意されています。

**MonKey.Command属性のプロパティ（抜粋）**

| プロパティ | 用途 |
| :-- | :-- |
| Name | コマンド名。コマンドの検索に用いられる。 |
| QuickName | 短縮されたコマンド名。<br/>3文字以内に収めることを推奨されている。 |
| Help| コマンド名の下に表示されるヘルプ。<br/>1行より長くなるとフォーマットが完全ではない可能性がある。 |

実際にこれらのプロパティを使用して「ヘルプ」と「短縮されたコマンド名」が設定された`Sample Command 02`を作成します。

```cs
[Command(
    "Sample Command 02",
    QuickName = "SC2",
    Help = "選択中のGameObjectの階層パスをコンソールに出力する"
)]
static void SampleCommand02()
{
}
```
![SampleCommand02_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/31a24733-fadf-90c3-fa84-cd4ba420b5c4.png)
設定した「ヘルプ」と「短縮されたコマンド名」がコマンドパレット上に表示されていることがわかります。
また、短縮されたコマンド名である「sc2」を入力しただけで`Smaple Command 02`が候補として表示されます。

### コマンドから選択されているGameObjectを参照する

次に、実際に選択中のGameObjectを参照してコンソールに出力する、というコマンドの処理部分を実装してみましょう。
現在選択されているGameObjectを参照するためには、通常エディタ拡張で使用している`UnityEditor.Selection`ではなく、MonKeyから提供されている`MonKey.Editor.MonkeyEditorUtils.OrderedSelectedGameObjects`を使用します。

(なお、選択されたGameObjectではなく、選択されたTransformを取得することのできる`MonKey.Editor.MonkeyEditorUtils.OrderedSelectedTransform`も用意されています)

`IEnumerable<GameObject>`で返ってくるので、foreachで列挙し処理していきます。

選択中のGameObjectを取得し、そのパスを出力するコードは以下になります。

```cs
using MonKey.Editor;

...

// OrderedSelectedGameObjectsから選択中のGameObjectを列挙できる。
foreach (var selectedGameObject in MonkeyEditorUtils.OrderedSelectedGameObjects)
{
    // 親のGameObjectの名前を手前に連結していく。
    var pathBuilder = new System.Text.StringBuilder(selectedGameObject.name);
    var parent = selectedGameObject.transform.parent;
    while (parent != null)
    {
        pathBuilder.Insert(0, parent.name + "/");
        parent = parent.transform.parent;
    }

    Debug.Log(pathBuilder.ToString());
}
```
![SampleCommand02_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/799ed51b-2781-340c-10cb-78d7f04a5968.png)
この選択状態でコマンドを実行すると、
![SampleCommand02_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/3e69548c-dc59-3482-8596-3156698be91c.png)
このように、各GameObjectの階層をパスとして出力することができました。

### コマンドを実行できる条件を設定する

現在の`Sample Command 02`は、GameObjectを全く選択していなくてもコマンドを実行することができます。

選択していない状態で実行したとしても`MonkeyEditorUtils.OrderedSelectedGameObjects`の要素が0で返ってくるだけなのでエラーが発生することはありませんが、それではコマンドを実行する意味がないため、「GameObjectを選択している状態でのみ」実行できるように、検証条件を設定してみましょう。

コマンドの検証条件は`MonKey.Command`属性の`DefaultValidation`プロパティで設定します。
`DefaultValidation`には`MonKey.DefaultValidation`列挙体の値が使用できます。

**MonKey.DefaultValidation列挙体（抜粋）**

| 値 | 用途 |
| :-- | :-- |
| AT_LEAST_ONE_GAME_OBJECT | 1個以上のGameObjectを選択している。 |
| AT_LEAST_TWO_GAME_OBJECTS | 2個以上のGameObjectを選択している。 |
| IN_PLAY_MODE | プレイモード中である。 |
| IN_EDIT_MODE | プレイモード中ではない。 |

今回は、「GameObjectを選択している状態でのみ」実行できるようにしたいので、`MonKey.DefaultValidation.AT_LEAST_ONE_GAME_OBJECT`を指定します。

```cs
[Command(
    "Sample Command 02",
    QuickName = "SC2",
    Help = "選択中のGameObjectの階層パスをコンソールに出力する",
    DefaultValidation = DefaultValidation.AT_LEAST_ONE_GAME_OBJECT
)]
static void SampleCommand02()
{
```
![SampleCommand02_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/67e5de36-4246-d8b5-eaaf-85b4946d11c3.png)
GameObjectを選択していない状態でコマンドを選択すると…
![SampleCommand02_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/89a323ae-2ca7-0d1e-754d-bfd6450db416.png)
ご覧のように<font color=orange>**「Select at least one GameObject」**</font>の警告が表示され、コマンドを実行することができません。

コマンドの製作者が自身だけで使用する分には使い方を気をつければいいので、検証条件の定義は必須ではありませんが、他者へ共有する予定がある場合は検証条件を正しく定義して正しい状況で使用してもらえるようにしたいですね。

## おわりに

カスタムコマンドを作成してMonKeyが実行できる機能を充実させることで、Unity上の作業を更に効率化することができます。

なお、Unityエディタ上からstaticメソッドを実行するには`UnityEditor.MenuItem`属性でメニューアイテム化する、という方法もありますが、MonKeyなら処理の実行条件の検証を行ったり実行時に引数を与えることができるので、メニューアイテムとして実行するよりも柔軟な処理の実行が可能となります。
そういった用途で実行したい処理がある場合にはMonKeyのカスタムコマンド作成を検討してみてください。

さて、[PONOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ponos)は6日目も続けて私@e73ryoが担当する予定です。
次回は引数を与えてカスタムコマンドを実行する方法について紹介したいと思います。