【Unity】ちょい足しエディタ拡張で快適に！

こんにちは、G-Blossomの松下です。この記事はG-Blossomアドベントカレンダー2024の12/9の記事となります。

ご存知のように、Unityにはエディタ拡張という機能があります。
ツールバーに項目を追加することで、インポートやコンバート、実行環境や変数を設定したりと、様々な機能を追加できますが、それだけでなく、**エディタ自体の振る舞いを変更**することも可能となっており、小さな変更で作業効率が大幅に改善されることもあります。
導入も、プロジェクト内の任意のディレクトリにスクリプトを配置するだけでOKと、非常に簡単です。
今回は、軽い気持ちで作成したら想像以上に役立ったエディタ拡張を２つと、おまけを１つ紹介します。
最後にちょっとした種明かしもあるのでぜひ見ていってください。

## ヒエラルキーでのマウスオーバーで強調表示

ヒエラルキー内の項目をマウスオーバーすることで、シーン内の対応するオブジェクトを強調表示する拡張です。

![buttons.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131072/007cf065-5330-e186-e678-d98aaf4d5f00.gif)

3Dも対応しています。

![village.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131072/448273a6-89d6-0f06-00b2-dbf10cdcf8aa.gif)

項目を選択してカーソルキーで順に見ていっても似た動作になりますが、インスペクタで表示させたくない場合には特にありがたみを感じます。

```HoverHighlighter.cs
using UnityEditor;
using UnityEngine;

[InitializeOnLoad]
public class HoverHighlighter
{
	private static GameObject _highlightedObject;

	static HoverHighlighter()
	{
		EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
		SceneView.duringSceneGui += OnSceneGUI;
	}

	// ヒエラルキーウィンドウのGUIイベント処理
	private static void OnHierarchyGUI(int instanceID, Rect selectionRect)
	{
		Event e = Event.current;

		if (selectionRect.Contains(e.mousePosition))
		{
			_highlightedObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
		}
	}

	// シーンビューのGUIイベント処理
	private static void OnSceneGUI(SceneView sceneView)
	{
		if (_highlightedObject != null)
		{
			// オブジェクトにRendererがあればそのBoundsを取得（3Dオブジェクト用）
			Renderer renderer = _highlightedObject.GetComponent<Renderer>();
			if (renderer != null)
			{
				Handles.color = Color.yellow;
				Handles.DrawWireCube(renderer.bounds.center, renderer.bounds.size);
			}
			else
			{
				// RectTransformがある場合はその矩形を描画（2Dオブジェクト用）
				RectTransform rectTransform = _highlightedObject.GetComponent<RectTransform>();
				if (rectTransform != null)
				{
					DrawRectTransformHighlight(rectTransform);
				}
				else
				{
					// 上記のコンポーネントがない場合はTransformの位置に小さなマーカーを描画
					Vector3 position = _highlightedObject.transform.position;
					Handles.SphereHandleCap(0, position, Quaternion.identity, 0.5f, EventType.Repaint);
				}
			}
		}

		SceneView.RepaintAll();
	}

	// RectTransformの矩形を描画
	private static void DrawRectTransformHighlight(RectTransform rectTransform)
	{
		Vector3[] worldCorners = new Vector3[4];
		rectTransform.GetWorldCorners(worldCorners);

		Handles.DrawLine(worldCorners[0], worldCorners[1]);
		Handles.DrawLine(worldCorners[1], worldCorners[2]);
		Handles.DrawLine(worldCorners[2], worldCorners[3]);
		Handles.DrawLine(worldCorners[3], worldCorners[0]);
	}
}
```

## 空のフィールドに対する警告

実装直後やPrefabの構造を修正したとき、インスペクタ上でアタッチすべきオブジェクトが参照されていない場合があります。
複数箇所で参照されている場合など特に見落としがちで、実行中に初めてNull Referenceエラーに気付いて諸々やり直し、という経験をしたことは誰にでもあるのではないでしょうか？
この拡張によって、スクリプトにアタッチされていないフィールドがあるオブジェクトが強調表示されて非常にわかりやすくなります。

![empty.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131072/8cb9e4de-e3b1-eac5-e333-f58754e0692d.gif)

```//MissingReferenceHighlighter.cs
using UnityEditor;
using UnityEngine;

[InitializeOnLoad]
public static class MissingReferenceHighlighter
{
	static MissingReferenceHighlighter()
	{
		EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
	}

	private static void OnHierarchyGUI(int instanceID, Rect selectionRect)
	{
		GameObject obj = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
		if (obj == null) return;

		var monoBehaviours = obj.GetComponents<MonoBehaviour>();
		foreach (var mono in monoBehaviours)
		{
			if (mono == null) continue;
			
			//ユーザ作成のスクリプト以外は除外
			if (mono.GetType().Assembly.GetName().Name != "Assembly-CSharp")
			{
				continue;
			}

			var fields = mono.GetType().GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance |
			                                      System.Reflection.BindingFlags.Public);
			foreach (var field in fields)
			{
				if (field.GetCustomAttributes(typeof(SerializeField), true).Length > 0 && (field.GetValue(mono) == null || field.GetValue(mono).Equals(null)))
				{
					if (field.GetCustomAttributes(typeof(IsNullableAttribute), true).Length > 0)
					{
						continue;
					}
					EditorGUI.DrawRect(selectionRect, new Color(1f, 0.3f, 0.3f, 0.3f));
					return;
				}
			}
		}
	}
}
```

