Post image

Три чарующих слова для Loony Quack: Entity Componet System

Не так давно я познакомился с Entite Component System, о чем писал в одной из предыдущих статей и был поглощён идеей применить данную технологию в своих будущих проектах. Я изучал ECS с разных сторон, смотрел существующие фреймворки, типа Ash framework и Artemis, разбирал проекты сделанные с их помощью, читал массу документации по теме. С каждой капелькой новой информации мне все больше хотелось использовать данный подход уже сейчас. Может быть по этому или из-за того что в Loony Quack намечался непроходимый бардак, я решил попробовать переписать свою игру применяя ECS подход для работы с игровыми объектами. Полностью переписывать весь проект не хотелось, к тому-же, до альфа теста оставалось четыре дня и я решил совместить уже существующий код с новой парадигмой, создав химеру из сущностей, компонентов, систем и моделей. Забегая вперед - пришлось переписать чуть более чем половину всей игры, но это того стоило!

Немного опишу что у меня было до того как я перевел проект на новые рельсы. Изначально, все данные и конфигурацию в Loony Quack я решил забивать хардкодом, посчитав что так будет быстрее и удобнее для разработки. В этом плане я несколько ошибся. Каждый объект в игре был унаследован от класса SimObject (Объект симуляции), а каждый персонаж - утка, от класса SimFoe. Все они попадали в объект класса WorldSimulator, который их двигал на нужные позиции, занимался подсчетом коллизий и еще черте знает чем. Весь этот бедлам творился каждый фрейм, вернее три раза за фрейм, и с ходу разобраться что сейчас делает стимулятор и что конкретно сломалось, было, мягко говоря, сложно. К тому же, от SimFoe были унаследованны такие “замечательные” классы как ZombieQuack, DeathDuck и т.д.

Картинка со старыми связями

К слову, в Loony Quack я не использовали ни одной чужой библиотеки кроме Gestouch .

Так как я решил что полностью переносить весь проект на ECS не буду, то использовать Ash я не стал, а написал свой небольшой фреймворк встроенный в игру. От нормального Entity Componet System подхода его отличают два основных момента. Во первых, каждая система (System) обладает доступом к моделям данных приложения, а во вторых, у каждой сущности (Entity) появилось поле mask в котором храниться информация о её типе и состоянии - жизненном цикле. Фактически поле mask это бинарная маска. Состояний объекта всего три:

  • Created - только что созданная сущность, не обрабатывается большинством систем, инициализируется определенными системами. Сущность с маской Created становится Alive автоматически на следующий фрейм.
  • Alive - основное состояние сущности, обрабатывается всеми системами.
  • Disposed - сущность готовая к удалению из общего контекста. Не обрабатывается ни одной системой и помещается в пул удаленных сущностей.

Так же сущность обладает несколькими типами: Projectile, Decoration, Foe, Effect.

Состояния и типы сущностей я ввел для их быстрой фильтрации в системах. Каждая система получает доступ к сущностям исключительно через итератора с настройкой определенного фильтра. Если системе нужны только “живые враги”, то она обрабатывает только их, не получая доступа к другим сущностям. Это позволяет не беспокоиться о том, что какая-то из систем изменит не подходящую для нее сущность.

Пример кода доступа к сущностям в системе

var it:IEntityIterator = this.entities.getFiltered(EntityType.FOE, EntityLifeCycle.ALIVE); for(it.begin(); !it.end(); it.next()){
var entity:Entity = it.current; ...//обработка сущностей }

В остальном глобальных отличий от ECS нету.

После того как я ввел в проект ECS возник вопрос, где и как хранить данные об игровых сущностях и их компонентах. Кто будет ответственен за их создание. Не долго думая я решил вынести все данные во внешний файл конфигурации, обозвав его entities.xml. Обычно я против использования XML в любом виде, но в этом случае данный формат оказался как нельзя кстати из-за его наглядности, ведь редактировать его будет человек, а не машина.

В самом документе полностью описываются игровые сущности с их компонентами, так как они будут представлены в игре. К примеру, одна из декораций, “злое дерево”:

<entity name="EvilTree" mask="DECORATION"> <Position x="-0.75" y="0" z="0.41"/> <RigidBody .../> <Display mesh="decor" name="evil_tree" fps="0" loop="false"/> </entity>

В процессе инициализации приложения данный файл загружается и разбирается в прототипы, из которых в игре будут созданы уже рабочие сущности

var entity:Entity = engine.createEntity("EvilTree", "DECORATION"); entity.add(new Position(-0.75, 0, 0.41);
entity.add(new RigidBody());
entity.add(new Display(“decor”, “evil_tree”, 0, false));

За создание сущностей ответственность несет система под названием WaveSystem. У этой системы есть доступ к моделям данных о всех прототипах игровых объектов и правилах из создания.

Таким образом, чтобы ввести в игру нового персонажа или декорацию мне больше не надо создавать новые классы, не надо подбрасывать новые зависимости, просто дописать пару строк в файл конфигурации. При этом в игре появится сущность с точно такой же структурой как в файле конфигурации, без дублирования кода. А все классы SimObject, SimFoe ушли в небытие, как и WorldSimulator. В место них в приложении появился один класс Entity и системы его обрабатывающие.

Картинка с новыми связями

После введения Entite Component System в Loony Quack создание и контроль игровых объектов значительно упростился. Код стал более устойчив к ошибкам и понятен. Каждую систему в игре можно оттестировать отдельно, ведь она независима от остальных систем. Огромным плюсом, ко всему прочему, стал прирост в производительности.

Как я писал выше, даже с учетом того, что мне пришлось писать свой фреймворк, на все ушло не более четырех дней. При добавлении нового функционала я больше не ломаю свою голову, как архитектурно разрешить проблемы с новыми связями, я просто добавляю новый компонент и систему к нему. В итоге я и моя игра очень довольны!

Вот еще один из не очевидных плюсов. До ECS одной из проблем являлась смена состояния приложения: паузы, переходы в меню и т.д. Теперь это решается по средствам удаления и восстановления систем в контексте. Предположим, мне необходимо поставить приложение на паузу, у меня есть система MovementSystem ответственная за передвижение персонажей. Я просто удаляю эту систему из общего цикла, она больше не действует, персонажи замерли на месте, пауза готова. При выходе из паузы MovementSystem добавляется обратно в общий цикл - как следствие персонажи снова двигаются.

Есть еще много интересного и вкусного, что предоставляет ECS подход при разработке игрового приложения и в будущих статьях я напишу про них и как Entite Component System повлиял на развитие Loony Quack.

И на десерт немного игрового видео: