Comments 17
Выглядит круто. Расскажите как обстоят дела с возможно самой печальной частью крутых проектов на С++ по сравнению с конкурентными подходами - как дела с времен компиляции?
Планируете ли приделывать сериализацию/десериализацию?
Ещё не хватает сравнения с классическими подходами(чисто на основе комбинации структур+векторов), насколько силён прирост в производительности?
PS: Подпишите пожалуйста что на финальной картинке "Lower values mean better performance". Спасибо за вклад в Open Source!
Привет!
Компиляция занимает секунды на моей машине.
Но чем больше будет компонентов тем дольше будет компиляция, спасибо темплейтам и тому как они быстро и классно собираются(нет) :harold:
https://github.com/wagnerks/ecss/actions/runs/18601409608/job/53040504015
если интересно: тут можно посмотреть сколько занимает build на гитхабовский серверах, в среднем в районе 30 секунд. Я потратил какое-то время на оптимизацию времени сборки, основной оверхед темплейты, и наверняка еще есть куда оптимизировать.
По поводу серализации - я думал над ней, но пока не дошли руки. Пришивать ее "жоско" к моей ECS не хочется. Вероятно сделаю отдельным модулем которому можно будет скормить реестр, и это будет выглядеть примерно так:
Serializer s;
Registry r;
auto obj = s.serialize(r);Мне просто не хочется смешивать ECS с чем-то ещё, особенно с вещами, которые можно реализовать поверх неё.
Например, сигналы: они есть во многих фреймворках, но их спокойно можно реализовать поверх ECS, не внутри неё - и такая реализация даже будет быстрее, чем generic-вариант, который создаёт оверхед даже если вы им не пользуетесь. А городить зоопарк выключателей мне тоже не хочется - я и так настрадался с опциональным включением thread safety :)
Про сравнения с классическими подходами: имеете ввиду c обычным вектором структур? Я делал такие сравнения пока оптимизировал, запишу себе добавить этот бенчмарк, у меня уже небольшой список вырисовывается :)
Надеюсь смогу к следующей статье (примерно через неделю), где как раз буду рассказывать про итерацию.
И за p.s. спасибо! поправил.
И за спасибо тоже спасибо))
тут можно посмотреть сколько занимает build на гитхабовский серверах, в среднем в районе 30 секунд
Для почти пустого целевого проекта(тоесть такого который использует ecss) это очень много, на первый взгляд. Хотя наверно для O2 может и норм. Чтобы не было потом неожиданностей с временем компиляции при разработке крупного(например, как стресс тест: 500 компонентов + 20000 архетипов) проекта хочется обширного исследования этой темы. Конечно понадобиться писать простые кодогенераторы, но оно того стоит - можно потом выдать ещё на публику рейтинг компиляторов по скорости компиляции и по производительности.
Про сравнения с классическими подходами: имеете ввиду c обычным вектором структур?
// да, что-то в духе вот этого:
struct t_node{vector<t_node> arr;vec3f offset,dir;string name;t_geom geom;};
struct t_tank{vec3f pos,v;float hp=100.0f;bool deaded=false;t_node n;};
struct t_bullet{vec3f pos,v;float dhp=-1.0;};
struct t_link{int a,b;float r=1.0f;};
struct t_network{
vector<QapColor> points_colors;
vector<vec3f> points;
vector<t_link> links;
QapColor links_color=0xffffffff;
QapColor points_color=0xffffffff;
float points_r=1.0f;
};
struct t_world{
vector<t_tank> tarr,dtarr;
vector<t_bullet> barr;
t_network net;
};
// t_node - можно выкинуть в одном из тестов для того чтобы было попроще: без деревьев.// ещё в недостижимом идеале, чтобы добить все остальные устаревшие подходы
// к построению архитектуры игровых движков можно сравнить производительность
// например с моими "древовидными системами сущностей"(2011 год):
// https://github.com/adler3d/MarketGame/blob/e8474691c5082c68e62790cfc5ac2440a830ed29/QapEngine/QapEntity.h#L260
struct QapItem:QapObject{
QapFactory*Factory;
QapList*Parent;
public:
#define DEF_PRO_CLASS_INFO(NAME,OWNER)NAME(QapItem)
#define DEF_PRO_METHOD(F)F(DoReset);
#define DEF_PRO_VARIABLE()\
ADDVAR(string,Caption,GetClassName()+"_Boss");\
ADDVAR(bool,Visible,true);\
ADDVAR(bool,Moved,true);\
ADDVAR(int,SaveID,0);\
ADDVAR(int,Tag,0);
...
};
struct QapList:QapItem{
vector<QapItem*> Items;
vector<QapItem*> DeadList;
...
};
// - это два базовые сущности от которых потом наследуются все остальные игровые объекты.PS: И я вам ещё достойного отечественного конкурента для сравнения нашёл/вспомнил.
https://wagnerks.github.io/ecss_benchmarks/ - я добавил в бенчи обычный вектор для сравнения, можете посмотреть
А как defrag работает на больших массивах? Один проход действительно хватает, чтобы не проседала скорость?
Привет!
Да, одного прохода действительно хватает. Алгоритм работает в O(N): я просто сканирую линейный упорядоченный массив секторов и “уплотняю” живые данные влево, пропуская мёртвые.
Так как структура уже отсортирована и не требует дополнительных поисков, дефрагментация - это один линейный проход без каких-либо вложенных операций, поэтому скорость не проседает.
тоесть у вас ентити без дерева? и вы ищите в линейности? но ведь суть в том что линейность есть и в дереве просто обойти от начала до конца, но вдобавок мы имеем не линейность за счет новых возможностей дерева, хотя может ваша ситуация так же быстра как BVH
тоесть просто ентити не интересно, у ентити должны быть описывающие классифицированные параметры подобны тем как в бвх, ну не по айди же будут происходить столкновения и выбор элемента
по началу я тоже думал ентити надо, потом погрузился в изучение деревьев и теперь я уверен в игре не ускорить линейностью как не дели эту линейность не так работает как дерево, по итогу выйдет так что эти принципы лежат и в обходе отрисовки и в обходе физики
тоесть 2-4 корня(уи,сцена)-деревья(рендер,рендер), они вершина иерархий, и по этим корням запускаем поиски в рантайме какие нужны
мы можем пойти дальше создать корень под текст, и отказаться от линейности в граффическом исполнении, и делать интерактив от указателя мышки по физике столкновений(для чего дерево бвх и помогает), как это достигать в линейном исполнении например? (перебор скорее всего)
Привет!
В моей ECS линейность относится только к размещению компонентов в памяти - чтобы итерация была максимально быстрой и кэш-френдли. Это не заменяет и не отменяет spatial-структур вроде BVH.
Системы, которым нужно искать отдельные объекты используют отдельные структуры данных - например, BVH, Octree, Quadtree, uniform grid и т. д. Они работают поверх ECS, а не внутри неё. У меня в движке, например, есть отдельная octree система в которой лежат entityid.
ECS хранит данные максимально плотно, а алгоритмы типа BVH используют эти данные.
Линейность по entityId никак не конфликтует с использованием деревьев для игрового мира. Она лишь ускоряет итерацию в системах, которым нужно пройти по компонентам в памяти.
Спасибо за статью. Возникли некоторые вопросы:
Большинство ECS пытаются хранить компоненты в SoA. Здесь же вы по сути делаете AoS. Чем это сильно лучше чем хранить std::vector<> структур содержащих те же самые поля?
Если хочется положить компоненты Position и Velocity рядом, то не логичнее ли сделать агрегатный компонент содержащий оба поля и в остальном пользоваться классическими SoA?
По бенчмарку, понятна ли причина отставания от flecs в части итераций?
Привет!
Технически SoA это следующая конструкция:
struct S {
float* x[];
float* y[];
float* z[];
};а ecs оперируют компонентами которые уже содержат несколько элементарных типов.
И прям по честному - ECS почти(возможно кто-то это сделал, но это ужас с точки зрения интерфейса) никогда не SoA, нужно разворачивать каким-то образом структуры компонентов на составляющие, укладывать их в памяти, а потом в системах еще и правильно эти данные использовать чтобы выжать максимум из этого.
В мою ecs можно положить компонент posX, posY, posZ и это будет почти настойщий SoA, но нужно делать это явно
В итоге SoA в контексте ECS это упрощение, где за единицу информации берется компонент, это дает некоторый бонус с точки зрения кэша и скорости итерации, но не все бенефиты SoA.
Отвечая на вопрос - чем это сильно лучше чем храните в векторе - тем что менеджер создания и распределения компонентов, а так же правильной итерации по несколькик компонентам одновременно.
Но можно просто сделать N векторов и через i обращаться к каждому в цикле. В предыдущей статье я как раз описывал что-то подобное как самый просто пример ECS.
А мой подход лучше тем - что я могу класть физически близко компоненты которые используются часто, и повышать дружелюбность кэша там где это выгодно.
Агрегатный компонент = архетип. Так делает часть ECS, и это рабочий вариант.
Но минусы такие:
количество архетипов растёт взрывным образом,
при каждом изменении набора компонентов нужно мигрировать сущность между архетипами,
миграции - это копирование данных,
сложность увеличивается пропорционально числу комбинаций.
То есть агрегатный компонент - это жёстко связанный тип. Sector в ECSS решает ту же задачу, но на уровне памяти, а не типов.
по бенчмарку - честно говоря пока не особо, есть вероятность что я неправильно замерил скорость для flecs, планирую поковыряться в этом, потому что если flecs может быстрее, то я тоже так хочу :) Или flecs использует архетипы и у него меньше накладных расходов на итерацию
flecs - архетипы, и с точки зрения простой итерации это лучший кейс, из-за этого она быстрее. Можно ли приблизиться к ней без архетипов и мержа данных - я буду пробовать :)
Кажется, у нас терминологическая путаница.
EnTT и Flecs таки хранят каждый компонент в своём "векторе", конкатенации компонент (мне кажется вы это называете агрегатным компонентом?) как у вас ECSS не происходит. В EnTT думаю понятно почему, да и в флексе тоже, т.к. при расположении каждого компонента в своём "векторе", то, что я назвал SoA, вы не платите никаких накладных расходов на итерацию отдельных произвольно выбранных компонентов архетипа.
В моей терминологии SoA / EC(S) двух компонент это примерно следующее:
struct EC {
std::vector<Entity> entities;
//
// some way to map entities[entity] to the position in vectors below
//
std::vector<ComponentA> cA;
std::vector<ComponentB> cB;
};Разумеется код выше это иллюстрация.
Мне всё же не совсем понятно зачем упаковывать ComponentA и ComponentB в рамках одного сектора памяти ECSS и в чем преимущество такого подхода по сравнению с созданием агрегатной структуры:
struct ComponentAandB {
ComponentA cA;
ComponentB cB;
};и затем такую структуру можно хранить хоть в ECS, хоть в классических пулах объектов.
Разница в том что эту структуру нужно заранее создать, назвать ее AandB, хранить как один компонент, и при итерации в системах брать AandB, а потом вытаскивать явно A или/и B.
И у вас не может отсутствовать A или B, они полюбому есть.
У меня сache locality без жёсткой связи: компоненты хранятся рядом в секторе [A | B | C], что даёт те же преимущества кеша, что и ручная структура, но без потери гибкости ECS (можно запрашивать только A, можно добавлять/удалять компоненты независимо).
Тоесть это вопрос удобства и гибкости, я автоматизировал создание таких структур, и позволяю работать с вложенными в нее компонентами как с обычным.
Правильно я понимаю что тем самым вы заставляете пользователя заранее размечать сектора, а это значит что модульность страдает, так как чтобы определить сектор нужно заранее знать все типы компонентов которые в него должны войти? И как по вашему это не превратится в ад менеджмента в большом проекте использующем разные фичи\модули и тысячи разных типов компонентов?
И что будет когда например у вас сектор [A | B | C] и сектор [D | F | G] и вам потребуется проитерировать все сущности с компонентами [B | F ] ? Кажется что стандартный Soa подход где каждый тип компонента в своем векторе в данном кейсе будет быстрей?
По поводу "ада менеджмента":
Архетипные ECS (flecs, Unity DOTS) тоже требуют "знать" структуру сущностей - просто это скрыто за рантайм-миграциями. В DOTS ты всё равно явно создаёшь архетипы через EntityManager.CreateArchetype() для производительности.
В ecss это explicit, а не implicit - честнее и предсказуемее. Дефолт: один компонент на сектор. Группировка - опциональная оптимизация для hot path, а не требование.
По поводу итерации, да, это worst case. Но: eсли [B|F] частый запрос - просто не группируйте их, бенчмарк iter_sparse_multi тестирует именно это - ecss там 0.5x от entt, не катастрофа, учитывая что в других кейсах ecss быстрее. Особенно создание и удаление новых компонентов - https://wagnerks.github.io/ecss_benchmarks/
Архетипы платят эту цену каждый раз при добавлении/удалении компонента. В реальной игре это сотни операций: подобрал предмет, вошёл в триггер, получил бафф и тд и тп
Я согласен касательно архетипов, мне тоже они видятся очень ограниченными за счет постоянных миграций и менеджмента. Не смотря на то что существуют вариации где не происходит копирование данных компонент, а только перебросы индексов. И мне нравится что вы ищите альтернативные решения. Явное указание это неплохо.
Но как в случае итерации по [B|F|M] при существовании секторов [A | B | C], [D | F | G] и отдельного [ M ] будет происходить отбор сущностей? Идентификатор сущности напрямую указывает на элемент компонента в чанке или имеет промежуточный маппинг (Sparse?).
Я все еще не понимаю как это помогает при расширении проекта: например популярный запрос Position + Velocity помещен в сектор, но таких "популярных" комбинаций может быть не 1 и даже не 10. Например вы добавили новый модуль с 3 новыми компонентами, и систему которой требуется Position и "Новый компонент". Получается в этом случае не получить бонуса от секторов потому что сектора просто не будет и будет замедление за счет участия сектора и обычного хранилища?
Привет, я немного раскрыл этот вопрос в своей следующей статье.
В случае итерации по [B|F|M] при существовании секторов [A | B | C], [D | F | G] и отдельного [ M ] - отбор будет через sparse, но без лишних прыжков по памяти, за О(1) мы получаем сразу указатель на компонент, это быстро.
Но сама итерация по таким наборам будет не максимально быстрой, как минимум потому что кэш будет забит лишними данными.
Это не оптимальный кейс.
Необходимость паковать компоненты рядом - редкая, потому что профит от этого может быть не везде, а иногда вред перекроет профит. Но плюс в том - что эта настройка в ваших руках :)
ECS with Sectors (ECSS) — структура памяти в моей ECS