変数の値で検索できるコンポーネント検索ツールを作った話

この記事は [KLab Advent Calendar 2018](https://qiita.com/advent-calendar/2018/klab) 13日目の記事です。
# はじめに
みなさんは既にリリースされた処理に手を入れた際に、影響範囲の洗い出しをどのように行っていますか？
ソースコード内で参照されているのであれば、IDEの参照検索機能で確認できますが
Prefabなどにアタッチされているコンポーネントを検索するのは少し手間です。

1. 検索したいコンポーネントの.metaの中に定義してあるguidをコピー
2. grep -inr [検索したいコンポーネントのguid]

でコンポーネントを参照している箇所の列挙だけはできるのですが、
実際の利用状況（非アクティブ化されているかなど）まではわかりません。

そこで今回は、プロジェクト内から利用箇所を取得したうえでコンポーネントの値設定状況で絞り込めるEditor拡張を作ってみました。

その過程で学びがあった事に関していくつか書き残しておこうかと思います。

# 作ったもの
<img src="https://qiita-image-store.s3.amazonaws.com/0/74151/90d4f76f-9c95-950c-af13-28765e2b1620.gif" width="1000">


* 検索設定の編集
* 検索結果の一覧表示
* 選択オブジェクトの詳細表示

で構成されています。

## 入力された文字列からTypeを取得する

```c#
public static Type GetTypeFromAssembly(string TypeName)
{
	var type = Type.GetType(TypeName);

	if (type != null)
	{
		return type;
	}

	if (TypeName.Contains("."))
	{
		var assemblyName = TypeName.Substring(0, TypeName.IndexOf('.'));

		try
		{
			var assembly = Assembly.Load(assemblyName);
			if (assembly == null)
			{
				return null;
			}

			type = assembly.GetType(TypeName);
			if (type != null)
			{
				return type;
			}
		}
		catch (System.IO.FileNotFoundException)
		{
		}
	}

	var currentAssembly = Assembly.GetExecutingAssembly();
	var referencedAssemblies = currentAssembly.GetReferencedAssemblies();
	foreach (var assemblyName in referencedAssemblies)
	{
		var assembly = Assembly.Load(assemblyName);
		if (assembly != null)
		{
			type = assembly.GetType(TypeName);
			if (type != null)
			{
				return type;
			}
		}
	}

	return null;
}
```
* `Type.GetType(TypeName)`
    * 自作クラスが取得できます
* `assembly.GetType(TypeName)`
    * UnityEngine.UI.TextやUnityEditor.EditorWindow
など他のアセンブリに定義されているクラスが取得できます。


## 変数値判定用の検索フィルターを表示する
![set.PNG](https://qiita-image-store.s3.amazonaws.com/0/74151/8613d750-54d1-ffe0-c634-c188adfd55fd.png)

```c#:フィルター要素の初期化
void InitFilterFieldInfo()
{
	if (Settings.Instance.Type == null)
	{
		return;
	}

	var type = Settings.Instance.Type;
	var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
 
	var propertys = type.GetProperties()
		.Where(p => p.CanWrite)
		.Select(p => new FilterFieldInfo(p.Name, p.PropertyType, null));

	var fields = type.GetFields(flags)
		.Where(f => f != null)
		.Where(f =>
		{
			var isValid = f.IsPublic;
			isValid |= f.GetCustomAttributes(typeof(SerializeField),true).Length > 0;
			return isValid;
		})
		.Select(f => new FilterFieldInfo(f.Name, f.FieldType, null));
	
	Settings.Instance.FilterParameters = propertys
		.Union(fields)
		.Where(x => !Settings.Instance.ignoreFilterField.Contains(x.FieldName))
		.ToArray();
}

```
検索対象に指定されているTypeのPropertyとpublicなFieldを取得し、
フィルター制御用のクラスに変換し保持します。

```c#:public,[SerializeField]なフィールドの取得
var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

var isValid = f.IsPublic;
isValid |= f.GetCustomAttributes(typeof(SerializeField),true).Length > 0;
```
BindingFlags.NonPublicを指定することで、引数なし`GetFields()`＋privateなフィールドを取得できます。

そのうえで、SerializeField属性を持っているかの判定を行っています。

---

```C#:制御用class
public class FilterFieldInfo
{
	public string FieldName { get; private set; }

	public Type Type { get; private set; }

	public object Value { get; private set; }

	public bool IsActive { get; private set; }

	public void OnGUI()
	{
		EditorGUILayout.BeginHorizontal();
		IsActive = EditorGUILayout.Toggle(IsActive, GUILayout.Width(30));
		if (Type == typeof(int))
		{
			int val = Value == null ? 0 : (int)Value;
			Update(EditorGUILayout.IntField(FieldName, val));
		}	
        /// ~~~　中略　~~~	///
		else if (Type.IsEnum)
		{
			object val = Value == null ? System.Enum.GetValues(Type).GetValue(0) : Value;
			Update(EditorGUILayout.EnumPopup(FieldName, (System.Enum)val));
		}
		else if (Type.IsClass)
		{
			Object val = Value == null ? null : (Object)Value;
			Update(EditorGUILayout.ObjectField(FieldName, val, Type, true));
		}
		else
		{
			Debug.LogWarning(string.Format("Type : {0}: {1}", Type, FieldName));
		}
		EditorGUILayout.Space();
		EditorGUILayout.EndHorizontal();
	}
}

```
項目表示に必要な情報と、検索条件にするかどうかのフラグを持たせています。
OnGUI内で型に応じた表示処理定義し、EditorWindowクラスから呼び出しています。


## コンポーネントがアタッチされている全てのオブジェクトを検索する
```c#:Project内から全取得する
void CacheObjects()
{
	if (HasCache)
	{
		return;
	}

	var paths = GetAssetPaths();
	
	var assetPaths = paths
		.Where(p => Settings.Instance.Extensions.Contains(System.IO.Path.GetExtension(p)))
		.ToArray();
	var scenePaths = paths
		.Where(x => x.EndsWith(".unity"))
		.ToArray();

	for (int i = 0; i < scenePaths.Length; i++)
	{
		EditorSceneManager.OpenScene(scenePaths[i], OpenSceneMode.Additive);

		string str = string.Format("{0} : {1}/{2}", scenePaths[i], i + 1, scenePaths.Length);
		EditorUtility.DisplayProgressBar("Sceneの展開中", str, scenePaths.Length / Mathf.Max(1, i));
	}

	EditorUtility.ClearProgressBar();

	cacheArray = assetPaths.SelectMany(path => AssetDatabase.LoadAllAssetsAtPath(path))
		.Concat(UnityEngine.Resources.FindObjectsOfTypeAll<GameObject>())
		.ToArray();
		
	HasCache = true;
}

```
Prefabはpathを元に`AssetDatabase.LoadAllAssetsAtPath(path)`を行うことで取得できます。
しかし、Scene内のオブジェクトの情報を取得するためにはそのSceneを開かなければならず
1. .unityファイルのアセットパスの取得
2. `EditorSceneManager.OpenScene(path, OpenSceneMode.Additive)`でSceneを開く 
3. `UnityEngine.Resources.FindObjectsOfTypeAll<GameObject>()`でオブジェクトを取得
といった手順をとる必要があります。

※OpenSceneMode.Additiveで開かなければ参照を保持できません

## 取得したインスタンスをGUI表示用クラスに変換する

```c#:GUI表示用class
public struct BehaviorFields
{
	public string Name;
	public string Tag;
	public bool Enabled;
	
	public BehaviorFields(Behaviour behaviour)
	{
		Name = behaviour.name;
		Tag = behaviour.tag;
		Enabled = behaviour.enabled;
	}

	public BehaviorFields(Component component)
	{
		Name = component.name;
		Tag = component.tag;
		Enabled = true;
	}
}

public ComponentData(UnityEngine.Object obj)
{
	this.Obj = obj;
	
	var b = obj as Behaviour;
	Behaviour = new BehaviorFields(b ?? (obj as Component));
	
	AssetPath = AssetDatabase.GetAssetOrScenePath(obj);
	this.IsSceneObject = AssetPath.EndsWith(".unity");
	
	// e.g. Text =>  Canvas\Button\Text
	TransformPath = GetTransformPath();
	AffiliationName = AssetPath.Split(new char[] { '/' }).LastOrDefault() ?? "";

	var color = ColorUtility.ToHtmlStringRGB(Settings.Instance.ResultTextColor);
	label = string.Format("[Name] <color=#{0}>{1}</color> | [AssetPath] <color=#{0}>{2}</color>", color, ObjectName, AssetPath);

	Settings.OnUpdateSetting += name => { guiCacheExpired = true; };
}
```
---
こちらのクラスは検索結果の数だけ生成されるため、大規模なプロジェクトの場合検索実行時・表示時に負荷をかける原因になっていました。
負荷を軽減するために行った対応は以下の通りです。
1. 文字列の結合処理を更新処理内では行わないようにする
2. Colorをnewしないようにする（予め定義しておく）
3. 表示判定を行うのは表示条件が変わった時だけにする（判定結果のキャッシュ）

これらを行うことで処理速度が15分から10秒ほどまで改善されました。

## フィルターで指定された値を持っているかを判定する(表示判定)

```c#:フィルターの値と、オブジェクトに設定されている値の一致判定
bool CheckConditionsSatisfied()
{
	// ーーーー略ーーーー

	foreach (FilterFieldInfo filter in Settings.Instance.FilterParameters)
	{
		SerializedObject so = new SerializedObject(Obj);
		var property = so.FindProperty(filter.FieldName);
		if (property != null)
		{
			if (property.type == "int")
			{
				return (int)filter.Value == property.intValue;
			}
			// ーーーー略ーーーー
			else if (property.type == "Enum")
			{
				return (int)filter.Value == property.enumValueIndex;
			}
			else
			{
				if (filter.Value == null && property.objectReferenceValue == null)
				{
					return true;
				}
				else if (filter.Value == null || property.objectReferenceValue == null)
				{
					continue;
				}

				var obj = Convert.ChangeType(filter.Value, filter.Type);
				var obj2 = Convert.ChangeType(property.objectReferenceValue, filter.Type);
				return obj.GetHashCode() == obj2.GetHashCode();
			}
		}
	}

	return false;
}
```
SerializedPropertyのValueが型毎に違う変数に入っているので辛い感じになっています。
一致判定を`==`でやってしまっていますが、範囲判定に変えた方が良かったかもしれません。
参照型の判定は、`Convert.ChangeType(value, type)` で変換した後にHashの比較で行っています。

# おわりに
普段Editor拡張でコンポーネントの情報を取得するときは
SerializedObjectやSerializedPropertyを使用してよろしくやることが多いので、
型情報からGUI表示するのは手間がかかりました。

今回の実装で初めて触るAPIも多くあったので、もっと色々触って精進してきたいと思います。
 
# 参考
[Unity のエディタ拡張で FoldOut をかっこよくするのをやってみた](http://tips.hecomi.com/entry/2016/10/15/004144)
[UnityBulkConverter](https://github.com/wotakuro/UnityBulkConverter)
[Type.GetType(string) does not work in Unity](https://answers.unity.com/questions/206665/typegettypestring-does-not-work-in-unity.html)
[リフレクションのBindingFlagsは一旦これだけ覚えておこう](https://www.shibuya24.info/entry/2017/02/07/093000)