Pull to refresh

Comments 6

Спасибо большое за статью. Очень интересно. Не могли бы вы поподробнее рассказать каким образом вы итерируетесь по SerializedProperty во время постобработки? Особенно интересно, если это постобработка сцены, а не префаба.

Я сейчас попробую ответить, возможно немного сумбурно получится.


Префаб обходится просто – берём все копоненты на всех GO внутри и вызываем валидацию на каждом. Со сценами сложнее, сцену нужно загрузить (EditorSceneManager.OpenScene), а потом пройтись по GetRootGameObjects так же, как по префабам. Ещё мы проверяем ассеты ScriptableObject.
Тут есть подводный камень – ассеты ScriptableObject могут быть вложены в любые другие ассеты.


Далее главное синхронизировать обход по SerializedObject и обход рефлексией вглубь по типу объекта.


Берём каждый компонент или скруптуемый объект и обходим его
var serializedObject = new SerializedObject(unityObject);
Enumerate(unityObject, serializedObject.GetIterator());

Вот так примерно выглядит сам обход
IEnumerable<ValueTuple<SerializedProperty, FieldInfo>> EnumerateProperties(object obj, SerializedProperty property)
{
    // и идём по property
    if (obj == null || !property.Next(true)) yield break; // это был пустой объект

    do
    {
        var fieldInfo = unityObject.GetType().GetFieldInfo(property.name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // *

        if (fieldInfo == null) continue;

        yield return (property.Copy(), fieldInfo); // отдаём на проверку

        // если это референс на другой объект, то вглубь идти не нужно, пропускаем
        if (property.propertyType != SerializedPropertyType.Generic) continue;

        if (property.isArray)
        {
            var array = fieldInfo.GetValue(unityObject) as IEnumerable; // не забываем проверить на null, бывают такие случаи
            var index = 0;
            foreach (var element in array)
            {
                var item = property.GetArrayElementAtIndex(index++);
                yield return (item.Copy(), fieldInfo); // отдаём на проверку элемент массива

                if (item.propertyType != SerializedPropertyType.Generic) continue; // опять же, не проверяем ссылки

                foreach (var pair in Enumerate(element, item.Copy())) yield return pair;
            }

            continue; // с массивом покончили
        }

        // тут просто поле, и его нужно вглубь
        foreach (var pair in Enumerate(fieldInfo.GetValue(obj), property.Copy())) yield return pair;

    }
    while (property.Next(false));
}

Примечание к * в коде:
Тут на самом деле нужно хитрее, пройтись по всей иерархии типа наверх (BaseType), пока не упрёшься в типы, которые уже не нужно проверять. Это как MonoBehaviour, SerializedObject и прочие основные типы, так и thirdparty компоненты, в которые вы не можете вставлять аттрибуты валидации. Это один подход. Другой подход – это заранее подготовить список типов, в которых есть аттрибуты валидации и искать только в них.

Интересно было бы узнать, используете ли вы в Unity какую-либо Dependency Injection и как вообще относитесь к этой идее?

Смотря что вы имеете ввиду под Dependency Injection. Если вы про DI-контейнер, то да, некоторые проекты используют Zenject, но не все (один, если быть точным). А если про более общее понятие "внедрение зависимостей", то тоже да, конечно же.
Что касается моей точки зрения, то я сторонник как можно более явного подхода к управлению зависимостями. Просто не очень люблю магию метапрограммирования в рантайме (это я про C# в частности). Я за так называемый Poor Man's DI.
Можно узнать, как вас подтолкнула тема статьи к этому вопросу?)

Спасибо, что нашли время ответить! Мой вопрос, конечно же, был не по теме статьи, поэтому прямой связи тут нет.

Вопрос отсутствия эффективной и как можно более автоматизированной валидации в Unity меня волновал с самого начала — можно легко забыть назначить поле, а еще легче удалить объект или неудачно переименовать класс, при этом все ссылки на него станут невалидными, а узнаем об этом, только когда дело дойдёт до их использования, что делает код совершенно ненадежным.

Просматривая исходники других разработчиков пришел к выводу, что люди в большинстве вообще не парятся на этот счет, и никаких общепринятных подходов тоже не прослеживается, поэтому между делом изобретал свои велосипеды. Толковых статей на эту тему мне пока не попадалось, а из вашей я сразу заимствовал некоторые идеи. Одновременно думал над тем, чтобы попробовать Zenject в следующем проекте и предположил, что вы, скорее всего, не только задавались этим вопросом, но и имеете практический опыт. Также подумал, что можете дать практичную оценку того или иного подхода, независимо от его трендовости. Так от чего бы не спросить :) Может мой вопрос подтолкнет вас к написанию статьи на эту тему, тем более что подобных материалов, в т.ч. связанных с архитектурой приложении в Unity, как мне кажется, сильно не хватает, если не считать довольно поверхностных обзоров.

С термином «Poor Man's DI» сталкиваюсь впервые. Хотя и сам предпочитаю явно наблюдаемые связи, ручное прокидывание зависимостей — то еще занятие. Пока что недостаточно разобрался с Zenject, чтобы понять, как это скажется на разных аспектах реализации, не ухудшится ли читабельности кода и удобство отладки. Сейчас использую синглтоны и пока не вижу с ними особых проблем, за исключением неудобной формы написания (MyComponent.Instance.SomeValue), но в данном подходе я точно знаю сколько это стоит (фактически нисколько). В случае же использования стороннего DI фреймворка — это черный ящик (если нет времни ковыряться в исходинках) и остается только надеяться, что все будет работать как минимум эффективнее чем Object.FindObjectsOfType(). Из вашего опыта мне интересно ваше общее впечатление от использования того же Zenject — станете ли использовать его в других проектах, а если нет, то из-за чего?
Большое спасибо за статью!

Вдохновлялся вашим решением при реализации подобной системы у себя в проекте. Только добавил еще возможность валидации ассетов, если на каком-то компоненте висит нужный атрибут. Помогает когда прямой ссылки на ассет нет (загрузка по имени из ресурсов/бандлов).

Хотел спросить — а как вы решали проблему с тем, что для каждой проперти в юнити может быть только один drawer? Например если у нас уже есть крутой отрисовщик для конкретной проперти, и мы к ней добавляем атрибут валидации. Или в таком случае вы дублируете код для уведомлений в каждом нужном отрисовщике?
Only those users with full accounts are able to leave comments. Log in, please.