Unityで自作コンポーネントアタッチ後、子要素にあるコンポーネントをドラッグ＆ドロップでバインドするのが面倒なので楽したい

## やりたいこと

Unityで自作コンポーネントアタッチ後、子要素にあるコンポーネントをドラッグ＆ドロップでバインドするのが面倒なので楽したい

## やったこと

バインドのための命名ルールを決めた上でエディタ拡張を実装した

## 動作例


バインドしたいコンポーネント上で右クリックして `BindComponensFromChildren` メニューを選択する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93212/b74749d7-fd2c-268e-cac7-03a809412c69.png)

バインドされる。嬉しい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93212/aee79ce1-377d-759a-ceb1-a22670913617.png)

## 実装概要

- リフレクションを使ってバインドできそうなそれっぽいフィールドを取得する
  - `Component` を継承した型のフィールド
  - `public` または `SerializeField` 属性のついたフィールド
- GameObjectの名前とフィールドの名前が一致したらバインドを試みる(大文字小文字比較なし)
- バインド可能なコンポーネントが複数存在した場合、先に見つけたほうを使用する
- GameObjectの探索に [LINQ to GameObject](https://assetstore.unity.com/packages/tools/integration/linq-to-gameobject-24256?locale=ja-JP) 使ってます



## コード

```c#
using System;
using System.Collections.Generic;
using System.Reflection;
using Unity.Linq;
using UnityEditor;
using UnityEngine;

namespace Assets.Scripts.Editor
{
    static class ContextMenuUtil
    {
        /// <summary>
        /// 子GameObjectにバインド可能な同名のコンポーネントがあったらバインドする。
        /// 
        /// ・比較する名前は大文字小文字の区別をしない
        /// ・public または SerializeField属性のついたフィールドが対象
        /// ・バインド可能なGameObjectが複数存在した場合、先に見つけたものをセットする
        /// </summary>
        [MenuItem("CONTEXT/MonoBehaviour/BindComponensFromChildren")]
        private static void BindComponentsFromChildren(MenuCommand menuCommand)
        {
            var context = menuCommand.context as MonoBehaviour;
            Debug.Log($"context.name: {context.name}");

            var contextType = context.GetType();
            Debug.Log($"contextType.Name: {contextType.Name}");

            var bindableFields = GetBindableFields(contextType);

            foreach (var field in bindableFields)
            {
                foreach (var child in context.gameObject.Descendants())
                {
                    // フィールドとオブジェクトの名前を大文字小文字無視して比較する
                    if (string.Compare(child.name, field.Name, true) != 0)
                    {
                        // 一致しなかったらスキップ
                        continue;
                    }

                    var childComponent = child.GetComponent(field.FieldType);

                    if (childComponent == null)
                    {
                        // 対象のコンポーネントが存在しないのでスキップ
                        continue;
                    }

                    Debug.Log($"component {field.FieldType.Name} {childComponent.name} found.", childComponent);
                    field.SetValue(context, childComponent);
                    break;
                }
            }

            EditorUtility.SetDirty(context);
            AssetDatabase.SaveAssets();
        }

        /// <summary>
        /// バインド可能なComponentフィールドの一覧を取得する
        /// </summary>
        private static IEnumerable<FieldInfo> GetBindableFields(Type contextType)
        {
            var serializableFields = new List<FieldInfo>();

            var publicFields = contextType.GetFields(BindingFlags.Instance | BindingFlags.Public);
            serializableFields.AddRange(publicFields);

            // NonPublicなフィールドのうち、SerializeField属性が設定されてるものを取得する
            var nonPublicFields = contextType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
            foreach (var field in nonPublicFields)
            {
                var serializeFieldAttribute = field.GetCustomAttribute<SerializeField>();

                if (serializeFieldAttribute != null)
                {
                    serializableFields.Add(field);
                }
            }

            foreach (var field in serializableFields)
            {
                var isComponentField = field.FieldType.IsSubclassOf(typeof(Component));

                if (isComponentField)
                {
                    yield return field;
                }
            }
        }
    }
}
```

## 気になってることとか

- `HideInInspector` や `NonSerialized` なフィールドに対して値設定したらどうなるのか未検証
- `[field: SerializeField]` 属性つけたプロパティに対しても同じことできると便利そう
- `Reset`メソッドから呼び出してもいいかも
- 似たような機能公式機能あってもおかしくなさそうだがググり方がわからない
- インスペクターにドラッグ＆ドロップでコンポーネントの参照設定する操作ってなんていうの？？？バインド？セット？アタッチ？