Как правильно реализовать обработку сообщений в системе компонентов на основе компонентов?


30

Я реализую вариант системы сущностей, который имеет:

  • Класс сущностей , который немного больше , чем ID , который связывает компоненты вместе

  • Группа классов компонентов , которые не имеют «компонентной логики», только данные

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

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

Первоначальный вариант обработки системных сообщений был примерно таким:

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

    messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))

    всякий раз, когда объект перемещается)

  3. Каждая система, подписавшаяся на конкретное сообщение, получает свой метод обработки сообщений, называемый

  4. Если система обрабатывает событие, а логика обработки события требует, чтобы было передано другое сообщение, сообщение сразу же транслируется, и вызывается другая цепочка методов обработки сообщения.

Этот вариант был в порядке, пока я не начал оптимизировать систему обнаружения столкновений (она становилась очень медленной по мере увеличения числа объектов). Сначала он будет просто повторять каждую пару сущностей, используя простой алгоритм перебора. Затем я добавил «пространственный индекс», в котором есть сетка ячеек, в которой хранятся объекты, которые находятся внутри области определенной ячейки, что позволяет выполнять проверки только для объектов в соседних ячейках.

Каждый раз, когда объект перемещается, система столкновений проверяет, сталкивается ли объект с чем-то в новой позиции. Если это так, столкновение обнаруживается. И если оба сталкивающихся объекта являются «физическими объектами» (они оба имеют компонент RigidBody и предназначены для отталкивания друг друга, чтобы не занимать одно и то же пространство), выделенная система отделения твердого тела просит систему перемещения переместить объекты в некоторые конкретные позиции, которые будут разделять их. Это, в свою очередь, заставляет систему перемещения отправлять сообщения, уведомляющие об изменении позиций объекта. Система обнаружения столкновений должна реагировать, потому что ей необходимо обновить свой пространственный индекс.

