背景
下図のように、Prefab に複数の GameObject を配置し、各 GameObject に複数の Component を持たせ、各 Component に UniqueId を割り振る機会がありました。
人力では手間とミスが問題になるので、私は 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