Как обновить данные компонентов и информировать системы в рамках ECS?

Я разрабатываю свой собственный игровой движок на основе ECS.
Это структура ECS, которую я использую:

  • Entity: просто идентификатор, который связывает его компоненты.
  • Компонент: структура, которая хранит чистые данные, а не методы вообще (поэтому я могу написать .xsd для описания компонентов и автоматического создания структурного кода C ++).
  • Система: обрабатывать игровую логику.
  • EventDispacher: отправка событий подписчикам (системы)

но я не понимаю, как системы должны обновлять элементы компонентов и информировать другие системы?
Например, у меня есть TransformComponent, как это:

struct TransformComponent
{
Vec3 m_Position;
Float m_fScale;
Quaternion m_Quaternion;
};

Очевидно, что если какой-либо элемент TransformComponent визуализируемой сущности изменяется, RenderSystem также должна обновить унифицированную форму шейдера «worldMatrix» перед рендерингом следующего кадра.
Так что, если я делаю «comp-> m_Position = …» в системе, как RenderSystem должна «заметить» изменение TransformComponent? Я придумал 3 решения:

  1. Отправьте UpdateEvent после обновления участников и обработайте событие в связанной Системе. Это ужасно, потому что, как только система модифицирует данные компонента, она должна отправить событие, подобное этому:

    {
    ...;
    TransformComponent* comp = componentManager.GetComponent<TransformComponent>(entityId);
    comp->m_Position = ...;
    comp->m_Quaternion = ...;
    eventDispatcher.Send<TransformUpdateEvent>(...);
    ...;
    }
    
  2. Сделайте члены частными, и для каждого класса компонентов напишите соответствующую систему с методами set / get (перенос события в методы set). Это принесет много громоздких кодов.

  3. Ничего не меняйте, но добавьте компонент «Подвижный». RenderSystem будет итеративно выполнять обновление для визуализируемых объектов с помощью компонента «Movable» в методе Update (). Это может не решить другие подобные проблемы, и я не уверен в производительности.

Я не могу придумать элегантный способ решить эту проблему. Должен ли я изменить свой дизайн?

3

Решение

Я думаю, что в этом случае самый простой способ будет лучшим: вы можете просто сохранить указатель на Transform компонент в компонентах, которые читают / пишут это.

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

  1. Transform Компонент очень прост — это не то, что будет изменено в процессе разработки. Абстрагирование доступа к нему на самом деле сделает код более сложным и сложным в обслуживании.

  2. Transform это компонент, который будет часто меняться для многих объектов, возможно, даже большинство ваших объектов будет обновлять его каждый кадр. Отправка событий каждый раз, когда происходит изменение, имеет определенную стоимость — вероятно, намного выше, чем простое копирование матрицы / вектора / кватерниона из одного места в другое.

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

  4. Как правило, средства визуализации просто копируют все матрицы отображаемых объектов в каждом кадре. Нет смысла кэшировать их в системе рендеринга.

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


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

  1. Update() в которых системы могут модифицировать компоненты, и

  2. PostUpdate() в которых системы могут только читать данные из компонентов

Например PhysicsSystem::Update() может скопировать данные преобразования в соответствующие TransformComponent компоненты, а затем RenderSystem::PostUpdate() мог просто прочитать с TransformComponent, без риска использования устаревших данных.

2

Другие решения

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

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

  2. Ваше решение 2 следует тому, что мы обсуждали в 1. Но оно обнаруживает проблему в этом общем подходе. Если вы обновляете ваш TransformComponent в нескольких системах, вы не можете знать, действительно ли TransformComponent изменился, пока последняя система не обновила его, потому что одна система могла переместить его в одном направлении, а другая могла переместить его назад, позволяя как в начале вашего тика. Вы можете решить эту проблему, обновив свой TransformComponent только один раз, в одной системе …

  3. Что похоже на ваше решение 3. Но, может быть, наоборот. Вы можете обновлять MovableComponent в нескольких системах, а затем в своем конвейере ECS, иметь единую систему, считывая ваш MovableComponent и записывая в ваш TransformComponent. В этом случае важно, чтобы в TransformComponents разрешалось писать только одной Системе. В то время, имея логическое значение, указывающее, был ли он перемещен или нет, отлично справился бы с этой задачей.

До этого момента мы торговали производительностью (потому что мы избегаем некоторой обработки в RenderSystem, когда TransformComponent не изменился) для памяти (потому что мы каким-то образом дублируем содержимое TransformComponent.

  • Другой способ сделать то же самое, не добавляя события, логические значения или компоненты, — это делать все в RenderSystem. По сути, в каждом RenderComponent вы можете сохранить копию (или хэш) вашего TransformComponent с момента последнего обновления, а затем сравнить его. Если это не то же самое, сделайте это и обновите свою копию.
// In your RenderSystem...

if (renderComponent.lastTransformUpdate == transformComponent) {
continue;
}
renderComponent.lastTransformUpdate = transformComponent;
render(renderComponent);

Это последнее, будет моим предпочтительным решением. Но это также зависит от характеристик вашей системы и ваших проблем. Как всегда, не пытайтесь слепо выбирать производительность. Сначала измерить, а потом сравнить.

1