Использование архитектуры системы сущностей с параллелизмом на основе задач

9

Фон

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

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

Примечание: я использую C ++ 11 для этого проекта, но я думаю, что большая часть того, что я спрашиваю, должна быть довольно независимой от языка.

Возможное решение

Иметь глобальный EntityManager, который используется для создания и управления Entity и EntityAttributes. Разрешите доступ к ним только для чтения на этапе обновления и сохраняйте все изменения в очереди для каждого потока. Как только все задачи выполнены, очереди объединяются, и изменения в каждой применяются. Это могло бы иметь проблемы с несколькими записями в одни и те же поля, но я уверен, что может быть система приоритетов или отметка времени, чтобы уладить это. Мне кажется, что это хороший подход, потому что системы могут уведомляться об изменениях сущностей довольно естественно на этапе распространения изменений.

Вопрос

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

Любая обратная связь / совет будет высоко ценится! Спасибо!

связи

Росс Хейс
источник

Ответы:

12

Вилка-Join

Вам не нужны отдельные копии компонентов. Просто используйте модель fork-join, которая (крайне плохо) упоминается в этой статье от Intel.

В ECS у вас фактически есть цикл что-то вроде:

while in game:
  for each system:
    for each component in system:
      update component

Измените это на что-то вроде:

while in game:
  for each system:
    divide components into groups
    for each group:
      start thread (
        for each component in group:
          update component
      )
    wait for all threads to finish

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

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

Обратите внимание, что «fork-join» не означает использование fork()(которое создает процессы, а не потоки) и не подразумевает, что вы действительно должны присоединиться к потокам. Это просто означает, что вы берете одну задачу, разбиваете ее на более мелкие части для обработки вашим пулом рабочих потоков, а затем ждете обработки всех посылок.

Доверенные

Этот подход можно использовать сам по себе или в сочетании с методом fork-join, чтобы сделать необходимость строгого разделения менее важной.

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

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

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

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

Шон Миддледич
источник
Я читал кое-что о ветвлении, о котором вы упоминали ранее, и у меня сложилось впечатление, что, хотя он и позволяет вам использовать некоторый параллелизм, существуют ситуации, когда некоторые рабочие потоки могут ожидать завершения одной группы. В идеале я пытаюсь избежать этой ситуации. Идея прокси интересна и немного напоминает то, над чем я работал. Entity имеет EntityAttributes, и они являются обертками для фактических значений, хранящихся в сущности. Значения могут быть прочитаны из них в любое время, но установлены только в определенное время и могут содержать значение прокси в атрибуте, правильно?
Росс Хейс
1
Есть большая вероятность, что, пытаясь избежать ожидания, вы тратите так много времени на анализ графика зависимости, что вы теряете время в целом.
Патрик Хьюз
@roflha: да, вы можете разместить прокси на уровне EntityAttribute. Или создайте отдельный объект со вторым набором атрибутов. Или просто отбросьте концепцию атрибутов и используйте менее детальный дизайн компонентов.
Шон Мидлдич
@SeanMiddleditch Когда я говорю «атрибут», я имею в виду, я думаю, компоненты. Атрибуты - это не просто отдельные значения, такие как числа с плавающей точкой и строки, если это то, что я сделал так, чтобы это звучало. Скорее это классы, которые содержат конкретную информацию, такую ​​как PositionAttribute. Если компонент является принятым именем для этого, то, возможно, я должен изменить. Но вы бы порекомендовали прокси на уровне сущности, а не на уровне компонента / атрибута?
Росс Хейс
1
Я рекомендую все, что вы найдете проще всего реализовать. Просто помните, что в этом случае я мог бы запрашивать прокси-серверы без каких-либо блокировок, без использования каких-либо элементов и без тупиков.
Шон Мидлдич