НИ+ ngplus.games

Архитектура, ориентированная на данные

677 слов no tags
  1. Поведение отделено от данных и часто существует в классах со статическими методами.
  2. Данные представлены в виде struct без методов, обрабатываются в виде обобщенных структур данных (например, Array<TData>).
  3. Данные и логика связываются с помощью адаптеров - MonoBehaviour-классы без поведенческой логики, только утилитарные функции.

При композиционном подходе наследование является инструментом абстрагирования/полиморфизма, для группировки объектов по тому или иному принципу, а так же инструментом расширения существующих классов, а не средством избегания повторения кода.

При композиции пригодятся паттерны Фабрика и Стратегия.

Сложности

Если мы лишаем игровые объекты объектности за счет разделения логики от данных и оставляем игровые сущности контейнерами данных, а логику концентрируем в нескольких системах, то возникает сложность передачи данных от сущностей системам, которая в объектно-ориентированном подходе почти не была выражена.

Чтобы системы имели доступ к сущностям, те должны быть собраны в коллекции, к которым имеют доступ все системы.

Данные

Формат данных

В DOP и Unity-разработке struct используется как идеальный носитель данных, а generic-классы — как инструмент для повторного использования кода систем (например, менеджеров, пулов, буферов), которые могут управлять любыми Data-struct каждый.

Связь с игровыми объектами

Игровой объект имеет компонент класса DataComponent, который принимает параметром Data типа struct. Поля struct автоматически появляются в инспекторе. Классы поведения работают со struct и меняют значения в них.

Логика

Объектный пулинг логики

Behavior-классы можно собирать в пулы. Тогда такой класс должен иметь метод Reset(), для сброса в значения по умолчанию, чтобы при присваивании к новому игровому объекту он был без последствий от жизни прошлого игрового объекта.

Если поведение привязано к MonoBehaviour с Update/корутинами — тогда лучше пулить весь GameObject, а не только C#-объект. В Unity MonoBehaviour нельзя переиспользовать без GameObject, так что если Behavior — наследник MonoBehaviour, то пулить нужно весь объект, а не только скрипт.

Но если Behavior — обычный C#-класс, то пулить его — отличная идея.

Обновление через Tick()

Можно избежать наследования от MonoBehaviour в Behavior-классах, если они будут реализовывать метод Tick(), и этот метод будут вызывать классы-адаптеры в Unity в цикле Update().

Реализация

Составляющие

  1. struct NameData - только данные в виде public полей, необходимые для работы соответствующей механики, без методов.
  2. class NameComponent: DataComponent<NameData> - компонент, который прикрепляется к игровому объекту, выводит поля struct NameData для возможности их изменения, больше компонент ничего не имеет, отвечает на вопрос “Как делать?”.
  3. public static class NameSystem - C#-класс, не наследующий от MonoBehaviour, реализует соответствующую логику, отвечает на вопрос “Что делать?”.
    1. public static void Method (ref NameData data) - метод, который принимает данные от struct NameData, обрабатывает и меняет их непосредственно у целевого игрового объекта. Все методы — статические, входы — через ref или in, допустимы ссылки на Unity-объекты GameObject, Transform и т.д., но без затрагивания их жизненного цикла.
  4. class Name: MonoBehaviour - адаптер, который инициализирует необходимые системы, имеет ссылки на игровые объекты, принимает ввод, является игро-специфичным кодом, отвечает на вопрос “Когда делать?”.
    1. private void Update() {NameSystem.Tick();} - метод Update() из жизненного цикла Unity позволяет “приводить в движение” системы, написанные в C# классах без MonoBehavior.

Использование Unity-функционала

  1. struct NameData используют using Unity для полей специфичных Unity-типов.
  2. public static class NameSystem используют using Unity для обработки Unity-специфичных объектов в своих методах.
  3. public static void Method (EntityId entityId, Transform transform, Rigidbody rd) - Unity-специфичные объекты передаются отдельными параметрами, внутри метода могут вызываться Unity-специфичные методы этих объектов.

Компоненты

  1. Содержат только данные, необходимые для воздействия системы на данную Сущность.
  2. Компоненты на Игровой Сущности никогда не должны ссылаться друг на друга.

Типы сущностей или теги

Компонент, позволяющий отметить сущность тем или иным типом. Позволяет разделять поведение сущностей с разным типом в рамках одной системы.

Например, два врага содержат одинаковые компоненты, но благодаря разным типам они двигаются по-разному и создают разные спецэффекты.

Системы

События

  1. Реализуются через Action-делегаты.
  2. EventSystem содержит список типов событий и список делегатов для каждого события, а так же метода подписки, отписки, активации.
  3. Система в нужный момент вызывает соответствующее событие.
  4. Остальные системы подписываются на это событие, чтобы вызвать свой соответствующий метод.
  5. Событие передает ссылку на объект.

Идея: EventHandler - подписан на все возможные события и вызывает нужные методы. Так системы остаются чистыми от подписок.

Структура папок

Assets/ └── Scripts/ ├── Data/ → HealthData.cs, ShooterData.cs ├── Components/ → HealthComponent.cs, ShooterComponent.cs ├── Systems/ → HealthSystem.cs, ShooterSystem.cs └── Core/ → DataComponent.cs, GameEntity.cs, GameSession.cs