NotNullAttributeで設定したオブジェクトをHierarchy上で確認できる機能拡張の作成

「[Applibot Advent Calendar 2021](https://qiita.com/advent-calendar/2021/applibot)」2日目の記事になります。
前日は @Sigsiguma さんの
「Unity TextMeshProにおけるダイナミックフォントの扱い」という記事でした!

# はじめに
### Unity開発中にこういった経験をしたことがありますか？
・SerializeFieldで設定したオブジェクトが参照されておらず**Unity再生中に進行不能**になってしまう
・オブジェクトを消したことによって、知らぬ間に別のオブジェクトが影響を受け、**Inspector上でMissing表示**になっている

こういったNull Reference Exceptionを見たくない人の悩みを減らすため
**オブジェクトが参照されているかどうかをHierarchy上で確認できるような**
環境を作りたいと思います。

この記事で載せているスクリプトをコピペすることで誰でも使うことができますのでぜひ導入してください！

# 概要
NotNullで設定したものを**InspectorとHierarchy上**で確認できるEditor拡張を作成します。

# 作業環境
Unity2020.3.22f1
※それ以前のバージョンでも使える場合はございます。

# 実装の流れ
1.NotNullの属性を作成
2.Inspector上で確認できる機能の作成
3.Hierarchy上で確認できる機能の作成

# 1.NotNullの属性を作成
以下のスクリプトを作成します。

```c#:NotNullAttribute.cs
using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field)]
public class NotNullAttribute : PropertyAttribute
{
}
```

これにより、以下のように属性を書くことができるようになります。

```c#:Test.cs
using UnityEngine;

public class Test : MonoBehaviour
{
    [NotNull, SerializeField] private GameObject _gameObject;
}
```

# 2．Inspector上で確認できる機能の作成
ここからはEditor拡張になりますので、Editorディレクトリを作成してそこに以下のスクリプトを追加してください。

```c#:NotNullAttributeDrawer.cs
using UnityEditor;
using UnityEngine;

/// <summary>
/// Inspector上でNotNullのAttributeつけたもの変数がnullの場合、注意文を表示する
/// </summary>
[CustomPropertyDrawer(typeof(NotNullAttribute))]
public class NotNullAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        position.height = base.GetPropertyHeight(property, label);
        EditorGUI.PropertyField(position, property, label);
        position.y += position.height;
        if (IsNull(property))
        {
            EditorGUI.HelpBox(position, "参照してるオブジェクトがありません", MessageType.Error);
        }
    }
    
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return IsNull(property)
            ? base.GetPropertyHeight(property, label) * 2f
            : base.GetPropertyHeight(property, label);
    }

    /// <summary>
    /// 参照しているオブジェクトがnullかどうかを判定
    /// </summary>
    private bool IsNull(SerializedProperty property)
    {
        if (property.isArray)
        {
            return property.arraySize == 0;
        }

        // オブジェクトを参照しないものは除く
        if (property.propertyType != SerializedPropertyType.ObjectReference)
        {
            return false;
        }

        return property.objectReferenceValue == null;
    }
}
```


先程のTest.csをオブジェクトにAddComponentすると以下のように注意文言が表示されます。

![スクリーンショット 2021-11-28 15.25.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/30408974-7f18-d6bc-d150-226941186846.png)

注意文言があることで少し便利になりましたね！

# 3.Hierarchy上で確認できる機能の作成
これもEditor拡張になりますので、Editorディレクトリを作成してそこに以下のスクリプトを追加してください。

```c#:HierarchyExtension.cs
using System.Reflection;
using UnityEditor;
using UnityEngine;

public class HierarchyExtension : EditorWindow
{
    [InitializeOnLoadMethod]
    private static void Initialize()
    {
        EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI;
    }

    // Hierarchy上表示する注意アイコンの大きさ
    private const int _ICON_SIZE = 16;

    private static void HierarchyWindowItemOnGUI(int instanceId, Rect selectionRect)
    {
        // GameObject が取得できない場合は SceneAsset
        GameObject obj = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
        if (obj == null)
        {
            return;
        }

        // 所持しているコンポーネント一覧を取得
        Component[] components = obj.GetComponents<Component>();

        // コンポーネントが一つもなければ返す
        if (components.Length <= 0)
        {
            return;
        }

        // すべてのコンポーネントを見る
        foreach (Component component in components)
        {
            // オブジェクトにつけられるスクリプトでなければ返す
            if (component is MonoBehaviour == false)
            {
                continue;
            }

            MonoBehaviour monoBehaviour = (MonoBehaviour) component;

            // SerializeFieldで表示しているものをみる
            foreach (FieldInfo fieldInfo in monoBehaviour.GetType()
                .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
            {
                if (fieldInfo.GetCustomAttributes(typeof(NotNullAttribute), false).Length <= 0)
                {
                    continue;
                }

                // 注意アイコンを表示
                object field = fieldInfo.GetValue(monoBehaviour);
                if (field == null || field.Equals(null))
                {
                    selectionRect.width = _ICON_SIZE;
                    GUI.DrawTexture(selectionRect, EditorGUIUtility.Load("console.erroricon") as Texture2D);
                    break;
                }
            }
        }
    }
}
```


先程のTest.csをAddComponentしたオブジェクトがHierarchy上で以下のように表示されます。

![スクリーンショット 2021-11-28 15.35.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/fb6c78d3-15ff-ce87-9b16-ffcf27d802e2.png)

もちろんオブジェクトを参照すれば注意マークは消えるようになっています。
これでNull Reference Exceptionを未然に防げるようになりましたね！

# 備考
Hierarchy上の警告表示について、親オブジェクトで閉じちゃうと注意マークが見えなくなります。
![スクリーンショット 2021-11-28 15.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/4769ad1f-4f9e-52c9-e631-8c4b6e5915b0.png)
親のオブジェクトに警告マークをつけるような実装を追加でやっていましたが、Editorが重くなり過ぎたので断念しました、、、。

# おわりに
今回はEditor拡張の作成について紹介しました。
個人的にこの機能は色んな場面で役立った印象でしたので、是非皆さんも使ってみてください。

以上、
「[Applibot Advent Calendar 2021](https://qiita.com/advent-calendar/2021/applibot)」 2日目の記事でした！

# 参考ブログ
https://hacchi-man.hatenablog.com/entry/2021/01/01/220000