Подтвердить что ты не робот

Коммуникация в компонентном игровом движке

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

Это дает вам несколько преимуществ:

  • Выделяет специфическое для столкновений состояние из GameObject.
  • Позволяет вам выполнять итерацию над GameObjects, у которых нет компонентов столкновения. (Если у вас много неинтерактивных объектов, таких как визуальные эффекты и декорации, это может сэкономить приличное количество циклов.)
  • Позволяет вам сжигать циклы между объектом и его компонентом. Если вы итерации через объекты, то getCollisionComponent() на каждом из них, это указание указателя может привести к промаху в кеше. Выполнение этого для каждого кадра для каждого объекта может сжечь много CPU.

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

Ответ 3

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

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

[EDIT] Подробнее об этом см. сообщение (спасибо snk_kid для указания этого).

Ответ 4

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

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

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

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

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