Я разрабатываю собственный игровой движок на платформе 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 решения:
Отправьте UpdateEvent после обновления членов и обработайте событие в связанном System. Это уродливо, потому что, как только система изменяет данные компонента, она должна отправить событие, подобное этому:
{ ...; TransformComponent* comp = componentManager.GetComponent<TransformComponent>(entityId); comp->m_Position = ...; comp->m_Quaternion = ...; eventDispatcher.Send<TransformUpdateEvent>(...); ...; }
Сделайте члены закрытыми и для каждого класса компонента напишите соответствующую систему с методами set / get (обертывание отправки событий в методах set). Это принесет много громоздких кодов.
Ничего не меняйте, но добавьте компонент «Подвижный». RenderSystem будет итеративно обновлять визуализируемые объекты с помощью компонента «Movable» в методе Update (). Это может не решить другие подобные проблемы, и я не уверен в производительности.
Я не могу придумать элегантного способа решить эту проблему. Стоит ли менять свой дизайн?
2 ответа
Я думаю, что в этом случае самый простой метод будет лучшим: вы можете просто сохранить указатель на компонент Transform
в компонентах, которые его читают / записывают.
Я не думаю, что использование событий (или других косвенных действий, например, наблюдателей) решает здесь настоящую проблему.
Компонент
Transform
очень прост - это не то, что будет меняться в процессе разработки. Абстрагирование доступа к нему на самом деле сделает код более сложным и трудным в поддержке.Transform
- это компонент, который будет часто изменяться для многих объектов, возможно, даже большинство ваших объектов будут обновлять его каждый кадр. Отправка событий каждый раз при изменении имеет стоимость - вероятно, намного выше, чем простое копирование матрицы / вектора / кватерниона из одного места в другое.Я думаю, что использование событий или какой-либо другой абстракции не решит другие проблемы, например, несколько компонентов, обновляющих один и тот же компонент
Transform
, или компоненты, использующие устаревшие данные преобразования.Как правило, рендереры просто копируют все матрицы рендеринговых объектов каждый кадр. Нет смысла кэшировать их в системе рендеринга.
Часто используются такие компоненты, как Transform
. Их чрезмерная сложность может быть проблемой во многих различных частях движка, тогда как использование простейшего решения - указателя - даст вам большую свободу.
Кстати, есть также очень простой способ убедиться, что RenderComponent
прочитает преобразование после его обновления (например, от PhysicsComponent
) - вы можете разделить работу на два этапа:
Update()
, в которых системы могут изменять компоненты, иPostUpdate()
, в котором системы могут считывать данные только из компонентов
Например, PhysicsSystem::Update()
может копировать данные преобразования в соответствующие компоненты TransformComponent
, а затем RenderSystem::PostUpdate()
может просто читать из TransformComponent
без риска использования устаревших данных.
Думаю, здесь есть что учесть. Я пойду по частям, сначала обсудим ваши суждения.
О вашем решении 1. Учтите, что вы можете сделать то же самое с логическим значением или назначить пустой компонент, действующий как тег. Во многих случаях использование событий в ECS чрезмерно усложняет архитектуру вашей системы. По крайней мере, я стараюсь этого избегать, особенно в небольших проектах. Помните, что компонент, действующий как тег, можно рассматривать как событие.
Ваше решение 2 является продолжением того, что мы обсуждали в 1. Но оно выявляет проблему, связанную с этим общим подходом. Если вы обновляете свой компонент TransformComponent в нескольких системах, вы не можете узнать, действительно ли TransformComponent изменился, пока последняя система не обновила его, потому что одна система могла переместить его в одном направлении, а другая могла переместить его назад, позволив ему как в начале вашего тика. Вы можете решить эту проблему, обновив TransformComponent только один раз в одной системе ...
Что похоже на ваше решение 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);
Последнее было бы моим предпочтительным решением. Но это также зависит от характеристик вашей системы и ваших проблем. Как всегда, не пытайтесь слепо выбирать производительность. Сначала измерьте, а потом сравните.
Новые вопросы
c++
C ++ - это язык программирования общего назначения. Первоначально он был разработан как расширение C и имеет аналогичный синтаксис, но теперь это совершенно другой язык. Используйте этот тег для вопросов о коде (который будет скомпилирован с помощью компилятора C ++). Используйте тег, зависящий от версии, для вопросов, связанных с конкретной редакцией стандарта [C ++ 11], [C ++ 14], [C ++ 17] или [C ++ 20] и т. Д.