Коммуникация в компонентном игровом движке - PullRequest
20 голосов
/ 05 июля 2010

Для 2D-игры, которую я делаю (для Android), я использую систему на основе компонентов, в которой GameObject содержит несколько объектов GameComponent. Компоненты GameComponents могут быть такими, как компоненты ввода, компоненты рендеринга, компоненты, выделяющие маркеры, и так далее. В настоящее время GameComponents имеют ссылку на объект, которому они принадлежат, и могут изменять его, но сам GameObject просто имеет список компонентов, и ему все равно, какие это компоненты, если они могут быть обновлены при обновлении объекта.

Иногда компонент имеет некоторую информацию, которую GameObject должен знать. Например, для обнаружения столкновений GameObject регистрируется в подсистеме обнаружения столкновений, чтобы получать уведомления при столкновении с другим объектом. Подсистема обнаружения столкновений должна знать ограничивающую рамку объекта. Я храню x и y непосредственно в объекте (потому что он используется несколькими компонентами), но ширина и высота известны только компоненту рендеринга, который содержит растровое изображение объекта. Я хотел бы иметь метод getBoundingBox или getWidth в GameObject, который получает эту информацию. Или вообще, я хочу отправить некоторую информацию от компонента к объекту. Тем не менее, в моем текущем дизайне GameObject не знает, какие конкретные компоненты он имеет в списке.

Я могу придумать несколько способов решения этой проблемы:

  1. Вместо того, чтобы иметь полностью общий список компонентов, я могу позволить GameObject иметь специальное поле для некоторых важных компонентов. Например, он может иметь переменную-член, называемую renderComponent; всякий раз, когда мне нужно получить ширину объекта, я просто использую renderingComponent.getWidth(). Это решение по-прежнему допускает общий список компонентов, но некоторые из них обрабатываются по-разному, и я боюсь, что в итоге у меня будет несколько исключительных полей, так как нужно будет запрашивать больше компонентов. Некоторые объекты даже не имеют компонентов рендеринга.

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

  3. У компонентов должен быть интерфейс, который указывает, к какому типу информации они могут запрашиваться. Например, компонент рендеринга реализует интерфейс HasSize, который включает в себя такие методы, как getWidth и getHeight. Когда GameObject требуется ширина, он перебирает свои компоненты, проверяя, реализуют ли они интерфейс HasSize (используя ключевое слово instanceof в Java или is в C #). Это выглядит как более общее решение, один из недостатков которого заключается в том, что поиск компонента может занять некоторое время (но тогда большинство объектов имеют только 3 или 4 компонента).

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

Короткая версия

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

Ответы [ 4 ]

17 голосов
/ 05 июля 2010

Мы получаем вариации по этому вопросу три или четыре раза в неделю на GameDev.net (где игровой объект обычно называют «сущностью»), и до сих пор нет единого мнения о лучшем подходе.Однако было показано, что несколько различных подходов работоспособны, поэтому я бы не стал слишком сильно беспокоиться об этом.

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

Полностью общие компоненты в значительной степени бесполезны: они должны предоставлять какой-то известный интерфейс, иначе нет смысла их существовать.В противном случае вы можете просто иметь большой ассоциативный массив нетипизированных значений и покончить с этим.В Java, Python, C # и других языках более высокого уровня, чем C ++, вы можете использовать отражение, чтобы дать вам более общий способ использования определенных подклассов без необходимости кодировать информацию о типе и интерфейсе в самих компонентах.

Что касается связи:

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

Некоторые люди используют публикацию / подписку, сигналы / слоты и т. д. для создания произвольных соединений между компонентами.Это кажется немного более гибким, но в конечном итоге вам все еще нужно что-то со знанием этих неявных зависимостей.(И если это известно во время компиляции, почему бы просто не использовать предыдущий подход?)

Или же вы можете поместить все общие данные в сам объект и использовать их в качестве общей области связи (незначительно связанной с система доски в AI), которую каждый из компонентов может читать и записывать.Это обычно требует некоторой устойчивости перед лицом определенных свойств, которые не существуют, когда вы ожидаете их.Это также не поддается параллелизму, хотя я сомневаюсь, что это большая проблема для небольшой встроенной системы ...?

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

11 голосов
/ 06 июля 2010

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

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

  1. Сохраните его в самом компоненте столкновения.
  2. заставить код обнаружения столкновения работать с компонентами напрямую.

Таким образом, вместо того, чтобы механизм коллизий выполнял итерацию по коллекции объектов GameObject для разрешения взаимодействия, он должен выполнять итерацию непосредственно по коллекции CollisionComponents. После того, как произошло столкновение, компонент должен передать его своему родительскому объекту GameObject.

Это дает вам пару преимуществ:

  1. Оставляет специфичное для столкновения состояние из GameObject.
  2. Избавляет вас от перебора объектов GameObject, в которых нет компонентов столкновения. (Если у вас много неинтерактивных объектов, таких как визуальные эффекты и декорации, это может сэкономить приличное количество циклов.)
  3. Избавляет вас от горящих циклов, проходящих между объектом и его компонентом. Если вы перебираете объекты, а затем выполняете getCollisionComponent() для каждого из них, это следование указателю может вызвать пропадание кэша. Выполнение этого для каждого кадра для каждого объекта может сжечь много ЦП.

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

4 голосов
/ 05 июля 2010

Используйте « шину событий ».(обратите внимание, что вы, вероятно, не можете использовать код как есть, но он должен дать вам основную идею).

По сути, создайте центральный ресурс, где каждый объект может зарегистрировать себя в качестве прослушивателя, и сказать «Если произойдет X, Я хочу знать".Когда что-то происходит в игре, ответственный объект может просто отправить событие X на шину событий, и все заинтересованные стороны заметят.

[РЕДАКТИРОВАТЬ] Более подробное обсуждение см. В разделе передача сообщений * 1008.* (спасибо snk_kid за указание на это).

3 голосов
/ 05 июля 2010

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

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

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

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

Управляемая расширяемая среда для .NET это хорошее решение этой проблемы.Я понимаю, что вы собираетесь разрабатывать на Android, но вы все равно можете получить вдохновение от этой среды.

...