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

  • Сущность: просто идентификатор, который соединяет его компоненты.
  • Компонент: структура, в которой хранятся чистые данные, без каких-либо методов (поэтому я могу написать .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 после обновления членов и обработайте событие в связанном System. Это уродливо, потому что, как только система изменяет данные компонента, она должна отправить событие, подобное этому:

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

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

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

3
Unicorn 23 Окт 2018 в 14:02

2 ответа

Лучший ответ

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

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

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

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

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

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

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


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

  1. Update(), в которых системы могут изменять компоненты, и

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

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

3
joe_chip 24 Окт 2018 в 00:42

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

  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
José Manuel 23 Окт 2018 в 12:21
52947430