Unity Editor上で動く自作ツールについて 〜 TIPS/豆知識を添えて 〜


## はじめに
開発中の自作ツールを紹介します。
簡単に説明すると
###「 Unity のスクリプト/プラグインを別のスクリプト/プラグインに置換作業をする際に、置換対象オブジェクトがどれくらいあるのかを調査、そして置換作業を補助するためのツール」
です。
## きっかけ
本ツールを作成するきっかけは、とあるプロジェクトで2D表現を2つのプラグインを組み合わせて表現していました。とある日、片方のプラグインだけでも事が足りる事が分かったので置換する事にしました。ただ、この置換作業はかなり大変でした。
実際にやった置換作業の大まかな流れは以下の通りです。

### 1.Project内の全てのプレハブ, 全シーンのHierarchyに置かれているオブジェクトが
###　置換対象プラグイン/スクリプトを持っているか調査する作業
### 2.置換対象オブジェクトをリストにまとめる作業
### 3.置換作業

これらの作業をサポートする目的で作成したツールが、今回紹介するツールです。
今回は調査部分のみを紹介します。

## 自作ツールの機能紹介

![ConfigWindow](https://dl.dropboxusercontent.com/u/55795545/Qiita/ConfigWindow2.png)

調査の流れは下記の通りです。

1. 置換対象プラグイン/スクリプトを登録
2. シーン内にある全オブジェクトを取得
3. 置換対象プラグイン/スクリプトを持っているシーン内のオブジェクトがないか調査

調査の流れに合わせて, スクリプトではどう動かしているのかを書いていきます。

### 1. 置換対象プラグイン/スクリプトを登録

ユーザは, 置換対象プラグイン/スクリプトを登録するために, プラグイン/スクリプトを赤枠で囲まれている場所に Drag & Drop (以下 D&D)します。

![replace](https://dl.dropboxusercontent.com/u/55795545/Qiita/ObjectField2.png)

D&Dすると登録が完了し、下記の図のように表示されます。
![replace](https://dl.dropboxusercontent.com/u/55795545/Qiita/ObjectField3.png)

置換対象プラグイン/スクリプトを複数登録した状態が下記の図です。

![replace](https://dl.dropboxusercontent.com/u/55795545/Qiita/SearchTargetComponents.png)

置換対象プラグイン/スクリプトを登録する際に、既に登録されているかどうかを判定しているコードが下記になります。

``` cs:ReplaceTargetPlugin.cs
// 置換対象プラグイン/スクリプトを入れる
targetObj = EditorGUILayout.ObjectField (targetObj, typeof(MonoScript), true);

if (targetObj) 
{
　// 置換対象プラグイン/スクリプトのリストに登録されているかチェック
　if (!searchTargetList.Contains (targetObj as MonoScript))
　{
    // 登録されていなければ置換対象プラグイン/スクリプトのリストに登録
　　searchTargetList.Add (targetObj as MonoScript);
　} else {
　　// 既に登録されているので, 登録されている事をダイアログで表示
　　var comment = string.Format ("{0}は既に登録してあります。", targetObj.name);
　　EditorUtility.DisplayDialog ("", comment, "OK");
　}
}
```

### 2. シーン内に置かれている全オブジェクトを取得

シーンごとに置かれているオブジェクトは違うため、変わる度にシーン内に置かれている全オブジェクトを取得します。

シーン内の全オブジェクトを持ってくる関数がなかったため、
###　Resources.FindObjectsByType(typeof(GameObject))
を用いてシーン内の全オブジェクトを取得しました。

Resources.FindObjectsByType は, シーン内の全オブジェクトだけでなくProject内部も調査します。
なので、シーン内のオブジェクトかどうか判定する必要があります。
今回は, 
###　AssetDatabase.GetAssetOrScenePath
を用いてシーン内のオブジェクトかどうかを判定しました。

AssetDatabase.GetAssetOrScenePathは、引数に入れたオブジェクトがProjectから取得したオブジェクトなら Asset Path を、
シーン内から取得したオブジェクトなら シーン名を返す関数です。

AssetDatabase.GetAssetOrScenePathから返ってきた文字列に ".unity" が付いているかで判定してます。

``` cs:FindAllObjectInScene.cs
// Project & Sceneにある GameObject を持つ全オブジェクトを取得
foreach(var obj in Resources.FindObjectsByType(typeof(GameObject)))
{
　// ProjectにあるものならAsset Path, SceneにあるオブジェクトはそのシーンのPathが返ってくる
　string path = AssetDatabase.GetAssetOrScenePath(obj);

　// シーンのPathは拡張子が .unity
　string sceneExtension = ".unity";
　
　// Path.GetExtension(path) で pathの拡張子を取得
　// Equals(sceneExtension)で sceneExtensionと比較
　bool isExistInScene = Path.GetExtension(path).Equals(sceneExtension);

　// シーン内のオブジェクトかどうか判定
　if(isExistInScene){ // シーンのオブジェクトを格納するリストに登録 }
　else{ // プロジェクトのオブジェクトを格納するリストに登録 }
}
```

　※ Unity4ならシーン内にあるオブジェクトかどうかの判定は, if(obj.activeInHierarchy)でも判定できます.

### 3. 置換対象プラグイン/スクリプトを持っているシーン内のオブジェクトがないか調査

 1.の作業で登録してある置換対象であるプラグイン/スクリプトのリストと, 
 2.で取得してきたシーン内の全オブジェクトのリストを使って調査します。

ポイントはGetComponentsInChildrenなのですが,
詳細は下記の TIPS / 豆知識 に書いてあります。
興味がある方はそちらをみてください。

``` cs:SearchAllGameObjectsInScene.cs
// 置換対象プラグイン/スクリプトを持っているオブジェクトがシーン内の全オブジェクトの中にないか調査
foreach(var currentAnalysisType in replaceTargetComponentTypeList)
{
　foreach(var obj in allGameObjectsInScene)
　{
　　// 置換対象プラグイン/スクリプトを持っていたオブジェクトを取得
　　var components = obj.GetComponentsInChildren(currentAnalysisType,true);
　　foreach(var com in components)
　　{
　　　//  置換対象プラグイン/スクリプトを持っていたオブジェクトを登録
　　　currentSceneComponent.Add(com.gameObject);
　　}
　}
}
```

## TIPS / 豆知識
### ・ProgressBar で調査がどこまで進んでいるのか表示
![progressbar1](https://dl.dropboxusercontent.com/u/55795545/Qiita/ProgressBar3.png) ![progressbar2](https://dl.dropboxusercontent.com/u/55795545/Qiita/ProgressBar1.png) ![progressbar3](https://dl.dropboxusercontent.com/u/55795545/Qiita/ProgressBar2.png)

調査ボタンを押したのに全く画面に動きが無い場合, 本当に動いているのか不安になります。
そこで調査がどこまで進んでいるのかを表示する際に使用した関数が、
###　EditorUtility.DisplayProgressBar
です。
使用方法は下記のコードをみてください。 
 注意としては 
###　EditorUtility.DisplayProgressBar を使用したら、
###　EditorUtility.ClearProgressBar で閉じることを忘れないようにしてください。
###　EditorUtility.ClearProgressBar を忘れると ProgressBar が表示されたままになります。 
###　表示されたままになった場合、通常終了できなくなり Unity を強制終了することになります。

``` cs:ProgressBar.cs
// ProgressBar表示
EditorUtility.DisplayProgressBar(
     "調査中",								 　　　　　// タイトル
     comment,								　　　　　 // 本文
     (float)sceneCount / (float)targetSceneNum		// ProgressBarの進行度合い
);

// 処理が終わってProgressBarを閉じる
EditorUtility.ClearProgressBar();
```
### ・シーンを切り替える際に編集中なら確認ダイアログを表示する
シーンを切り替える際は、EditorApplication.OpenScene を使用しています。
現在開いているシーンが編集中で保存していない時に、 
EditorApplication.OpenScene を使用すると確認なしでシーンを開きます。
もちろんシーンは保存されません。それでは困ります。
編集中だったら保存するかどうかを聞いてほしい。
そこで現在開いているシーンが編集中かどうかを返す関数が
###　EditorApplication.SaveCurrentSceneIfUserWantTo
です。
EditorApplication.SaveCurrentSceneIfUserWantTo は、
現在開いているシーンが編集中の場合、シーンを保存するかしないかを聞く確認ダイアログを表示します。

![ChangeScene](https://dl.dropboxusercontent.com/u/55795545/Qiita/ChangeSceneConfirmDialog.png)

この確認ダイアログで, Save または Don't Save を押すと true、
Cancel を押すと false を返します。
現在開いているシーンが編集中でないときは, true を返します。

``` cs:ChangeScene.cs
// 編集中なら保存するかしないかを確認するダイアログを表示
if (EditorApplication.SaveCurrentSceneIfUserWantsTo())
{
	EditorApplication.OpenScene(scenePath);
}
```

### ・GetComponentsInChildrenで非アクティブのオブジェクトも調査対象にする方法
GetComponentsInChildren は, 対象コンポーネントを持っているオブジェクトを全て取得する関数です。
普段引数なしの GetComponentsInChildren<Transform>() を使っていませんか？

引数なしの GetComponentsInChildren で気をつけないといけないことは,
###　非アクティブのオブジェクトは調べていない
ことです。

つまり, 非アクティブのオブジェクトが対象コンポーネントを持っていたとしても返り値には含まれていません。
非アクティブのゲームオブジェクトも調べるようにするためにはどうすればいいのか。
GetComponentsInChildrenの引数で指定することで非アクティブのオブジェクトも調べるようにできます。

``` cs:GetComponentsInChildren.cs
// これでは非アクティブのオブジェクトを取得できない
// 引数を指定しない場合にはfalseが入る
var objs = targetObj.GetComponentsInChildren<Transform>();

// 引数に true を入れると, 非アクティブのオブジェクトも取得できるようになる
var objs = targetObj.GetComponentsInChildre<Transform>(true);
```

## おわりに
今回は記事を書くきっかけを頂き、ありがとうございました。
出来る限り多くの情報をお伝えできればと思い、記事を書いていたらこんなことに・・・。
読んでくださった皆様に何かしら役に立つ情報を共有できれば幸いです。