【Unity拡張】ツールを作って得たエディター拡張系Tipsいろいろ

#はじめに
先日、EasyScriptTesterというツールをGitHubにて公開しました。
https://github.com/rngtm/EasyScriptTester

今回はこのツールを作っているときに得たエディター拡張系Tipsのうち、あまり知られていなさそうだと思ったものをピックアップしてご紹介したいと思います。

Unityのバージョンは**Unity5.5.0b7**です

#Tips
今回の記事では以下をご紹介します。
・EditorWindowからMonoBehaviourにコルーチンを実行させる
・外部エディタでスクリプトファイルを開く
・GameObjectにアタッチされているすべてのコンポーネントを取得する
・MonoScriptがEditorフォルダ以下のものかどうかを判別する
・Listから直接ReorderableListを作成する
・ReorderableListの表示サイズを変える
・ReorderableListのプラス・マイナスボタンの位置を変える

##EditorWindowからMonoBehaviourにコルーチンを実行させる

Unityのコルーチンは通常、以下のように実行することができます。

```NewBehaviourScript.cs
using System.Collections;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour 
{
	void Start()
	{
		// コルーチン実行
		this.StartCoroutine(this.TestCoroutine());
	}
	IEnumerator TestCoroutine()
	{
		yield return null;
	}
}
```

しかし、```StartCoroutine()```はMonoBehaviour内のメソッドなので、EditorWindowからはコルーチンを実行させることができません。

そこで、以下のような方法でEditorWindowからMonoBebaviourのコルーチンを実行させることができます。
step 1: リフレクションを使ってコルーチンを取り出す
step 2: リフレクションを使ってStartCoroutineメソッドを取り出す.
step 3: コルーチンをInvokeしてその結果をStartCoroutineへ渡してInvokeする.

サンプル
https://gist.github.com/rngtm/3329ecdf550da153046603ce535c5fd4
###step 1: リフレクションを使ってコルーチンを取り出す
```Test.cs
var component = オブジェクト.GetComponent(オブジェクトのタイプ);

// メソッドを取り出す
var flag = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var method = typeof(NewBehaviourScript).GetMethod(メソッド名, flag); 
```

###step 2: リフレクションを使って```StartCoroutine()```を取り出す
```Test.cs
var argumentTypes = new Type[] { typeof(IEnumerator) }; 

// StartCoroutineを取り出す
var startCoroutine = オブジェクト.GetType().GetMethod("StartCoroutine", argumentTypes);
```

###step 3: コルーチン実行
```Test.cs
// メソッドを実行して実行結果のIEnumeratorを取得
var coroutine = method.Invoke(component, parameters); 

// コルーチン実行
startCoroutine.Invoke(component, new object[] { coroutine });
```

※ゲーム停止中にコルーチンを実行しても正しく動かないので注意してください

##スクリプトファイルを外部エディタで開く
MonoScriptを```AssetDatabase.OpenAsset```メソッドに渡して実行すると外部エディタでソースコードを開くことができます。

```NewBehaviourScript.cs
/// <summary>
/// スクリプトを外部エディタで開く
/// </summary>
private static void OpenInEditor(string scriptName, int scriptLine)
{
    string[] paths = AssetDatabase.GetAllAssetPaths();
    foreach (string path in paths)
    {
        string fileName = System.IO.Path.GetFileNameWithoutExtension(path);
        if (fileName.Equals(scriptName))
        {
            MonoScript script = AssetDatabase.LoadAssetAtPath(path, typeof(MonoScript)) as MonoScript;
            if (script != null)
            {
                if (!AssetDatabase.OpenAsset(script, scriptLine))
                {
                    Debug.LogWarning("Couldn't open script : " + scriptName);
                }
                break;
            }
            else
            {
                Debug.LogWarning("Couldn't open script : " + scriptName);
            }
            break;
        }
    }
}
```

使用例. 
以下のコードを実行するとNewBehaviourScript.csが開かれてカーソルがソースコードの10行目へ移動します。

```NewBehaviourScript.cs
OpenInEditor("NewBehaviourScript", 10);
```

## GameObjectにアタッチされているすべてのコンポーネントを取得する.
以下のコードでGameObjectにアタッチされているすべてのコンポーネントを取得することができます。

```NewBehaviourScript.cs
private static IEnumerable<Type> GetAllComponents(GameObject target)
{
    var monoScripts = Resources.FindObjectsOfTypeAll<MonoScript>();
    foreach (var ms in monoScripts)
    {
        var cls = ms.GetClass();
        if (cls == null) { continue; }
        if (cls.IsSubclassOf(typeof(MonoBehaviour)) && target.GetComponent(cls) != null)
        {
            yield return cls;
        }
    }
}
```

