Начнём с того, что я обожаю сериализацию в Unity. Она надёжна и очень проста в использовании. Я просто расширяю MonoBehaviour, ScriptableObject и подобные классы и настраиваю сериализуемые поля экземпляров в инспекторе.
Но у неё есть и слабости. Одна из них ― человеческий фактор. Представьте себе огромный проект, который живёт несколько лет и над которым работает около сотни человек. И любой из них может совершить ошибку: оставить пустую ссылку на объект, указать число вне диапазона, ввести строку в неверном формате, заполнить массив слишком маленьким или, наоборот, слишком большим количеством объектов. Уверен, у каждого из вас найдутся такие примеры из своего опыта. Причин и оправданий тоже множество: невнимательность, неожиданные последствия слияния веток, сбои редактора… И никто от этого не застрахован.
Такие ошибки до поры до времени остаются незаметными: компилятору до них нет дела, в отличие от опечаток в коде. Особенно неприятны они тем, что проявляются часто уже во время выполнения кода. Только тогда вы начинаете читать журнал сообщений и идёте проверять данные: тыкать их в редакторе или листать YAML. Но объектов может быть достаточно много, есть риск что-то пропустить или попросту залениться.
Конечно, можно добавить проверок в коде, но от этого он загрязнится. Иногда эти проверки негативно влияют на производительность. А ещё не всегда однозначно понятно, как именно обработать каждую конкретную ошибку.
Универсального или даже штатного метода бороться с подобным в Unity нет. Поэтому мы в Pixonic реализовали свою систему валидации ассетов. И это очень помогает нам жить.
Сейчас я опишу, как там всё устроено.
Работает система на основе атрибутов на сериализуемых полях. Сначала тот, кто пишет скрипт, с помощью атрибута явно указывает свои ожидания от значения поля. Самый частый пример ― ExpectNotNull.
public class LookAtTarget : MonoBehaviour { [SerializeField, ExpectNotNull] private Transform target; // ... }
При постобработке ассетов значения в таких полях будут проверены на соответствие ожиданиям. В обычном режиме при сохранении ассета в логе можно увидеть нарушения с подробной локализацией, а при клике на такой лог объект подсвечивается в редакторе:
[Error] Reference is null Property: target Attribute: ExpectNotNullAttribute Script: Game.Core.LookAtTarget Object: Turret Asset: Assets/Scripts/Game/Test.unity
В режиме batch mode у нас собирается отдельный отчёт обо всех нарушениях. Опционально существует возможность завершить сборку в CI (Continuous Integration) неудачно, если хотя бы одно ожидание с Severity.Error не сработало.
Именно эта деталь даёт нам хорошие (но не стопроцентные!) гарантии того, что данным можно доверять, и проверять их дополнительно, скорее всего, не следует.
Вроде бы этого должно быть достаточно. Но бывает такое, что в редакторе закрыта панель лога, или в ней стоит фильтр, из-за чего человек лог не видит. Поэтому для того, чтобы разработчик сразу при настройке понимал, что нарушает ожидание, была сделана система уведомлений прямо в инспекторе:

Система расширяема, и любой может добавить свой тип ожидания, написав атрибут и валидатор к нему. У нас реализовано несколько таких типов и иногда добавляются новые: как общие, так и проектно-специфичные.
Пример атрибута для валидации длины строки:
public class ExpectStringLengthAttribute : ExpectationWithSeverityAttribute { public readonly int Min; public readonly int Max; public ExpectStringLengthAttribute(int min, int max) => (Min, Max) = (min, max); }
И сам валидатор длины строки:
[Validator(typeof(ExpectStringLengthAttribute))] public class ExpectStringLengthValidator : IValidator { void IValidator.Validate(SerializedProperty property, ExpectationAttribute attribute, IList<Issue> output) { if (property.propertyType != SerializedPropertyType.String) { output.AddTypeNotSupported(attribute, property.type); return; } var length = property.stringValue.Length; var rangeAttribute = (ExpectStringLengthAttribute) attribute; if (length < rangeAttribute.Min || length > rangeAttribute.Max) { output.Add( rangeAttribute.Severity, attribute, "Length out of range [{0}, {1}]", rangeAttribute.Min, rangeAttribute.Max ); } } }
Для чего нужны эти два класса? Класс атрибута должен находиться в сборке с основным кодом игры: это просто метка с параметрами. Код класса валидатора запускается только в контексте редактора и поэтому должен находиться в соответствующей сборке (в поддиректории Editor). После загрузки скриптов в контексте редактора мы обходим сборки и составляем статический словарь с ключами в виде типов атрибутов и значениями в виде экземпляров валидатора.
[DidReloadScripts] public static void ReloadScripts() { var validatorType = typeof(IValidator); _validators.Clear(); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { foreach (var type in assembly.GetTypes()) { if (type == validatorType || !validatorType.IsAssignableFrom(type)) { continue; } foreach (var attribute in type.GetCustomAttributes(typeof(ValidatorAttribute), true)) { _validators.Add( ((ValidatorAttribute) attribute).AttributeType, (IValidator) Activator.CreateInstance(type) ); } } } }
Этот словарь далее используется как в постобработке, так и при отрисовке инспектора.
public static void Validate(SerializedProperty property, FieldInfo fieldInfo, IList<Issue> output) { var attributes = fieldInfo.GetCustomAttributes(typeof(ExpectationAttribute), true); for (int i = 0, count = attributes.Length; i != count; ++i) { var propertyAttribute = (ExpectationAttribute) attributes[i]; if (_validators.TryGetValue(propertyAttribute.GetType(), out var validator)) { validator.Validate(property, propertyAttribute, output); } } }
Во время отрисовки инспектора вызвать валидацию легче, так как в PropertyDrawer сразу доступны и property, и fieldInfo. Во время постобработки дело обстоит сложнее, так как обходить всё сериализуемое дерево приходится вручную, параллельно через рефлексию собирая FieldInfo для каждого SerializedProperty. Это достаточно объёмный код, поэтому я не буду добавлять его в статью, но здесь можно посмотреть, как это делается.
В иных сферах подобная валидация будет слишком слабой защитой от непредвиденных ошибок, но для наших целей такой способ вынести некоторые проверки из этапа времени выполнения на этапы редактирования и сборки вполне себя оправдывает. Это ускоряет разработку уже хотя бы тем, что мы можем точно увидеть, где и кто что-то сломал, снижая влияние человеческого фактора на качество сборки.
ссылка на оригинал статьи https://habr.com/ru/company/pixonic/blog/491324/
Добавить комментарий