[英]How do I determine the name and reference of a class being targeted by an attribute at runtime?
我正在嘗試為 Unity 創建一個自定義 class 屬性,以防止目標 MonoBehaviour 存在於場景中的多個 object 上。 我做了一些搜索,據說為了獲得屬性所針對的 class 的類型,我應該使用屬性的構造函數; 它不能使用反射來完成
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DisallowMultipleInScene : Attribute
{
public DisallowMultipleInScene(Type type)
{
// Do stuff with type here
}
}
如果我沒記錯的話,這意味着在 class 上使用它,例如一個名為 ManagerManager 的,可以這樣實現:
[DisallowMultipleInScene(typeof(ManagerManager))]
public class ManagerManager : MonoBehaviour
{
// Implementation
}
這似乎有點多余,並且還允許傳入不正確的 class 名稱。 為了禁止將多個組件(繼承自 MonoBehaviour 的類)放置在同一個 object 上,使用了[DisallowMultipleComponent]
屬性。 這個屬性類似於我想要的。 您不需要傳遞它所應用的 class 的名稱,它似乎知道。
我查看了 UnityCsReference GitHub 中的源代碼,試圖了解它是如何在幕后工作的,但似乎沒有實現,只是一個定義,位於此處,摘錄如下:
[RequiredByNativeCode]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class DisallowMultipleComponent : Attribute {}
因此,對於我自己的實現,要與[DisallowMultipleComponent]
屬性類似地工作,我的屬性需要確定它所應用的 class,並獲取對 MonoBehaviour 腳本的引用,以便可以從 object 中刪除它剛剛添加到。
那么,首先, [DisallowMultipleComponent]
屬性如何繞過將 class 類型作為屬性參數傳入的要求,我該怎么做?
其次,如何獲得對新創建的帶有屬性注釋的 class 實例的引用?
有四個步驟可以實現您想要的功能。
AppDomain.CurrentDomain
中的每個Assembly
來做到這一點。 每次重新加載腳本程序集時,您都需要緩存這些類型,您可以使用 static class 和編輯器中的InitializeOnLoad
屬性進行檢查。 (如果你不需要,你肯定不想進行反思)。EditorApplication.hierarchyChanged
來完成。UnityEditor.SceneManagement.EditorSceneManager
class 通過循環場景中的所有根對象並跟蹤適當的信息來完成。這可以通過以下屬性來實現
using System;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DisallowMultipleComponentsInSceneAttribute : Attribute
{
public DisallowMultipleComponentsInSceneAttribute()
{
}
}
和下面的編輯器腳本,它必須放在項目中的“編輯器”文件夾中。
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor;
using UnityEditor.SceneManagement;
using System;
using System.Reflection;
using System.Collections.Generic;
[InitializeOnLoad]
public static class SceneHierarchyMonitor
{
private class TrackingData
{
public Dictionary<Scene, Component> components = new Dictionary<Scene, Component>();
}
private static Dictionary<Type, TrackingData> __trackingData = new Dictionary<Type, TrackingData>();
static SceneHierarchyMonitor()
{
EditorApplication.hierarchyChanged += OnHierarchyChanged;
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (Type type in assembly.GetTypes())
{
if (type.GetCustomAttribute<DisallowMultipleComponentsInSceneAttribute>() != null)
{
__trackingData.Add(type, new TrackingData());
}
}
}
for (int i = 0; i < EditorSceneManager.sceneCount; ++i)
{
MonitorScene(EditorSceneManager.GetSceneAt(i));
}
}
private static void OnHierarchyChanged()
{
for (int i = 0; i < EditorSceneManager.sceneCount; ++i)
{
MonitorScene(EditorSceneManager.GetSceneAt(i));
}
}
private static void MonitorScene(Scene scene)
{
foreach (KeyValuePair<Type, TrackingData> kvp in __trackingData)
{
// If the scene hasn't been tracked, initialize the component to a null value.
bool isOpeningScene = false;
if (!kvp.Value.components.ContainsKey(scene))
{
isOpeningScene = true;
kvp.Value.components[scene] = null;
}
foreach (GameObject rootGameObject in scene.GetRootGameObjects())
{
Component[] components = rootGameObject.GetComponentsInChildren(kvp.Key, true);
for (int i = 0; i < components.Length; ++i)
{
Component component = components[i];
// If we haven't found a component of this type yet, set it to remember it. This will occur when either:
// 1. The component is added for the first time in a given scene.
// 2. The scene is being opened and we didn't have any tracking data previously.
if (kvp.Value.components[scene] == null)
{
kvp.Value.components[scene] = component;
}
else
{
// You can determine what to do with extra components. This makes sense to me, but you can change the
// behavior as you see fit.
if (kvp.Value.components[scene] != component)
{
GameObject gameObject = component.gameObject;
EditorGUIUtility.PingObject(gameObject);
if (!isOpeningScene)
{
Debug.LogError($"Destroying \"{component}\" because it has the attribute \"{typeof(DisallowMultipleComponentsInSceneAttribute).Name}\", " +
$"and one of these components already exists in scene \"{scene.name}.\"", gameObject);
GameObject.DestroyImmediate(component);
EditorUtility.SetDirty(gameObject);
}
else
{
Debug.LogWarning($"Found multiple components of type {kvp.Key.Name} in scene {scene.name}. Please ensure there is exactly one " +
$"instance of this type in the scene before continuing.", component.gameObject);
}
}
}
}
}
}
}
}
我已經在編輯器中對此進行了測試,效果非常好。
最后一點:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.