【Unity】Prefab や Scene 上で被らない連番を割り振る Attribute

# 背景

下図のように、Prefab に複数の GameObject を配置し、各 GameObject に複数の Component を持たせ、各 Component に UniqueId を割り振る機会がありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307985/aa9301a9-b7a6-b70c-e6c9-152af2f4e8be.png)

人力では手間とミスが問題になるので、私は Prefab と Scene を表示・保存した際に Prefab と Scene 上のオブジェクト同士で被らないように連番を割り振る Attribute を作りました。
※　他の Prefab や Scene の間では値が被ります

# 使い方

``` test.cs
using UnityEngine;

public class test : MonoBehaviour
{
    // 引数に文字列を入れると、同じ文字列グループによる連番になります
    [SerializeField, NumberingInt(nameof(uniqueId))] int uniqueId;
    // 引数を省略すると「default」グループの連番になります
    [SerializeField, NumberingInt] int eventId1, eventId2;
    // int 型以外では利用できません
    // [SerializeField, NumberingInt] long eventId3;
}
```

# コード

``` NumberingIntAttribute.cs
using System;
using System.Collections.Generic;

#if UNITY_EDITOR
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
#endif

[AttributeUsage(AttributeTargets.Field)]
public sealed class NumberingIntAttribute : Attribute, INumberingIntAttribute
{
    private readonly string m_numberingId;
    public string NumberingId => m_numberingId;
    public NumberingIntAttribute(string numberingId = "default")
    {
        m_numberingId = numberingId;
    }
#if UNITY_EDITOR
    public void Inject(SerializedProperty serializedProperty, int index)
    {
        serializedProperty.intValue = index;
    }
#endif
}

public interface INumberingIntAttribute
{
#if UNITY_EDITOR
    void Inject(SerializedProperty serializedProperty, int index);
    string NumberingId { get; }
#endif
}

#if UNITY_EDITOR
internal static class NumberingIntAttributeInjector
{
    [DidReloadScripts]
    private static void OnReloadScripts()
    {
        EditorSceneManager.sceneOpened += OnSceneOpened;
        EditorSceneManager.sceneSaving += OnSceneSaving;
        PrefabStage.prefabStageOpened += OnPrefabStageOpened;
        PrefabStage.prefabSaving += OnPrefabSaving;
        EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
        InjectAll(true);
    }

    private static void OnSceneOpened(Scene scene, OpenSceneMode mode) => InjectAll(true);
    private static void OnSceneSaving(Scene scene, string path) => InjectAll(false);
    private static void OnPrefabStageOpened(PrefabStage prefabStage) => InjectAll(true);
    private static void OnPrefabSaving(GameObject prefab) => InjectAll(false);
    private static void OnPlayModeStateChanged(PlayModeStateChange change)
    {
        if (change != PlayModeStateChange.ExitingEditMode) return;
        InjectAll(true);
    }

    private static void InjectAll(bool canUndo)
    {
        if (EditorApplication.isPlaying) return;

        var monoBehaviours = Resources
            .FindObjectsOfTypeAll<MonoBehaviour>()
            .Where(x => x.gameObject.scene.isLoaded)
            .Where(x => x.gameObject.hideFlags == HideFlags.None)
            .ToArray();

        Dictionary<string, int> indexDict = new();
        foreach (var mb in monoBehaviours) indexDict = Inject(indexDict, mb, canUndo);
    }
    private static Dictionary<string, int> Inject(
        Dictionary<string, int> indexDict, MonoBehaviour monoBehaviour, bool canUndo)
    {
        var serializedObject = new SerializedObject(monoBehaviour);
        var fieldInfos = monoBehaviour
            .GetType()
            .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        foreach (var fieldInfo in fieldInfos)
        {
            var attribute = fieldInfo
                    .GetCustomAttributes()
                    .OfType<INumberingIntAttribute>()
                    .FirstOrDefault();
            if (attribute == null) continue;

            if (indexDict.ContainsKey(attribute.NumberingId) == false) indexDict.Add(attribute.NumberingId, 0);
            attribute.Inject(serializedObject.FindProperty(fieldInfo.Name), indexDict[attribute.NumberingId]++);
        }

        if (canUndo) serializedObject.ApplyModifiedProperties();
        else serializedObject.ApplyModifiedPropertiesWithoutUndo();
        return indexDict;
    }
}
#endif
```