首页 > 解决方案 > 如何在运行时确定属性所针对的类的名称和引用?

问题描述

我正在尝试为 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]绕过将类类型作为属性参数传入的要求,我该怎么做?

其次,我如何获得对新创建的用属性注释的类实例的引用?

标签: c#unity3dreflection

解决方案


有四个步骤可以实现您想要的功能。

  1. 检测具有适当属性的所有类型。您可以通过循环遍历每个Assemblyin来做到这一点AppDomain.CurrentDomain。每次重新加载脚本程序集时,您都需要缓存这些类型,您可以使用静态类和InitializeOnLoad编辑器中的属性进行检查。(如果你不需要,你肯定不想进行反思)。
  2. 检测何时在场景层次结构中添加/修改对象。这可以通过事件来完成EditorApplication.hierarchyChanged
  3. 检查是否有任何不应该存在的组件添加到场景中。这可以UnityEditor.SceneManagement.EditorSceneManager通过循环遍历场景中的所有根对象并跟踪适当的信息来完成。
  4. 如果遇到多个相同的组件(销毁、向用户显示消息等),请确定该怎么做。这个取决于你,但我在下面提供了一个合乎逻辑的答案。

这可以通过以下属性来实现

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);
                            }
                        }
                    }
                }
            }
        }
    }
}

我已经在编辑器中对此进行了测试,效果非常好。

最后一点:

  • 此编辑器脚本适用于小型项目,但在具有大场景和/或大量脚本文件的大型项目中,可能会遇到性能问题。重新加载脚本程序集时,即使是几分之一秒的延迟也是很明显的,并且它会在项目的整个生命周期中累加。

推荐阅读