Система компонентов cущностей (Entity Component System)

Использование паттерна “Компонент” – это единственная альтернатива не потеряться среди леса из деревьев наследования. Я поясню.

Когда люди работают над сложными механиками, они пытаются выделить общие части системы, чтобы их переиспользовать. Но так случается, что общие части должны присутствовать в совершенно несвязанных модулях.

Частая ошибка – пытаться объединить то, что должно лежать раздельно. Из-за того, что общий функционал нужен в двух несвязанных ветках наследования, а большинство языков не поддерижвают множественное наследование, то люди извращаются как могут. Впиливают куда-нибудь наверх еще уровень наследования, чтобы где-то через несколько уровней вниз, 2 класса использовали 1-2 метода.

Inheritnace-problem-diagram

Часто еще бывает, что этот Common класс не используется целиком, и среди всей иерархии тащатся бесполезные части.

Component pattern

Если использовать шаблон Component, то все упрощается. Мы просто меняем наследование на композицию, добавляем там где нам нужно компонент, и вызываем методы.

Inheritnace-problem-diagram

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

Entity Component Pattern

Я не смог найти правильное название для этого шаблона. Это что-то среднее между использование компонентов, что я рассмотрел ранее, и Entity Component System, что мы рассмотрим позже.

Предыдущий подход имеет один большой недостаток: использование всех компонентов зашито в коде. То есть, мы не можем добавить к классу игрока новый компонент, не поменяв код.

Это может быть довольно сильным ограничением, ведь геймплей должен быть настраиваемым. Если каждый раз гейм-дизайнер будет дергать программиста, чтобы изменить скорость перемещения персонажа – это катастрофа.

Для этого в играх есть некая система для конфигурации. Если можно конфигурировать то что уже написано в коде, так почему бы не дать еще больше свободы для креатива и не “хардкодить” поведение заранее.

Шаблон Entity Component – это то, как работает вся система в Unity. Идея, на самом деле неплохая, но реализация так себе. О ее недостатках все известно, но мы часто забываем о преимуществах.

Данный шаблон предполагает, что все в программе – это некая “Сущность (Entity)”. В терминах Unity сущность – это GameObject. И каждая сущность может иметь набор компонентов. Они заранее не определены и их список может меняться посредством методов.

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

Классический пример.

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

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

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

Немного программер-арта:

Health component attached to every object

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

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

Это классный паттерн, но у него есть свои недостатки:

  • мы не знаем какие компоненты есть у объекта заранее, можем определить это только в рантайме, что рождает все эти ошибки, когда мы пытаемся использовать компонент, которого нет
  • зависимости между компонентами сложнее реализовать и отслеживать
  • такая система бьет по производительности, так как многие вещи делаются в рантайме

Но преимущества сильно перевешивают недостатки, имхо.

Entity Component System

На самом деле, шаблон Entity Component System (ECS) очень похож на предыдущий вариант. С одним ключевым отличием: бизнес логика по обработке компонентов лежит в системах, а не в самих компонентах.

В отличие от предыдущего подхода, где, например, метод Update присутствует в каждом отдельном компоненте, в ECS есть система обработки компонентов, которая имеет список всех компонентов одного типа, и, итерируясь по ним, исполняет бизнес логику.

В чем здесь выгода?

Попробуйте в Unity отключить все компоненты определенного типа. Это возможно, но будет проблематично:

  • Найти все компоненты типа T и выключить их: FindObjectsOfType, который по сути итерируется по всем объектам в сцене.
  • Сделать статическую переменную в классе компонента и проверять ее внутри Update и других методов, т.е. по сути исполнять N раз одну и ту же работу в каждом компоненте.

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

Аналогично, если в Update есть какое-то вычисление, одинаковое для всех компонентов, то система его может закэшировать.

Итерация и обработка однотипных компонентов происходит гораздо быстрее.

Система может манипулировать несколькими типами компонентов и эффективно их обрабатывать, решая проблему зависимостей и взаимодействия. Даже если у вас несколько типов компонентов, код логично сгруппирован в системе.

А самое главное – порядок обработки компонентов системами строго определен, так как системы обрабатывают компоненты последовательно.

Кстати, Unity уже сделали свою реализацию ECS

Подводим итоги

Люди все еще думают классическими терминами ООП, и стремятся “унаследовать” все подряд. Для механики игр больше подходят другие решения вроде Entity Component System (ECS). И да, если вы не пробовали писать код с таким подходом, то скорее всего придется немножко сломать голову и поменять мышление.

Ссылки