【Unity】ヒエラルキーウィンドウにコンポーネントのアイコンを表示するEditor拡張

多階層構造のプレハブなど、「Colliderが付与されているGameObjectってどれ？」等よく探すことになるので、ヒエラルキーウィンドウのみで付与されているコンポーネントが判断できるEditor拡張を作りました。

バージョン2020.3.5f1で動作確認。

![動作イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/98f4477b-400c-f8e2-9180-78e4fc63fe52.png)


# 仕様
こんな仕様にしました。

- コンポーネントの付与されている順に、ヒエラルキーウィンドウのGameObject名の右側にコンポーネントのアイコンを右揃えで表示
- Transform、RectTransformのアイコンは除く
- Scriptは複数付与されていてもアイコンは1つのみ表示
- コンポーネントが非活性の場合は半透明でアイコンを表示
- ヒエラルキーウィンドウのコンテキストメニューに表示、非表示を切り替えるメニューを用意


# ポイント解説
## 描画更新イベント
以下のイベントに登録すると、ヒエラルキーウィンドウの描画更新時に行単位でイベントハンドラメソッドが呼び出されます。

```cs
EditorApplication.hierarchyWindowItemOnGUI += DisplayIcons;
```

`[InitializeOnLoadMethod]` 属性を付与したメソッドで上記イベント登録を行っておきます。

```cs
[InitializeOnLoadMethod]
private static void Initialize()
{
    EditorApplication.hierarchyWindowItemOnGUI += DisplayIcons;
}
```

イベントハンドラメソッドの定義はこんな感じ。
引数の `instanceID` からGameObject、 `selectionRect` からヒエラルキーウィンドウの描画範囲の座標を取得できます。

```cs
private static void DisplayIcons(int instanceID, Rect selectionRect)
{
    // ヒエラルキーウィンドウの描画更新時に追加で行いたい処理をここに記述
}
```

## コンポーネント一覧取得
GameObjectに付与されているコンポーネントの一覧は以下のように取得することができます。
`Transform` は全てのGameObjectに存在するので除いています。
あと動かしてみたら `ParticleSystemRenderer` というものもアイコン登録無しでヒットしたので除くようにしました。

```cs:hierarchyWindowItemOnGUIイベントハンドラ内でのコンポーネントの一覧取得
// instanceIDをオブジェクト参照に変換
if (!(EditorUtility.InstanceIDToObject(instanceID) is GameObject gameObject)) return;

// オブジェクトが所持しているコンポーネント一覧を取得
List<Component> components
    = gameObject
        .GetComponents<Component>()
        .Where(x => !(x is Transform || x is ParticleSystemRenderer))
        .Reverse()
        .ToList();
```

## コンポーネントのアイコン取得
以下の形でアイコン画像を取得することができます。

```cs:コンポーネントのアイコン取得
Texture image = AssetPreview.GetMiniThumbnail(component);
```