В некоторых случаях это вызывает проблему, потому что содержимое ячейки (универсальные объекты List of Entity в C #) изменяется во время итерации, в результате чего итератор создает исключение.

Итак ... как я могу предотвратить прерывание системы столкновений при проверке столкновений?

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

Что я пробовал:

  • Очереди входящих сообщений . Каждый раз, когда какая-либо система передает сообщение, оно добавляется в очереди сообщений систем, которые заинтересованы в нем. Эти сообщения обрабатываются, когда обновление системы вызывается каждый кадр. Проблема : если система A добавляет сообщение в очередь системы B, это работает хорошо, если система B должна быть обновлена ​​позже, чем система A (в том же фрейме игры); в противном случае это приводит к тому, что сообщение обрабатывается в следующем игровом кадре (не желательно для некоторых систем).
  • Очереди исходящих сообщений . Пока система обрабатывает событие, любые сообщения, которые она передает, добавляются в очередь исходящих сообщений. Сообщения не должны ждать обработки обновления системы: они обрабатываются «сразу» после того, как начальный обработчик сообщений завершил свою работу. Если при обработке сообщений происходит рассылка других сообщений, они также добавляются в исходящую очередь, поэтому все сообщения обрабатываются в одном и том же кадре. Проблема: если система времени жизни объекта (я реализовал управление временем жизни объекта с помощью системы) создает объект, он уведомляет об этом некоторые системы A и B. В то время как система A обрабатывает сообщение, она вызывает цепочку сообщений, которые в конечном итоге приводят к уничтожению созданного объекта (например, объект пули был создан именно там, где он сталкивается с некоторым препятствием, что приводит к самоуничтожению пули). Пока цепочка сообщений разрешается, система B не получает сообщение о создании объекта. Таким образом, если система B также заинтересована в сообщении об уничтожении объекта, она получает его, и только после того, как "цепочка" завершает разрешение, она получает сообщение о создании исходного объекта. Это приводит к тому, что сообщение уничтожения игнорируется, а сообщение создания принимается,

РЕДАКТИРОВАТЬ - ОТВЕТЫ НА ВОПРОСЫ, КОММЕНТАРИИ:

  • Кто изменяет содержимое ячейки, пока система столкновений перебирает их?

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

  • Не можете ли вы работать с глобальной очередью исходящих сообщений?

Я недавно попробовал одну глобальную очередь. Это вызывает новые проблемы. Проблема: я перемещаю танковый объект в настенный объект (танк управляется с помощью клавиатуры). Тогда я решаю сменить направление движения танка. Чтобы отделить резервуар от каждой рамы, система CollidingRigidBodySeparationSystem отодвигает резервуар от стены на минимально возможное количество. Направление разъединения должно быть противоположным направлению движения танка (когда начинается игра, танк должен выглядеть так, как будто он никогда не двигался в стену). Но направление становится противоположным НОВОМУ направлению, таким образом перемещая резервуар к другой стороне стены, чем это было первоначально. Почему возникает проблема: Вот как теперь обрабатываются сообщения (упрощенный код):

public void Update(int deltaTime)
{   
    m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
    while (m_messageQueue.Count > 0)
    {
        Message message = m_messageQueue.Dequeue();
        this.Broadcast(message);
    }
}

private void Broadcast(Message message)
{       
    if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
    {
        // NOTE: all IMessageListener objects here are systems.
        List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
        foreach (IMessageListener listener in messageListeners)
        {
            listener.ReceiveMessage(message);
        }
    }
}

Код выглядит следующим образом (предположим, что это не первый игровой кадр):

  1. Системы начинают обработку TimePassedMessage
  2. InputHandingSystem преобразует нажатия клавиш в действие объекта (в этом случае стрелка влево превращается в действие MoveWest). Действие объекта сохраняется в компоненте ActionExecutor
  3. ActionExecutionSystem , в ответ на действие сущности, добавляет MovementDirectionChangeRequestedMessage в конец очереди сообщений.
  4. MovementSystem перемещает позицию объекта на основе данных компонента Velocity и добавляет сообщение PositionChangedMessage в конец очереди. Движение выполняется с использованием направления движения / скорости предыдущего кадра (скажем, на север)
  5. Системы прекращают обработку TimePassedMessage
  6. Системы начинают обработку MovementDirectionChangeRequestedMessage
  7. MovementSystem изменяет скорость объекта / направление движения в соответствии с запросом
  8. Системы прекращают обработку MovementDirectionChangeRequestedMessage
  9. Системы начинают обработку PositionChangedMessage
  10. CollisionDetectionSystem обнаруживает, что из-за перемещения объекта он столкнулся с другим объектом (танк прошел внутри стены). Он добавляет CollisionOccuredMessage в очередь
  11. Системы прекращают обработку PositionChangedMessage
  12. Системы начинают обработку CollisionOccuredMessage
  13. CollidingRigidBodySeparationSystem реагирует на столкновение, разделяя резервуар и стенку. Поскольку стена неподвижна, перемещается только танк. Направление движения танков используется как индикатор того, откуда танк пришел. Это смещено в противоположном направлении

ОШИБКА: Когда танк перемещал этот кадр, он двигался, используя направление движения от предыдущего кадра, но когда он был отделен, использовалось направление движения от ЭТОГО кадра, даже если он уже отличался. Это не так, как это должно работать!

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

  • Возможно, вы захотите прочитать gamadu.com/artemis, чтобы увидеть, что они сделали с Аспектами, с какой стороны решаются некоторые проблемы, с которыми вы сталкиваетесь.

На самом деле, я давно знаком с Артемидой. Изучил его исходный код, прочитал форумы и т. Д. Но я видел, что «Аспекты» упоминаются лишь в нескольких местах, и, насколько я понимаю, они в основном означают «Системы». Но я не вижу, как сторона Артемиды решает некоторые из моих проблем. Он даже не использует сообщения.

  • Смотрите также: «Связь между сущностями: очередь сообщений против публикации / подписки против сигнала / слотов»

Я уже прочитал все вопросы gamedev.stackexchange, касающиеся систем сущностей. Этот, кажется, не обсуждает проблемы, с которыми я сталкиваюсь. Я что-то пропустил?

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

Я не уверен, что ты имеешь в виду. Более старые реализации CollisionDetectionSystem просто проверяли наличие коллизий при обновлении (когда обрабатывалось TimePassedMessage), но мне приходилось минимизировать проверки настолько, насколько я мог из-за производительности. Поэтому я переключился на проверку столкновений при перемещении объекта (большинство объектов в моей игре статичны).


Есть кое-что, что мне не понятно. Кто изменяет содержимое ячейки, пока система столкновений перебирает их?
Пол Манта

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

Если вы хотите сохранить этот замысловатый дизайн, вы должны следовать @RoyT. Совет, это единственный способ (без сложных, основанных на времени сообщений) справиться с вашей проблемой секвенирования. Возможно, вы захотите прочитать gamadu.com/artemis, чтобы увидеть, что они сделали с Аспектами, с какой стороны решаются некоторые проблемы, с которыми вы сталкиваетесь.
Патрик Хьюз


2
Возможно, вы захотите узнать, как Axum сделал это, загрузив CTP и скомпилировав некоторый код, а затем обратный инжиниринг результата в C # с использованием ILSpy. Передача сообщений - важная особенность языков актерской модели, и я уверен, что Microsoft знает, что они делают, поэтому вы можете обнаружить, что у них была «лучшая» реализация.
Джонатан Дикинсон

Ответы:


12

Вы, наверное, слышали об анти-паттерне объекта God / Blob. Ну, твоя проблема - это цикл Бога / Blob. Приспособление к вашей системе передачи сообщений в лучшем случае обеспечит решение проблемы Band-Aid, а в худшем случае - пустая трата времени. На самом деле, ваша проблема не имеет ничего общего с разработкой игр вообще. Я поймал себя на том, что пытался изменить коллекцию, повторяя ее несколько раз, и решение всегда одинаково: подразделять, подразделять, подразделять.

Как я понимаю формулировку вашего вопроса, ваш метод обновления вашей системы коллизий в настоящее время выглядит примерно следующим образом.

for each possible collision
    check for collision
    handle collision
    modify collision world to reflect change // exception happens here

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

for each possible collision
    check for collision, record it if a collision occurs

for each found collision
    handle collision, record the collision response (delete object, ignore, etc.)

for each collision response
    modify collision world according to response

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

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


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

11

Как правильно реализовать обработку сообщений в системе компонентов на основе компонентов?

Я бы сказал, что вам нужны два типа сообщений: синхронные и асинхронные. Синхронные сообщения обрабатываются немедленно, тогда как асинхронные обрабатываются не в одном и том же кадре стека (но могут обрабатываться в одном и том же игровом кадре). Решение, которое обычно принимается на основе «для каждого класса сообщений», например «все сообщения EnemyDied являются асинхронными».

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

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

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

Подумайте об этом: в природе синхронной (немедленно обрабатывать все последствия какого-либо действия) и системы сообщений (отсоединение получателя от отправителя, чтобы отправитель не знал, кто реагирует на действия), вы не сможете легко это сделать. Найдите такие петли. Я говорю следующее: будьте готовы много справляться с подобной самоизменяющейся итерацией. Своего рода «по замыслу». ;-)

