
Архитектура ECS замечательна, потому что она очень производительна для обработки большого количества данных в отношении локальности памяти и поддерживает многопоточность — и это только поверхностные преимущества. Неудивительно, что в прошлом году Unity представила новую ECS, и Lumberyard также отказывается от архитектуры, унаследованной от CryEngine, в пользу собственной ECS.
Но бывают случаи, когда вы на самом деле не хотите обрабатывать тонны данных. Бывают случаи, когда из тысячи объектов хочется работать лишь с несколькими из них.
Что мы обнаружили в Mana Engine, так это то, что большую часть времени, когда вы хотите работать над некоторыми — но не всеми — вашими компонентами, это может быть связано с одним из трех событий: Создание, Обновление и Уничтожение.
Из-за этого Mana Engine поддерживает знание того, когда ваши компоненты были созданы, изменены или обновлены. Многие кодовые базы могут делать это через делегата (т.е. OnComponentCreated) или через системы обмена сообщениями, которые требуют настройки обработчиков и всех видов шаблонного кода.
В Mana Engine каждый AuthorizedSystem, имеющий доступ к данному типу компонента, по своей сути имеет доступ к списку компонентов, которые были созданы, изменены или уничтожены. Не суетись. Нет настройки. Без крючков и манипуляторов. Никаких обратных вызовов. Просто набор хэшей EntityIds уже предоставлен вам.
Это работает примерно так:
class MySystem : public AuthorizedSystem<MyComp1, MyComp2>
{
void Update()
{
// Grab access to the components we want.
auto worldProxy = GetWorldProxy<MyComp1, MyComp2>();
//For each id in the created set...
for( EntityId id : GetCreatedSet<MyComp1>())
{ // ^ Magic happens here.
//Find all components by this Id.
EntityView<MyComp1, MyComp2> ev = worldProxy.Get(id);
// If it is a fully formed EntityView...
if(ev)
{
//Do something with the entity.
}
}
}
};
Разберем код.
- У нас есть
AuthorizedSystem, у которого есть права на запись как вMyComp1, так и вMyComp2. - В вызове
Update()мы получаемWorldProxyиз двух компонентов. Мы хотим знать, есть ли какие-либо вновь созданные компонентыMyComp1в этом кадре, поэтому мы вызываемGetCreatedSet<MyComp1>и перебираем их. - Внутри цикла мы хотим работать только с завершенными EntityView, что означает, что для них существуют все компоненты. Поэтому мы вызываем
worldProxy.Get(id)и проверяем полнотуEntityView’sперед его использованием.EntityViewимеет логический оператор, позволяющий нам использовать синтаксисif(ev)для проверки его полноты. - Наконец, мы что-то делаем с этим
EntityView.
И вы можете делать то же самое с OnChangedSet и OnDestroyedSet.
Mana Engine ведет всю бухгалтерию автоматически. Иногда, однако, эта бухгалтерия может быть дорогой. Иногда вы просто хотите перебрать все, что там есть, измененное или нет. При определении компонента есть ключевое слово, которое позволяет вам это сделать.
Частью этого является философия не платить за то, что вам не нужно, и иметь гибкость для достижения того, чего вы хотите, не мешая вам.
Вот отличный пример, когда это проявляется в пиках.
В нашей тестовой игре одним из стресс-тестов является рендеринг более 100 000 движущихся объектов. Когда мы это сделаем, мы можем предположить, что в каждом кадре объекты будут иметь новое значение в своих данных преобразования. Мы не хотим записывать тот факт, что более 100 000 этих преобразований изменились. Вместо этого мы просто предполагаем, что они соответствующим образом изменяют и обновляют свои буферы графического процессора.
Однако в другом случае мы хотим визуализировать тысячи статических объектов и несколько динамических объектов. При этом статичные объекты почти никогда не двигаются. Таким образом, вместо того, чтобы обновлять их буферы графического процессора каждый кадр, мы хотим обновлять только те изменения, которые, как мы знаем, произошли.
Для этого у нас есть два компонента. StaticModel и DynamicModel. Они почти одинаковы, но с одним отличием в поведении.
struct StaticModel
{
CHANGED_LIST_COMPONENT_DEFINITION();
matrix transform;
ModelId model;
};
struct DynamicModel
{
COMPONENT_DEFINITION();
matrix transform;
ModelId model;
};
Обратите внимание, что StaticModel помечен CHANGED_LIST_COMPONENT_DEFINITION(), а DyanmicModel помечен COMPONENT_DEFINITION().
Что лучше, так это то, что, поскольку он становится constexpr static bool в компоненте, мы можем использовать операторы if constexpr и static_asserts, чтобы знать во время компиляции, пытаетесь ли вы получить доступ к измененному набору, который не существует. Вот так:
void Update()
{
auto wp = GetWorldProxy<DynamicModel>();
for(auto&& id : GetChangedSet<DynamicModel>() )
{ // ^ Compile time error.
// Do something with it...
}
}
Обратите внимание, что названия «Статический» и «Динамический» указывают на данные преобразования этих объектов, а не на что-либо, связанное с их фактическими шейдерами или вершинами в их сетке. StaticModel может относиться к чему-то, что никогда не анимируется, например к камню. С таким же успехом это может относиться к чему-то анимированному, что (часто) не меняет своего положения, ориентации или масштаба, например, к качающемуся на ветру дереву.
На момент написания этой статьи возможность проверки созданных, измененных и уничтоженных компонентов была довольно примитивной. У нас есть планы расширить его и упростить использование синтаксиса для более сложных вариантов поведения, которые вам часто нужны вместе.
В качестве краткого примера, в приведенном выше коде мы получили CreatedSet из MyComp1, но более вероятно, что мы хотим знать, когда EntityView становится завершенным. Это означает, что мы должны сначала увидеть, есть ли какие-либо новые компоненты MyComp1, а затем проверить, есть ли какие-либо новые компоненты MyComp2, и если у одного из них есть новый компонент, проверить, существует ли вся сущность. Это становится большим количеством шаблонного кода, который, как мы знаем, может быть обернут и использован повторно.
Подобные основные функции являются строительными блоками того, чем суждено стать Mana Engine. Следите за новостями, чтобы узнать, что это такое!