Хороший вопрос! Прежде чем перейти к конкретным вопросам, которые вы задали, я скажу: не стоит недооценивать силу простоты. Тенпн прав. Имейте в виду, что все, что вы пытаетесь сделать с помощью этих подходов, - это найти элегантный способ отложить вызов функции или отделить вызывающего абонента от вызываемого. Я могу рекомендовать сопрограммы как удивительно интуитивный способ облегчить некоторые из этих проблем, но это немного не по теме. Иногда вам лучше просто вызвать функцию и жить с тем фактом, что сущность A связана непосредственно с сущностью B. См. YAGNI.
Тем не менее, я использовал и был доволен моделью сигнал / слот в сочетании с простой передачей сообщений. Я использовал его в C ++ и Lua для довольно успешного названия для iPhone с очень плотным графиком.
В случае с сигналом / слотом, если я хочу, чтобы объект A делал что-то в ответ на то, что сделал объект B (например, открывает дверь, когда что-то умирает), я мог бы попросить объект A подписаться непосредственно на событие смерти объекта B. Или, возможно, субъект А подписывался на каждую группу объектов, увеличивал счетчик на каждое запущенное событие и открывал дверь после того, как N из них умерло. Кроме того, «группа объектов» и «N из них» обычно определяются конструктором в данных уровня. (Кроме того, это одна область, где сопрограммы могут действительно сиять, например, WaitForMultiple («Умирающий», entA, entB, entC); door.Unlock ();)
Но это может быть обременительным, когда дело доходит до реакций, которые тесно связаны с кодом C ++, или по сути эфемерных игровых событий: нанесение урона, перезагрузка оружия, отладка, управляемая игроками обратная связь с ИИ на основе определения местоположения. Именно здесь передача сообщений может заполнить пробелы. По сути, это сводится к чему-то вроде: «Скажите всем сущностям в этой области нанести урон через 3 секунды» или «Когда вы закончите физику, чтобы выяснить, кого я застрелил, скажите им запустить эту функцию сценария». Трудно понять, как это сделать, используя публикацию / подписку или сигнал / слот.
Это легко может быть излишним (по сравнению с примером tenpn). Это также может быть неэффективным раздутием, если у вас много действий. Но, несмотря на свои недостатки, этот подход «сообщения и события» очень хорошо сочетается со скриптовым игровым кодом (например, в Lua). Код сценария может определять свои собственные сообщения и события и реагировать на них, не обращая внимания на код C ++. И код сценария может легко отправлять сообщения, которые запускают код C ++, например, изменение уровней, воспроизведение звуков или даже просто разрешение оружию установить, какой урон наносит сообщение TakeDamage. Это сэкономило мне массу времени, потому что мне не приходилось постоянно дурачиться с Луабиндом. И это позволило мне хранить весь мой luabind-код в одном месте, потому что его было немного. Когда правильно соединены,
Кроме того, мой опыт использования варианта № 2 заключается в том, что вам лучше обрабатывать его как событие в другом направлении. Вместо того, чтобы спрашивать, каково состояние объекта, запускайте событие / отправляйте сообщение всякий раз, когда здоровье вносит существенные изменения.
Что касается интерфейсов, между прочим, я реализовал три класса для реализации всего этого: EventHost, EventClient и MessageClient. EventHosts создает слоты, EventClients подписываются / подключаются к ним, а MessageClients связывают делегата с сообщением. Обратите внимание, что целевой объект делегата MessageClient не обязательно должен быть тем же объектом, который владеет ассоциацией. Другими словами, MessageClients могут существовать исключительно для пересылки сообщений другим объектам. FWIW, метафора хост / клиент является неуместной. Источник / Раковина могли бы быть лучшими понятиями.
Извините, я там немного побродил. Это мой первый ответ :) Надеюсь, это имело смысл.
Вы спросили, как комерческие игры делают это. ;)
источник
Более серьезный ответ:
Я видел, как часто использовали доски. Простые версии - это не что иное, как распорки, которые обновляются такими вещами, как HP объекта, которые затем могут запрашивать объекты.
Ваши классные доски могут быть либо представлением мира об этой сущности (спросите у доски B, каков его HP), либо представлением субъекта о мире (A запрашивает свою доску, чтобы узнать, какова цель HP A).
Если вы обновляете классные доски только в точке синхронизации в фрейме, вы можете прочитать их позже в любом потоке, что делает многопоточность довольно простой для реализации.
Более сложные классные доски могут больше походить на хеш-таблицы, отображающие строки в значения. Это более ремонтопригодно, но, очевидно, требует затрат времени выполнения.
Классическая доска традиционно является только односторонней связью - она не справится с нанесением ущерба.
источник
long long int
схожи или похожи в чистой системе ECS.)Я немного изучил эту проблему и нашел хорошее решение.
В основном это все о подсистемах. Это похоже на идею классной доски, упомянутую tenpn.
Сущности состоят из компонентов, но они являются только имущественными пакетами. В самих сущностях поведение не реализовано.
Допустим, у сущностей есть компонент Health и компонент Damage.
Затем у вас есть несколько MessageManager и три подсистемы: ActionSystem, DamageSystem, HealthSystem. В какой-то момент ActionSystem вычисляет игровой мир и генерирует событие:
Это событие публикуется в MessageManager. Теперь в определенный момент времени MessageManager просматривает ожидающие сообщения и обнаруживает, что DamageSystem подписалась на сообщения HIT. Теперь MessageManager доставляет сообщение HIT в DamageSystem. DamageSystem просматривает список сущностей, имеющих компонент Damage, вычисляет очки урона в зависимости от силы удара или какого-либо другого состояния обеих сущностей и т. Д. И публикует событие
HealthSystem подписалась на сообщения DAMAGE, и теперь, когда MessageManager публикует сообщение DAMAGE в HealthSystem, HealthSystem имеет доступ к обеим сущностям entity_A и entity_B со своими компонентами Health, поэтому снова HealthSystem может выполнять свои вычисления (и, возможно, публиковать соответствующее событие в MessageManager).
В таком игровом движке формат сообщений является единственной связью между всеми компонентами и подсистемами. Подсистемы и объекты полностью независимы и не знают друг друга.
Я не знаю, реализовал ли какой-то реальный игровой движок эту идею или нет, но она кажется довольно солидной и чистой, и я надеюсь когда-нибудь реализовать ее самостоятельно для моего игрового движка уровня хобби.
источник
entity_b->takeDamage();
)Почему бы не иметь глобальную очередь сообщений, что-то вроде:
С участием:
И в конце игрового цикла / обработки событий:
Я думаю, что это шаблон командования. И
Execute()
является чисто виртуальным вEvent
, производные которого определяют и делают вещи. Так вот:источник
Если ваша игра однопользовательская, просто используйте метод целевых объектов (как предложил tenpn).
Если вы (или хотите поддерживать) многопользовательский режим (если быть точным, мультиклиент), используйте очередь команд.
источник
Я бы сказал: не используйте ни того, ни другого, если вам явно не нужна мгновенная обратная связь от повреждения.
Получающий урон объект / компонент / что-либо должно выдвигать события либо в локальную очередь событий, либо в систему на равном уровне, который содержит повреждения-события.
Затем должна быть наложенная система с доступом к обоим объектам, которая запрашивает события у объекта a и передает его объекту b. Не создавая общую систему событий, которую что-либо может использовать из любого места для передачи события чему-либо в любое время, вы создаете явный поток данных, который всегда облегчает отладку кода, облегчает измерение производительности, облегчает понимание и чтение и часто приводит к более хорошо продуманной системе в целом.
источник
Просто позвони. Не делайте request-hp, следующий за query-hp - если вы будете следовать этой модели, вы попадете в мир боли.
Возможно, вы захотите взглянуть и на Mono Continuations. Я думаю, что это было бы идеально для NPC.
источник
Так что же произойдет, если у нас есть игрок A и B, пытающийся ударить друг друга в одном и том же цикле update ()? Предположим, что обновление () для игрока A происходит до обновления () для игрока B в цикле 1 (или тика, или как вы его называете). Есть два сценария, о которых я могу думать:
Немедленная обработка через сообщение:
Это несправедливо, игрок A и B должны ударить друг друга, игрок B умер до удара A только потому, что этот объект / объект игры получил update () позже.
Очередь сообщения
Опять же, это несправедливо. Игрок А должен брать очки жизни в один и тот же ход / цикл / тик!
источник
pEntity->Flush( pMessages );
. Когда entity_A генерирует новое событие, он не читается entity_B в этом кадре (у него тоже есть шанс принять зелье), затем оба получают урон, и после этого они обрабатывают сообщение об исцелении зельем, которое будет последним в очереди , Игрок B все равно умирает, поскольку сообщение с зельем является последним в очереди: P, но оно может быть полезно для других видов сообщений, таких как очистка указателей для мертвых объектов.