Я реализую вариант системы сущностей, который имеет:
Класс сущностей , который немного больше , чем ID , который связывает компоненты вместе
Группа классов компонентов , которые не имеют «компонентной логики», только данные
Куча системных классов (ака «подсистемы», «менеджеры»). Они делают всю обработку логики объекта. В большинстве базовых случаев системы просто повторяют список интересующих их сущностей и выполняют действия для каждого из них.
Объект класса MessageChannel , который является общим для всех игровых систем. Каждая система может подписываться на определенный тип сообщений для прослушивания, а также может использовать канал для трансляции сообщений в другие системы.
Первоначальный вариант обработки системных сообщений был примерно таким:
- Запускать обновление в каждой игровой системе последовательно
Если система делает что-то с компонентом, и это действие может представлять интерес для других систем, система отправляет соответствующее сообщение (например, системные вызовы
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
всякий раз, когда объект перемещается)
Каждая система, подписавшаяся на конкретное сообщение, получает свой метод обработки сообщений, называемый
Если система обрабатывает событие, а логика обработки события требует, чтобы было передано другое сообщение, сообщение сразу же транслируется, и вызывается другая цепочка методов обработки сообщения.
Этот вариант был в порядке, пока я не начал оптимизировать систему обнаружения столкновений (она становилась очень медленной по мере увеличения числа объектов). Сначала он будет просто повторять каждую пару сущностей, используя простой алгоритм перебора. Затем я добавил «пространственный индекс», в котором есть сетка ячеек, в которой хранятся объекты, которые находятся внутри области определенной ячейки, что позволяет выполнять проверки только для объектов в соседних ячейках.
Каждый раз, когда объект перемещается, система столкновений проверяет, сталкивается ли объект с чем-то в новой позиции. Если это так, столкновение обнаруживается. И если оба сталкивающихся объекта являются «физическими объектами» (они оба имеют компонент 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);
}
}
}
Код выглядит следующим образом (предположим, что это не первый игровой кадр):
- Системы начинают обработку TimePassedMessage
- InputHandingSystem преобразует нажатия клавиш в действие объекта (в этом случае стрелка влево превращается в действие MoveWest). Действие объекта сохраняется в компоненте ActionExecutor
- ActionExecutionSystem , в ответ на действие сущности, добавляет MovementDirectionChangeRequestedMessage в конец очереди сообщений.
- MovementSystem перемещает позицию объекта на основе данных компонента Velocity и добавляет сообщение PositionChangedMessage в конец очереди. Движение выполняется с использованием направления движения / скорости предыдущего кадра (скажем, на север)
- Системы прекращают обработку TimePassedMessage
- Системы начинают обработку MovementDirectionChangeRequestedMessage
- MovementSystem изменяет скорость объекта / направление движения в соответствии с запросом
- Системы прекращают обработку MovementDirectionChangeRequestedMessage
- Системы начинают обработку PositionChangedMessage
- CollisionDetectionSystem обнаруживает, что из-за перемещения объекта он столкнулся с другим объектом (танк прошел внутри стены). Он добавляет CollisionOccuredMessage в очередь
- Системы прекращают обработку PositionChangedMessage
- Системы начинают обработку CollisionOccuredMessage
- CollidingRigidBodySeparationSystem реагирует на столкновение, разделяя резервуар и стенку. Поскольку стена неподвижна, перемещается только танк. Направление движения танков используется как индикатор того, откуда танк пришел. Это смещено в противоположном направлении
ОШИБКА: Когда танк перемещал этот кадр, он двигался, используя направление движения от предыдущего кадра, но когда он был отделен, использовалось направление движения от ЭТОГО кадра, даже если он уже отличался. Это не так, как это должно работать!
Чтобы предотвратить эту ошибку, старое направление движения должно быть где-то сохранено. Я мог бы добавить его в какой-то компонент, чтобы исправить эту конкретную ошибку, но разве этот случай не указывает на какой-то принципиально неправильный способ обработки сообщений? Почему система разделения должна заботиться о том, какое направление движения она использует? Как я могу решить эту проблему элегантно?
- Возможно, вы захотите прочитать gamadu.com/artemis, чтобы увидеть, что они сделали с Аспектами, с какой стороны решаются некоторые проблемы, с которыми вы сталкиваетесь.
На самом деле, я давно знаком с Артемидой. Изучил его исходный код, прочитал форумы и т. Д. Но я видел, что «Аспекты» упоминаются лишь в нескольких местах, и, насколько я понимаю, они в основном означают «Системы». Но я не вижу, как сторона Артемиды решает некоторые из моих проблем. Он даже не использует сообщения.
- Смотрите также: «Связь между сущностями: очередь сообщений против публикации / подписки против сигнала / слотов»
Я уже прочитал все вопросы gamedev.stackexchange, касающиеся систем сущностей. Этот, кажется, не обсуждает проблемы, с которыми я сталкиваюсь. Я что-то пропустил?
- Относитесь к двум случаям по-разному, для обновления сетки не нужно полагаться на сообщения о движении, так как это часть системы столкновений.
Я не уверен, что ты имеешь в виду. Более старые реализации CollisionDetectionSystem просто проверяли наличие коллизий при обновлении (когда обрабатывалось TimePassedMessage), но мне приходилось минимизировать проверки настолько, насколько я мог из-за производительности. Поэтому я переключился на проверку столкновений при перемещении объекта (большинство объектов в моей игре статичны).