[IsNullable]アトリビュートをつけることで、アタッチされていなくても警告表示を避けることができます。
```//IsNullableAttribute.cs
using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class IsNullableAttribute : PropertyAttribute { }
```

## GameObjectの種類判別

おまけとして、アタッチされているGameObjectがシーン上のものか、プロジェクト中のPrefabかによって表示を変更するスクリプトと、Prefab以外をアタッチすると警告を表示するスクリプトも紹介します。
この例ではRock3に[PrefabOnly]アトリビュートが設定されており、かつヒエラルキー上のオブジェクトがアタッチされています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131072/92d9079f-7685-461e-ef6c-c7030a8495fd.png)

効果を発揮する場面は思ったほどないですが、あったらなんとなく嬉しい気がします。

```PrefabColorDrawer.cs
using UnityEditor;
using UnityEngine;

//GameObjectがPrefabかヒエラルキー上かで色を変える
[CustomPropertyDrawer(typeof(GameObject))]
public class PrefabColorDrawer : PropertyDrawer
{
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
	{
		GameObject obj = property.objectReferenceValue as GameObject;
		
		if (obj != null)
		{
			if (PrefabUtility.IsPartOfPrefabAsset(obj))
			{
				GUI.backgroundColor = Color.cyan;
			}
			else
			{
				GUI.backgroundColor = Color.yellow;
			}
		}
		else
		{
			GUI.backgroundColor = Color.red;
		}

		EditorGUI.PropertyField(position, property, label);
		GUI.backgroundColor = Color.white;
	}
}
```

```PrefabColorDrawer.cs
using UnityEditor;
using UnityEngine;

//GameObjectがPrefabかヒエラルキー上かで色を変える
[CustomPropertyDrawer(typeof(GameObject))]
public class PrefabColorDrawer : PropertyDrawer
{
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
	{
		GameObject obj = property.objectReferenceValue as GameObject;
		
		if (obj != null)
		{
			if (PrefabUtility.IsPartOfPrefabAsset(obj))
			{
				GUI.backgroundColor = Color.cyan;
			}
			else
			{
				GUI.backgroundColor = Color.yellow;
			}
		}
		else
		{
			GUI.backgroundColor = Color.red;
		}

		EditorGUI.PropertyField(position, property, label);
		GUI.backgroundColor = Color.white;
	}
}
```

```PrefabOnlyDrawer.cs
using UnityEditor;
using UnityEngine;

//AssetからのPrefabのみ受け付ける
[CustomPropertyDrawer(typeof(PrefabOnlyAttribute))]
public class PrefabOnlyDrawer : PropertyDrawer
{
	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
	{
		if (property.objectReferenceValue == null)
		{
			GUI.backgroundColor = Color.red;
			EditorGUI.PropertyField(position, property, label);
			GUI.backgroundColor = Color.white;
			return;
		}

		// Prefabでない場合、警告を表示
		GameObject obj = property.objectReferenceValue as GameObject;

		if (obj != null && !PrefabUtility.IsPartOfPrefabAsset(obj))
		{
			EditorGUILayout.HelpBox("Prefabをアタッチしてください", MessageType.Error);
		}

		EditorGUI.PropertyField(position, property, label);
		GUI.backgroundColor = Color.white;
	}
}
```

```PrefabOnlyAttribute.cs
using UnityEngine;

public class PrefabOnlyAttribute : PropertyAttribute { }
```

## 終わりに
汎用性が高く実用性もある機能ができてしまったので、いっそアセットストアに1＄くらいで置いてしまおうか、とも考えたのですが、一つ種明かしをすると、これらのスクリプトはChatGPTくん(4o）にベースを作成してもらって、少し修正したものがほとんどです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131072/b673888f-3c42-f49e-25bc-e66fbd6766f1.png)

開発中にちょっとした不便を感じたとき、どのように改善したいかを言語化するだけで、作業効率は大幅に改善するかもしれません。
エディタ拡張によって何ができて、何が難しいのか、そういった感覚を掴むためにも、どんどんAIを活用して、エンジニア同士で共有し、楽しく開発していけることを願っています。

これを読んだ方も、おすすめのエディタ拡張や、拡張でも解決できないちょっとした不便があれば、お気軽にコメントで教えてください！