как я могу предотвратить прерывание системы столкновений во время проверки на наличие столкновений?

Для вашей конкретной проблемы с обнаружением коллизий может быть достаточно сделать асинхронные события коллизий, поэтому они ставятся в очередь до тех пор, пока менеджер коллизий не будет завершен и не будет выполнен как один пакет впоследствии (или в какой-то более поздний момент в кадре). Это ваше решение "входящая очередь".

Проблема: если система A добавляет сообщение в очередь системы B, это работает хорошо, если система B должна быть обновлена ​​позже, чем система A (в том же фрейме игры); в противном случае это приводит к тому, что сообщение обрабатывается в следующем игровом кадре (не желательно для некоторых систем)

Легко:

while (! queue.empty ()) {queue.pop (). handle (); }

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


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

5

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

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

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

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

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


1
+1, но я не могу поверить, что у этого подхода нет недостатков. Разве это не заставляет нас жестко кодировать взаимозависимости между системами? Или, может быть, эти взаимозависимости должны быть жестко, так или иначе?
Патрик Чачурски

2
@Daedalus: если игровая логика нуждается в обновлениях физики, чтобы сделать правильную логику, как вы не будете иметь эту зависимость? Даже с моделью pubsub вы должны явно подписаться на такой-то тип сообщения, который генерируется только какой-то другой системой. Избегать зависимостей сложно, и в основном это просто выяснение правильных слоев. Графика и физика, например, независимы, но будет слой клея более высокого уровня, который обеспечит отражение интерполированных обновлений симуляции физики в графике и т. Д.
Шон Миддлдич

Это должен быть принятый ответ. Простой способ добиться этого - просто создать компонент нового типа, например, CollisionResolvable, который будет обрабатываться каждой системой, заинтересованной в выполнении действий после столкновения. Это хорошо вписывается в предложение Дрейка, однако существует система для каждого цикла подразделения.
user8363
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.