-
Notifications
You must be signed in to change notification settings - Fork 77
(RU) Basics
Акторы - это ECS Entity Component System
фреймворк со спецификой работы на движке Unity. ECS решает множество задач в написании игр : локальность данных, низкая связанность кода, модульность, потенциально высокая производительность за счет сравнительно простой настройки распараллеливания вычислений.
Среди разработчиков юнити широко распространена OOP Object Oriented Design
парадигма однако для оптимальной работы с ECS нужно настраивать образ мышления на DOD
Data Oriented Design
.
Разница подходов выражена в том как мы воспринимаем объекты и работаем с ними:
class Spaceship {
Transform body;
Vector3 position;
float speed;
void Move();
}
Для OOP характерно думать категорией отдельно взятого объекта. Весь мир состоит из отдельных объектов которые общаются друг с другом посредством своих методов и интерфейсов. Когда мы пишем метод Move в OOP мы подсознательно хотим двигать конкретный кораблик в пространстве.
Классы объектов могут наследоваться друг от друга расширяя функционал и скрывая базовый функционал и переменные. Так появляется класс Дредноут наследуемый от Spaceship и выполняющий все те же функции + добавляется стрельба из пушки.
class Dreadnought: Spaceship {
Cannon weapon;
void Shoot();
}
class Spaceships {
Transform[] body;
Vector3[] position;
float[] speed
void Move();
}
Для DOD характерно думать о том как изменяются данные. С точки зрения DOD при описании движения нет разницы двигается ли кораблик или астероид до тех пор пока они живут по одним законам. Так, если в движении участвуют компоненты скорости и тела то сам объект уже не важен. Программисты составляют композиции компонентов обрабатывающихся по единым правилам.
class Entities{
Transform[] body;
Vector3[] position;
float[] speed
Cannon[] weapon;
void Move();
void Shoot();
}
Одна из задач фреймворка предоставить удобную среду для работы с объектами в стиле DOD.
Компонент - это кирпич игры описывающий некий набор переменных которыми будет обладать сущность. Во фреймворке компоненты часто пишутся с атрибутом [Serializable] для того чтобы редактировать поля в инспекторе Unity.
Правила компонентов:
- По возможности нужно стараться делать компоненты небольшими.
- Компонент должен описывать одну задачу в игре. В крайнем случае несколько тесносвязанных задач.
- Компоненты могут наследоваться от других компонентов если нужно.
- Компоненты не должны содержать никакой игровой логики.
- Компоненты могут содержать методы инициализации, настроек, сброса значений или другие утилитарные методы связанные с компонентом.
При сомнениях о том расширять функционал компонента или создавать новый как правило лучше создавать новый. Составление композиций компонентов одна из главных задач этого подхода и оптимальные сборки приходят с опытом.
[Serializable]
public class ComponentMotion : IComponent{
public float speed;
public Vector3 direction;
}
[Serializable]
public class ComponentDamageble : IComponent{
public int hp;
}
[Serializable]
public class ComponentObject : IComponent{
public Transform body;
public GameObject go;
}
Для каждого компонента создается хранилище, Storage<T>
где T - тип компонента.
Например Storage<ComponentMotion>
, Storage хранит массив компонента. Этот массив может только расти. Хранилища всех компонентов растут пропорционально количеству сущностей активных в игре. Так, если уровень состоит из 20 астероидов и 1 корабля игрока то размер хранилищ будет равен 21.
Таким образом каждый из представленных в игре компонентов может быть "доступен" любой из сущности. Чтобы на уровне игровой логики определить какой из компонентов активен используется дополнительный массив bool масок.
Component/Entities | Entity Asteroid 1 | Entity Asteroid 2 | Entity SpaceShip 3 | Entity Player 4 |
---|---|---|---|---|
Component Motion | ✔️ | ✔️ | ✔️ | ✔️ |
Component Health | ✔️ | ✔️ | ✔️ | ✔️ |
Component Shoot | ❌ | ❌ | ✔️ | ✔️ |
Component Rotate | ✔️ | ✔️ | ✔️ | ✔️ |
Component Input | ❌ | ❌ | ❌ | ✔️ |
Component Render | ✔️ | ✔️ | ✔️ | ✔️ |
Component Loot | ✔️ | ✔️ | ✔️ | ❌ |
Выше приведена таблица компонентов и сущностей астероидов, кораблика и игрока.
При OOP парадигме мы бы описывали их как разные классы объектов, а в случае с корабликом в лучшем случае у нас был бы некий AI controller и мы бы добавляли контроллер игрока отдельному кораблю чтобы управлять им.
В DOD парадигме мы описываем функционал объекта через композицию компонентов не придавая значения типу объекта. Мы предполагаем что астероиды это нечто что имеет компонент движения, разворота, здоровья ,отрисовки и лута. Но астероиды не умеют стрелять и у них нет управления.
Мы можем манипулировать набором компонентов изменяя поведение сущностей. Так, мы легко можем активировать компонент Shoot у астероида и он начнет стрелять. А у игрока в бою отбили пушку на корабле, мы убираем компонент Shoot и корабль игрока больше не может стрелять.
В описании выше часто применялось слово сущность. Сущность Entity
- это инкремент, абстракция выраженная числом.
В OOP подходе мы обращаемся к экземплярам классов объекта чтобы получить переменные/методы.
В ECS подходе мы обращаемся к массиву храналища чтобы получить нужный нам компонент "объекта".
Сущность служит индексом массива компонентов. ComponentMotion[EntityAsteroidID]
Что происходит когда сущность создается/уничтожается?
Когда сущность "уничтожается" она уходит в область зарезервированных сущностей. При попытке создания сущности в следующий раз индекс будет взят из этой области.
Если зарезервированная область пуста следующая сущность возьмет новый индекс. Массив хранилищ компонентов будет расширен до значения этого индекса.
В фреймворке системами называются обработчики Processing
. Выше было сказано, что компоненты не хранят игровой логики. Обработчики - это скрипты поведений которые обрабатывают поступающие компоненты. Обработчики ничего не знают об объектах и не принадлежат отдельно взятому объекту.
Помимо этого обработчики хорошие кандидаты для "контроллеров, менеджеров" если вам когда либо такие понадобятся.
Как правило Processing наследуется от ProcessingBase
и добавляется в систему через Starter
классы. Оттуда обработчики передаются в Toolbox
- сервис локатор. При переходе с основной сцены на новую все обработчики будут почищены и удалены. Исключение составляют обработчики наследуемые от интерфейса IKernel
. Однако это уже более сложный вид работы.
public class ProcessingPlayers : ProcessingBase
{
public Group<ComponentPlayer> groupPlayer;
}
public class StarterLevel1 : Starter
{
protected override void Setup()
{
Add<ProcessingCamera>();
Add<ProcessingPlayers>();
Add<ProcessingInputConnect>();
Add<ProcessingShadows>();
Add<ProcessingAnimations>();
Add<ProcessingMotion>();
Add<ProcessingMotionAir>();
}
}
Обработчики работают с группами. Группы - это композиция компонентов у сущности. Каждая сущность может состоять во множестве групп. Сортировка групп автоматическая. По умолчанию можно указать до 6 компонентов в группе, однако если нужно больше это легко сделать отредактировав класс GroupBase.cs
Чтобы сущность попала в конкретную группу должны быть выполнены условия : у сущности есть указанные компоненты и фильтры.
public class ProcessingPlayers : ProcessingBase
{
// у сущности должен быть компонент Motion, Rotate и Render чтобы попасть в эту группу.
public Group<ComponentMotion, ComponentRotate, ComponentRender> groupMovable;
}
За инициализацию группы отвечает метод ProcessingGroupAttributes.Setup(this);
где this - экземляр класса в котором есть группы. Этот метод уже есть в ProcessingBase поэтому разработчику не нужно делать лишних движений. Однако про метод полезно знать если вы захотите иметь группу скажем у юнити компонента наследуемого от Monobehavior
.
Мы работаем с ссылками на группы: все поля с одинаковой группой в разных классах будут ссылаться на один источник группы.
Актор - это вспомогательный ванильный класс для визуальной настройки сущности в инспекторе. Актор так же служит связующим элементом между игровым объектом Unity и фреймворком. Actor наследуется от Monocached
класса. Как правило в акторах нет смысла писать логики, они служат для сборки сущности. Дальнейшая работа происходит уже в обработчиках. Акторы хороши еще тем, что деактивация или уничтожение объекта с Актором влечет автоматиеческое удаление сущности из групп. При создании объекта с актором автоматически создается сущность.
public class ActorPlayer : Actor
{
[FoldoutGroup("Setup")] public ComponentLight componentLight;
protected override void Setup()
{
Set(Tag.GroupAlly);
Set(componentLight);
}
}
MonoCached - базовый класс в фреймворке для юнити компонентов. Наследуется от Monobehavior. Monocached классы отвечают за правильное уничтожение объекта через пулы Pool
. В зависимости от настройки пула или его отстутсвия объект будет либо деактивирован и использован в будущем либо полностью уничтожен.