Когда / где обновить компоненты

10

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

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

Чтобы эти компоненты работали, они должны обновлять каждый кадр, самый простой способ сделать это - пройтись по дереву сцены, а затем для каждой сущности обновить каждый компонент. Но некоторым компонентам может потребоваться немного больше управления. Например, компонент, который делает объект конфликтным, должен управляться чем-то, что может контролировать все конфликтующие компоненты. Компонент, который делает объект доступным для рисования, требует, чтобы кто-то наблюдал за всеми другими объектами для рисования, чтобы выяснить порядок рисования и т. Д.

Итак, мой вопрос: где я могу обновить компоненты, как это можно донести до менеджеров?

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

Рой Т.
источник
1
Вы используете системы каким-то образом?
Асакерон
Компонентные системы являются обычным способом сделать это. Лично я просто вызываю update для всех объектов, который вызывает update для всех компонентов, и у меня есть несколько «особых» случаев (например, пространственный менеджер для обнаружения столкновений, который является статическим).
ashes999
Компонентные системы? Я никогда не слышал о них раньше. Я начну поиск в Google, но я бы приветствовал любые рекомендуемые ссылки.
Рой Т.
1
Entity Systems - будущее разработки MMOG - отличный ресурс. И, честно говоря, меня всегда смущают эти имена архитектуры. Разница с предлагаемым подходом состоит в том, что компоненты содержат только данные, а системы обрабатывают их. Этот ответ также очень актуален.
Асакерон
1
Я написал своего рода блуждающее сообщение в блоге на эту тему здесь: gamedevrubberduck.wordpress.com/2012/12/26/…
AlexFoxGill

Ответы:

15

Я бы предложил начать с чтения 3 большой лжи Майка Актона, потому что вы нарушаете две из них. Я серьезно, это изменит ваш дизайн кода: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

Так что вы нарушаете?

Ложь № 3 - Код важнее данных

Вы говорите о внедрении зависимости, которая может быть полезна в некоторых (и только в некоторых) случаях, но всегда должна вызывать большой сигнал тревоги, если вы ее используете, особенно в разработке игр! Почему? Потому что это часто ненужная абстракция. И абстракции в неправильных местах ужасны. Итак, у вас есть игра. В игре есть менеджеры для разных компонентов. Компоненты все определены. Так что создайте класс где-нибудь в коде основного игрового цикла, в котором «есть» менеджеры. Подобно:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

Дайте ему некоторые функции получения, чтобы получить каждый класс менеджера (getBulletManager ()). Может быть, этот класс сам по себе является синглтоном или доступен из него (возможно, у вас где-то есть центральный синглтон игры). Нет ничего плохого в четко определенных жестко закодированных данных и поведении.

Не создавайте ManagerManager, который позволяет регистрировать менеджеров с помощью ключа, который может быть получен с помощью этого ключа другими классами, которые хотят использовать менеджер. Это отличная система и очень гибкая, но здесь речь идет об игре. Вы точно знаете , какие системы есть в игре. Почему притворяешься, как ты? Потому что это система для людей, которые думают, что код важнее данных. Они скажут: «Код гибкий, данные просто заполняют его». Но код это просто данные. Система, которую я описал, гораздо проще, надежнее, проще в обслуживании и намного более гибкой (например, если поведение одного менеджера отличается от других менеджеров, вам нужно всего лишь изменить несколько строк вместо того, чтобы переделывать всю систему)

Ложь № 2 - Код должен быть разработан вокруг модели мира

Итак, у вас есть сущность в игровом мире. У сущности есть ряд компонентов, определяющих ее поведение. Таким образом, вы создаете класс Entity со списком объектов Component и функцию Update (), которая вызывает функцию Update () каждого компонента. Правильно?

Нет :) Это строится вокруг модели мира: в вашей игре есть пуля, поэтому вы добавляете класс Bullet. Затем вы обновляете каждую пулю и переходите к следующей. Это абсолютно убьет вашу производительность и даст вам ужасную запутанную кодовую базу с дублирующимся кодом везде и без логической структуризации аналогичного кода. (Ознакомьтесь с моим ответом здесь для более подробного объяснения того, почему традиционный ОО-проект отстой, или посмотрите на Data-Oriented Design)

Давайте посмотрим на ситуацию без нашей предвзятости. Мы хотим следующее, не больше и не меньше (обратите внимание, что нет необходимости создавать класс для сущности или объекта):

  • У вас есть куча сущностей
  • Объекты состоят из ряда компонентов, которые определяют поведение объекта
  • Вы хотите обновить каждый компонент в игре каждый кадр, желательно контролируемым образом
  • Кроме определения компонентов как принадлежащих друг другу, сам объект ничего не должен делать. Это ссылка / идентификатор для нескольких компонентов.

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