Scriptのアイコンは複数表示されても邪魔になるので1つのみ表示するようにしました。
Scriptのアイコンは以下の形で取得することができました。
アイコン名は[こちら](https://unitylist.com/p/5c3/Unity-editor-icons)を参考にしましたが、Unityのバージョンによって変わるかもしれません。

```cs:Scriptのアイコン取得
Texture scriptIcon = EditorGUIUtility.IconContent("cs Script Icon").image;
```

## アイコンをヒエラルキーウィンドウに描画
以下の形で画像を描画することができます。
コンポーネントごとにposをずらして順に呼び出せば、アイコンを並べて描画することができます。

```cs:アイコンをヒエラルキーウィンドウに描画
GUI.DrawTexture(pos, image, ScaleMode.ScaleToFit);
```

## コンテキストメニューで表示/非表示切替
`[MenuItem]` 属性でメニューへの項目追加が行えます。
メニューのパスに `GameObject/` で始まる文字列を指定すると、ヒエラルキーウィンドウのGameObjectへのコンテキストメニューとなります。

```cs:ヒエラルキーウィンドウのコンテキストメニューで表示切替
private const string MenuPath = "GameObject/コンポーネントアイコン表示切替";

private static bool enabled = true;

[MenuItem(MenuPath, false, 20)]
private static void ToggleEnabled()
{
    enabled = !enabled;
    UpdateEnabled();
}

private static void UpdateEnabled()
{
    EditorApplication.hierarchyWindowItemOnGUI -= DisplayIcons;
    if (enabled)
        EditorApplication.hierarchyWindowItemOnGUI += DisplayIcons;
}
```

# コード全文

```cs:ヒエラルキーウィンドウにコンポーネントのアイコンを表示するEditor拡張
#nullable enable
using System.Linq;
using UnityEditor;
using UnityEngine;

/// <summary>
/// Hierarchyウィンドウにコンポーネントのアイコンを表示する拡張機能
/// </summary>
/// <remarks>
/// <para>Unity2020.3.5f1で動作確認。</para>
/// <para>
/// <list type="bullet">
/// <item><description>Transform以外のコンポーネントのアイコン表示。</description></item>
/// <item><description>スクリプトのアイコンは複数付与されていても1つのみ表示。</description></item>
/// <item><description>コンポーネントが無効になっている場合はアイコン色が半透明になっている。</description></item>
/// <item><description>Hierarchyウィンドウで右クリックで表示されるメニュー「コンポーネントアイコン表示切替」の選択で表示/非表示の切替可能。</description></item>
/// </list>
/// </para>
/// </remarks>
public static class ComponentIconDrawerInHierarchy
{
    private const int IconSize = 16;

    private const string MenuPath = "GameObject/コンポーネントアイコン表示切替";

    private const string ScriptIconName = "cs Script Icon";

    private static readonly Color colorWhenDisabled = new Color(1.0f, 1.0f, 1.0f, 0.5f);
 
    private static Texture? scriptIcon;

    private static bool enabled = true;

    [InitializeOnLoadMethod]
    private static void Initialize()
    {
        UpdateEnabled();

        /*
         * ビルトインアイコンの呼び出し方は以下を参考にした
         * https://unitylist.com/p/5c3/Unity-editor-icons
         */
#pragma warning disable UNT0023 // Coalescing assignment on Unity objects
        scriptIcon ??= EditorGUIUtility.IconContent(ScriptIconName).image;
#pragma warning restore UNT0023
    }

    [MenuItem(MenuPath, false, 20)]
    private static void ToggleEnabled()
    {
        enabled = !enabled;
        UpdateEnabled();
    }

    private static void UpdateEnabled()
    {
        EditorApplication.hierarchyWindowItemOnGUI -= DisplayIcons;
        if (enabled)
            EditorApplication.hierarchyWindowItemOnGUI += DisplayIcons;
    }

    private static void DisplayIcons(int instanceID, Rect selectionRect)
    {
        // instanceIDをオブジェクト参照に変換
        if (!(EditorUtility.InstanceIDToObject(instanceID) is GameObject gameObject)) return;

        var pos = selectionRect;
        pos.x = pos.xMax - IconSize;
        pos.width = IconSize;
        pos.height = IconSize;

        // オブジェクトが所持しているコンポーネント一覧を取得
        var components
            = gameObject
                .GetComponents<Component>()
                .Where(x => !(x is Transform || x is ParticleSystemRenderer))
                .Reverse()
                .ToList();

        var existsScriptIcon = false;
        foreach (var component in components)
        {
            Texture image = AssetPreview.GetMiniThumbnail(component);
            if (image == null) continue;

            // Scriptのアイコンは1つのみ表示
            if (image == scriptIcon)
            {
                if (existsScriptIcon) continue;
                existsScriptIcon = true;
            }

            // アイコン描画
            DrawIcon(ref pos, image, component.IsEnabled() ? Color.white : colorWhenDisabled);
        }
    }

    private static void DrawIcon(ref Rect pos, Texture image, Color? color = null)
    {
        Color? defaultColor = null;
        if (color.HasValue)
        {
            defaultColor = GUI.color;
            GUI.color = color.Value;
        }

        GUI.DrawTexture(pos, image, ScaleMode.ScaleToFit);
        pos.x -= pos.width;

        if (defaultColor.HasValue)
            GUI.color = defaultColor.Value;
    }

    /// <summary>
    /// コンポーネントが有効かどうかを確認する拡張メソッド
    /// </summary>
    /// <param name="this">拡張対象</param>
    /// <returns>コンポーネントが有効となっているかどうか</returns>
    private static bool IsEnabled(this Component @this)
    {
        var property = @this.GetType().GetProperty( "enabled", typeof(bool));
        return (bool)(property?.GetValue(@this, null) ?? true);
    }
}
```

# Missingチェックと併用
Missingなコンポーネント、SerializeFieldが存在したらワーニングアイコンとエラーメッセージを表示するEidot拡張との併用版を以下の記事に記載しました。

https://qiita.com/masamin/items/78ba7238dae7aacdc28c

# パッケージ
上記のMissingチェックとの併用版を少し改良を加えたものを UPMパッケージとして公開したので、使えそうと感じられたらぜひご活用ください。

GitHub:

https://github.com/lilytech-lab/ComponentIconInHierarchy
 
OpenUPM:

https://openupm.com/packages/com.lilytech-lab.component-icon-in-hierarchy/