Архитектура, ориентированная на данные
- Поведение отделено от данных и часто существует в классах со статическими методами.
- Данные представлены в виде struct без методов, обрабатываются в виде обобщенных структур данных (например,
Array<TData>). - Данные и логика связываются с помощью адаптеров - 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().
Реализация
Составляющие
struct NameData- только данные в видеpublicполей, необходимые для работы соответствующей механики, без методов.class NameComponent: DataComponent<NameData>- компонент, который прикрепляется к игровому объекту, выводит поляstruct NameDataдля возможности их изменения, больше компонент ничего не имеет, отвечает на вопрос “Как делать?”.public static class NameSystem- C#-класс, не наследующий от MonoBehaviour, реализует соответствующую логику, отвечает на вопрос “Что делать?”.public static void Method (ref NameData data)- метод, который принимает данные отstruct NameData, обрабатывает и меняет их непосредственно у целевого игрового объекта. Все методы — статические, входы — через ref или in, допустимы ссылки на Unity-объекты GameObject, Transform и т.д., но без затрагивания их жизненного цикла.
class Name: MonoBehaviour- адаптер, который инициализирует необходимые системы, имеет ссылки на игровые объекты, принимает ввод, является игро-специфичным кодом, отвечает на вопрос “Когда делать?”.private void Update() {NameSystem.Tick();}- методUpdate()из жизненного цикла Unity позволяет “приводить в движение” системы, написанные в C# классах без MonoBehavior.
Использование Unity-функционала
struct NameDataиспользуютusing Unityдля полей специфичных Unity-типов.public static class NameSystemиспользуютusing Unityдля обработки Unity-специфичных объектов в своих методах.public static void Method (EntityId entityId, Transform transform, Rigidbody rd)- Unity-специфичные объекты передаются отдельными параметрами, внутри метода могут вызываться Unity-специфичные методы этих объектов.
Компоненты
- Содержат только данные, необходимые для воздействия системы на данную Сущность.
- Компоненты на Игровой Сущности никогда не должны ссылаться друг на друга.
Типы сущностей или теги
Компонент, позволяющий отметить сущность тем или иным типом. Позволяет разделять поведение сущностей с разным типом в рамках одной системы.
Например, два врага содержат одинаковые компоненты, но благодаря разным типам они двигаются по-разному и создают разные спецэффекты.
Системы
События
- Реализуются через Action-делегаты.
EventSystemсодержит список типов событий и список делегатов для каждого события, а так же метода подписки, отписки, активации.- Система в нужный момент вызывает соответствующее событие.
- Остальные системы подписываются на это событие, чтобы вызвать свой соответствующий метод.
- Событие передает ссылку на объект.
Идея: 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