ECS性能核心是数据局部性,必须用std::vector连续存储同类型组件,Entity仅为带版本号的ID,系统按预计算索引集或位掩码批量操作,禁止堆分配、虚函数和运行时集合。

组件存储必须用连续内存,别用 std::map 或 std::shared_ptr 堆分配
实体组件系统(ECS)性能核心在数据局部性,std::map 查找慢、内存不连续;std::shared_ptr 堆分配导致遍历 cache miss 严重。你写个渲染系统每帧遍历 10 万个 Transform 组件,用指针跳来跳去,帧率直接掉一半。
- 用
std::vector存储同类型组件,按插入顺序连续排列 - 每个组件类型对应一个独立
std::vector,不要混合存储 - 实体只存索引(如
size_t),不是指针——避免悬空和重分配失效 - 删组件时用“交换并弹出”(swap-and-pop)策略,保持连续性,别留空洞
Entity 只能是 ID,别塞逻辑或虚函数
把 Entity 设计成 uint32_t 或 struct Entity { uint32_t id; }; 就够了。加虚函数、成员方法、或者继承体系,等于亲手把 ECS 拉回面向对象老路——系统无法批量操作,缓存友好性归零,而且后期想做 job system 或 SIMD 处理时根本没法拆解。
- 禁止在
Entity里放std::vector<Component*>这类运行时集合 - 实体生命周期由世界(
World)统一管理,ID 复用需带版本号防误用(如uint64_t高 32 位为版本) - 查询组件走系统层(如
world.get<Transform>(entity)),而不是实体自己提供get<T>()
系统(System)按组件签名过滤,别手动遍历所有实体
手动写 for (auto& e : entities) { if (has<A>(e) && has<B>(e)) {...} } 看似灵活,实则每次都要查表、分支预测失败、cache 不友好。ECS 的系统应该只看到它关心的那几列数据。
- 构建时预计算每个组件类型的活跃索引集(如
std::vector<size_t> transform_indices),系统执行前取交集 - 更优做法:用位掩码(
bitset)表示实体拥有的组件类型,用AND快速筛选匹配系统签名 - 系统内遍历时,直接按组件数组下标顺序访问——确保 CPU prefetcher 能跟上
- 别在系统里调用
world.create_entity()或world.destroy_entity(),延迟到帧末统一处理,避免迭代器失效
别过早抽象 World 接口,先实现单线程版本再加并发
一上来就搞 IWorld、AbstractSystemScheduler、多线程注册/卸载,90% 的需求根本用不上。反而让组件查找变慢(虚调用)、调试困难(断点进不去具体实现)、热重载支持变复杂。
立即学习“C++免费学习笔记(深入)”;
- 第一版
World就是几个std::vector加一个std::vector<std::unique_ptr<System>> - 组件类型用
typeid(T).hash_code()当 key,别自己搞字符串哈希或宏注册(除非真要反射) - 多线程安全的关键不是锁整个
World,而是按数据分区:读 Transform 写 RenderData 的系统可以并行,但两个都写 Transform 的必须互斥 - 真正卡性能的地方从来不是“能不能并发”,而是“数据是不是连续+有没有冗余拷贝”
组件布局对齐、稀疏集合(sparse set)优化、 archetype 模式这些,等你跑通 5 个系统、压测出真实瓶颈再说。现在最该盯住的是:std::vector<Transform> 里每个元素是不是紧挨着,以及遍历它的时候有没有意外触发 operator new。











