Как определить имя и ссылку на класс, на который нацеливается атрибут во время выполнения? - PullRequest
0 голосов
/ 09 апреля 2020

Я пытаюсь создать собственный атрибут класса для 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] удается обойти требование передачи типа класса в качестве параметра атрибута и как я могу это сделать?

И, во-вторых, как получить ссылку на недавно создан экземпляр класса с аннотированным атрибутом?

1 Ответ

0 голосов
/ 11 апреля 2020

Есть четыре шага для достижения желаемой функциональности.

  1. Обнаружение всех типов с соответствующим атрибутом. Вы можете сделать это, просматривая каждый Assembly в AppDomain.CurrentDomain. Вы захотите кэшировать эти типы каждый раз при перезагрузке сборки скрипта, что можно проверить с помощью класса stati c и атрибута InitializeOnLoad из редактора. (Вы определенно не хотите делать отражение, если вам не нужно).
  2. Обнаружение, когда объекты добавляются / изменяются в иерархии сцены. Это можно сделать с помощью события EditorApplication.hierarchyChanged.
  3. Проверьте, не был ли добавлен какой-либо компонент в сцену, которого там быть не должно. Это можно сделать с помощью класса UnityEditor.SceneManagement.EditorSceneManager, циклически перебирая все объекты root на сцене и отслеживая соответствующую информацию.
  4. Решите, что делать, если вы встретите несколько одинаковых компонентов (уничтожить, показать сообщение пользователю, et c.). Это ваш вопрос, но я включил логический ответ ниже.

Этого можно достичь с помощью следующего атрибута

using System;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DisallowMultipleComponentsInSceneAttribute : Attribute
{
    public DisallowMultipleComponentsInSceneAttribute()
    {

    }
}

и следующего сценария редактора , который должен быть помещен в папку «Editor» в вашем проекте.

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

Я протестировал это в редакторе, и он работает довольно хорошо.

Последнее замечание:

  • Этот скрипт редактора отлично работает в небольшом проекте, но в большом проекте с большими сценами и / или большим количеством файлов сценариев можно столкнуться с проблемами производительности. Даже несколько долей секундной задержки при перезагрузке сборки скрипта заметны, и это может накапливаться в течение всего жизненного цикла проекта.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...