Если вы знакомы либо с архитектурой компьютера, либо с ориентированным на данные дизайном, вы знаете, как достигается лучшая производительность: плотно упакованная память и группировка выполнения кода. Если вы выполняете фрагменты кода A, B и C, например: ABCABCABC, вы не получите такую ​​же производительность, как при ее выполнении, например: AAABBBCCC. Это не только потому, что кэш инструкций и данных будет использоваться более эффективно, но и потому, что если вы выполняете все буквы «А» один за другим, есть много возможностей для оптимизации: удаление дублирующего кода, предварительный расчет данных, которые используются все буквы "А" и т. д.

Поэтому, если мы хотим обновить все компоненты, давайте не будем делать их классами / объектами с помощью функции обновления. Давайте не будем вызывать эту функцию обновления для каждого компонента в каждой сущности. Это решение "ABCABCABC". Давайте сгруппируем все идентичные обновления компонентов вместе. Затем мы можем обновить все A-компоненты, затем B и т. Д. Что нам нужно для этого?

Во-первых, нам нужны менеджеры компонентов. Для каждого типа компонента в игре нам нужен класс менеджера. Он имеет функцию обновления, которая обновит все компоненты этого типа. Он имеет функцию создания, которая добавит новый компонент этого типа, и функцию удаления, которая уничтожит указанный компонент. Могут быть и другие вспомогательные функции для получения и установки данных, специфичных для этого компонента (например, установка трехмерной модели для компонента модели). Обратите внимание, что менеджер в некотором смысле является черным ящиком для внешнего мира. Мы не знаем, как хранятся данные каждого компонента. Мы не знаем, как обновляется каждый компонент. Нам все равно, пока компоненты ведут себя так, как должны.

Далее нам нужна сущность. Вы могли бы сделать это классом, но это вряд ли необходимо. Сущность может быть не чем иным, как уникальным целочисленным идентификатором или хешированной строкой (также целочисленным). Когда вы создаете компонент для сущности, вы передаете идентификатор в качестве аргумента менеджеру. Если вы хотите удалить компонент, вы передаете идентификатор еще раз. Могут быть некоторые преимущества добавления немного большего количества данных в сущность, а не просто в качестве идентификатора, но это будут только вспомогательные функции, потому что, как я перечислил в требованиях, все поведение сущности определяется самими компонентами. Хотя это ваш двигатель, поэтому делайте то, что имеет для вас смысл.

Нам нужен Entity Manager. Этот класс будет либо генерировать уникальные идентификаторы, если вы используете решение только для идентификаторов, либо его можно использовать для создания / управления объектами сущностей. Он также может вести список всех сущностей в игре, если вам это нужно. Entity Manager может быть центральным классом вашей системы компонентов, храня ссылки на все ComponentManager в вашей игре и вызывая их функции обновления в правильном порядке. Таким образом, весь игровой цикл должен вызывать EntityManager.update (), и вся система хорошо отделена от остальной части вашего движка.

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

  • Создание данных компонента при вызове create (entityID)
  • Удалить данные компонента, когда вызывается remove (entityID)
  • Обновлять все (применимые) данные компонента при вызове update () (т.е. не все компоненты должны обновлять каждый кадр)

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

Обратите внимание, что я до сих пор никогда не призывал к созданию класса XxxComponent для хранения данных компонентов. Это зависит от вас. Вам нравится Data Oriented Design? Затем структурируйте данные в отдельных массивах для каждой переменной. Вам нравится объектно-ориентированный дизайн? (Я бы не советовал, это все равно убьет вашу производительность во многих местах). Затем создайте объект XxxComponent, который будет содержать данные каждого компонента.

Самое замечательное в менеджерах - это инкапсуляция. В настоящее время инкапсуляция является одной из самых ужасно неправильно используемых философий в мире программирования. Вот как это должно быть использовано. Только менеджер знает, где хранятся данные компонента, как работает логика компонента. Есть несколько функций для получения / установки данных, но это все. Вы можете переписать весь менеджер и его базовые классы, и если вы не измените открытый интерфейс, никто даже не заметит. Изменен физический движок? Просто перепишите PhysicsComponentManager и все готово.

Тогда есть одна последняя вещь: связь и обмен данными между компонентами. Теперь это сложно, и не существует универсального решения. Вы можете создать функции get / set в менеджерах, чтобы, например, компонент столкновения мог получить позицию от компонента position (то есть PositionManager.getPosition (entityID)). Вы можете использовать систему событий. Вы можете хранить некоторые общие данные в сущности (на мой взгляд, самое уродливое решение). Вы можете использовать (это часто используется) систему обмена сообщениями. Или используйте комбинацию нескольких систем! У меня нет ни времени, ни опыта, чтобы заходить в каждую из этих систем, но поиск в google и stackoverflow ваши друзья.