##MonoScriptがEditorフォルダ以下のものかどうかを判別する
Type.Module.Nameを見るとそのMonoScriptがEditorフォルダの中にあるかどうかを判別することができます。

```NewBehaviourScript.cs
/// <summary>
/// MonoScriptのタイプがエディタスクリプトならtrueを返すメソッド
/// </summary>
private static bool IsEditorScript(System.Type type)
{
    return type.Module.Name == "Assembly-CSharp-Editor.dll";
}
```

これを利用することでプロジェクト内のEditorスクリプトだけを列挙する、などといったことも可能です。

```Editorフォルダ以下に存在するすべてのスクリプトを表示するサンプル.cs
/// <summary>
/// プロジェクト内のEditorフォルダ以下に存在するすべてのスクリプトを表示
/// </summary>
[MenuItem("Test/Show EditorScripts")]
static void GetEditorScripts()
{
    var monoScripts = Resources.FindObjectsOfTypeAll<MonoScript>();
    foreach (var ms in monoScripts)
    {
        var classType = ms.GetClass();
        if (classType != null && IsEditorScript(classType))
        {
            // Consoleへスクリプト名を出力
            Debug.Log(ms.name);
        }
    }
}
```

#Listから直接ReorderableListを作成する

<img width = 400 src = "https://qiita-image-store.s3.amazonaws.com/0/44288/d3e135b6-4656-0c63-6f97-016216c41b5a.png">

サンプル
https://gist.github.com/rngtm/8a2e1b0421fa5b370f8ecba17fd518ed

カスタムエディターでserializedObjectからReorderableListを作成する方法はよく見かけますが、この方法はEditorWindowでは使うことができません。

```serializedObjectからReorderableListを作成する例.cs
list = new ReorderableList (serializedObject, serializedObject.FindPropertyRelative(配列やリストの名前);
```

EditorWindowからReorderableListを利用したい場合は以下のようにListからReorderableListを作成します。

```Test.cs
List<int> list = new List<int>();
var reorderableList = new ReorderableList(list, typeof(int));
```

ListからReorderableListを作成した場合、内部のserializedPropertyがnullになるので、代わりにreorderableList.listを参照します。

```Test.cs
reorderableList.list[index] = EditorGUI.IntField(rect, "Element " + index , (int)reorderableList.list[index]);
```

#ReorderableListの表示サイズを変える
reorderableList.DoList()を呼ぶとReorderableListの表示サイズを変更することができます。

```Test.cs
var height = this.reorderableList.headerHeight + this.reorderableList.elementHeight * this.reorderableList.count+ this.reorderableList.footerHeight + 8f;
var rect = GUILayoutUtility.GetRect(0f, height);
rect.x += 28f;
rect.width -= 40f;
this.reorderableList.DoList(rect);
```

<img width = 400 src = "https://qiita-image-store.s3.amazonaws.com/0/44288/d50d16a5-058c-8218-675e-6f45ea382e35.png">

<br>
reorderableList.DoLayoutList()を呼んだ場合は以下のような表示になります。
<img width = 400 src = "https://qiita-image-store.s3.amazonaws.com/0/44288/88880fd8-fed0-079c-b310-6b2a9b1c29e9.png">

#ReorderableListのプラス・マイナスボタンの位置を変更する

<img width = 400 src = "https://qiita-image-store.s3.amazonaws.com/0/44288/4101ea3f-4f0e-f2b7-df53-ad3e8b80ee37.png">

サンプル
https://gist.github.com/rngtm/49383fd0ea39a66b06d5074691eeffbb

###プラスボタン・マイナスボタンのy座標を0にしてみる
以下はプラスボタン・マイナスボタン(＝フッター)のy座標を0にするサンプルコードです

```フッターのy座標を0にするサンプル.cs
// フッター
list.drawFooterCallback = (rect) =>
{
    rect.y = 0f;
    ReorderableList.defaultBehaviours.DrawFooter(rect, list);
};
```
```ReorderableList.defaultBehaviours.DrawFooter(rect, list)```を呼ぶことで、フッター(プラスボタン・マイナスボタン)をrectの部分へ描画させることができます。


###プラスボタン・マイナスボタンをヘッダーに合わせてみる
以下はフッターの位置をヘッダーに重ねるサンプルコードです

```フッター位置をヘッダーに合わせるサンプル.cs
// ヘッダー
Rect headerRect = default(Rect);
list.drawHeaderCallback = (rect) =>
{
    headerRect = rect;
    EditorGUI.LabelField(rect, "ヘッダー");
};

// フッター
list.drawFooterCallback = (rect) =>
{
    rect.y = headerRect.y + 3f;
    ReorderableList.defaultBehaviours.DrawFooter(rect, list);
};
```
ReorderableListにはヘッダーのrectを取得する機能が用意されていないようなので、drawHeaderCallback内で強引にrectを取得しています。