c# - 如何在运行时确定属性所针对的类的名称和引用?
问题描述
我正在尝试为 Unity 创建一个自定义类属性,以防止目标 MonoBehaviour 存在于场景中的多个对象上。我做了一些搜索,据说为了获得属性所针对的类的类型,我应该使用属性的构造函数;它不能使用反射来完成
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DisallowMultipleInScene : Attribute
{
public DisallowMultipleInScene(Type type)
{
// Do stuff with type here
}
}
如果我没记错的话,这意味着在一个类上使用它,比如一个名为 ManagerManager 的类,可以这样实现:
[DisallowMultipleInScene(typeof(ManagerManager))]
public class ManagerManager : MonoBehaviour
{
// Implementation
}
这似乎有点多余,并且还允许传入不正确的类名。为了禁止将多个组件(从 MonoBehaviour 继承的类)放置在同一个对象上,[DisallowMultipleComponent]
使用该属性。这个属性类似于我想要的。您不需要传递它所应用的类的名称,它似乎知道。
我查看了 UnityCsReference GitHub 中的源代码,试图了解它在幕后的工作原理,但似乎没有实现,只是一个定义,位于此处,摘录如下:
[RequiredByNativeCode]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class DisallowMultipleComponent : Attribute {}
因此,对于我自己的实现,要与属性类似地工作[DisallowMultipleComponent]
,我的属性需要确定它被应用到的类,并获取对 MonoBehaviour 脚本的引用,以便可以从刚刚添加到的对象中删除它.
那么,首先,属性如何[DisallowMultipleComponent]
绕过将类类型作为属性参数传入的要求,我该怎么做?
其次,我如何获得对新创建的用属性注释的类实例的引用?
解决方案
有四个步骤可以实现您想要的功能。
- 检测具有适当属性的所有类型。您可以通过循环遍历每个
Assembly
in来做到这一点AppDomain.CurrentDomain
。每次重新加载脚本程序集时,您都需要缓存这些类型,您可以使用静态类和InitializeOnLoad
编辑器中的属性进行检查。(如果你不需要,你肯定不想进行反思)。 - 检测何时在场景层次结构中添加/修改对象。这可以通过事件来完成
EditorApplication.hierarchyChanged
。 - 检查是否有任何不应该存在的组件添加到场景中。这可以
UnityEditor.SceneManagement.EditorSceneManager
通过循环遍历场景中的所有根对象并跟踪适当的信息来完成。 - 如果遇到多个相同的组件(销毁、向用户显示消息等),请确定该怎么做。这个取决于你,但我在下面提供了一个合乎逻辑的答案。
这可以通过以下属性来实现
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);
}
}
}
}
}
}
}
}
我已经在编辑器中对此进行了测试,效果非常好。
最后一点:
- 此编辑器脚本适用于小型项目,但在具有大场景和/或大量脚本文件的大型项目中,可能会遇到性能问题。重新加载脚本程序集时,即使是几分之一秒的延迟也是很明显的,并且它会在项目的整个生命周期中累加。
推荐阅读
- r - 在 R gganimate 中做动画时如何保留以前的数据层?
- r - 如何使用 ggplot 创建具有两个因素的构面?
- i2c - I2C 复位协议测试
- arrays - 对象reactjs的数组
- php - how to pass json data into datatable with passing id in codeigniter?
- amazon-web-services - 用于为 Lex 转换 MP3 输入的 AWS Lambda 代码
- laravel - Laravel Passport:如何查询“护照密钥”?
- java - 如何在 Web 应用程序的 aspectj 类中访问请求对象
- excel - Large table to be transformed by moving 200+ columns to rows
- python - 我无法在 pycharm 社区版中导入 tkinter