рынок
источник
Я нахожу этот ответ очень интересным. Только один вопрос (надеюсь, вы или кто-то можете ответить мне). Как вам удается устранить сущность в системе на основе компонентов DOD? Даже Артемида использует Entity как класс, я не уверен, что это очень глупо.
Wolfrevo Kcats
1
Что вы подразумеваете под устранением этого? Вы имеете в виду систему сущностей без класса сущностей? Причина, по которой у Артемиды есть Entity, заключается в том, что в Artemis класс Entity управляет своими собственными компонентами. В предложенной мной системе классы ComponentManager управляют компонентами. Поэтому вместо того, чтобы нуждаться в классе Entity, вы можете просто иметь уникальный целочисленный идентификатор. Допустим, у вас есть сущность 254, которая имеет компонент позиции. Если вы хотите изменить позицию, вы можете вызвать PositionCompMgr.setPosition (int id, Vector3 newPos), с 254 в качестве параметра id.
Март
Но как вы управляете идентификаторами? Что если вы хотите удалить компонент из сущности, чтобы потом назначить его другому? Что если вы хотите удалить сущность и добавить новую? Что если вы хотите, чтобы один компонент был разделен между двумя или более объектами? Я действительно заинтересован в этом.
Wolfrevo Kcats
1
EntityManager может использоваться для выдачи новых идентификаторов. Его также можно использовать для создания целых объектов на основе предварительно определенных шаблонов (например, создать «EnemyNinja», который генерирует новый идентификатор и создает все компоненты, составляющие вражеского ниндзя, такие как рендеринг, коллизия, AI, возможно, какой-то компонент для рукопашного боя). , так далее). Он также может иметь функцию removeEntity, которая автоматически вызывает все функции удаления ComponentManager. ComponentManager может проверить, есть ли у него данные компонента для данного объекта, и если да, удалить эти данные.
Март
1
Переместить компонент из одного объекта в другой? Просто добавьте функцию swapComponentOwner (int oldEntity, int newEntity) в каждый ComponentManager. Все данные находятся в ComponentManager, все, что вам нужно, это функция, чтобы изменить владельца, которому она принадлежит. Каждый ComponentManager будет иметь что-то вроде индекса или карты для хранения того, какие данные принадлежат какому идентификатору объекта. Просто измените идентификатор объекта со старого на новый идентификатор. Я не уверен, насколько легко делиться компонентами в системе, которую я придумал, но насколько это сложно? Вместо одной ссылки Entity ID <-> Component Data в индексной таблице их несколько.
Март
3

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

Это типичный наивный подход к обновлению компонентов (и нет ничего плохого в том, что он наивный, если он работает для вас). Одна из больших проблем, с которой вы на самом деле коснулись - вы работаете через интерфейс компонента (например IComponent), поэтому вы ничего не знаете о том, что именно вы только что обновили. Вы, вероятно, также ничего не знаете о порядке компонентов внутри объекта, поэтому

  1. вы, вероятно, будете часто обновлять компоненты разных типов (по сути, плохая ссылочная местность кода)
  2. эта система не подходит для одновременных обновлений, потому что вы не можете определить зависимости данных и таким образом разделить обновления на локальные группы несвязанных объектов.

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

Синглтон здесь на самом деле не нужен, поэтому вам следует избегать его, потому что он несет в себе недостатки, о которых вы упомянули. Внедрение зависимостей не является чрезмерным - суть концепции заключается в том, что вы передаете вещи, которые нужны объекту, этому объекту, в идеале в конструкторе. Для этого вам не нужен тяжелый DI-каркас (например, Ninject ) - просто передайте дополнительный параметр конструктору.

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

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

Тогда сама система, которая владеет объектами нижнего уровня, отвечает за их обновление и может делать это массово и таким способом, который позволяет при необходимости выполнять многопоточное обновление. Ваш основной игровой цикл управляет грубым порядком, в котором эти системы обновляются (сначала физика, затем рендер и т. Д.). Если у вас есть подсистема, которая не имеет срока действия или не контролирует обновления экземпляров, которые она раздает, вы можете создать простую оболочку, которая будет также обрабатывать обновление всех компонентов, относящихся к этой системе, и решать, где ее разместить. обновление по отношению к остальным обновлениям вашей системы (я часто нахожу, что это происходит с компонентами «скрипта»).

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


источник