entt::registry 是 ECS 唯一数据容器,所有组件必须注册于同一实例;需用 view 而非手动遍历以保障内存连续性;组件须为 POD 类型,避免虚函数与非平凡析构;系统调度需手动控制顺序,不可在迭代中增删实体。

entt::registry 是 ECS 的唯一数据容器,别建多个
ENTT 的核心不是抽象类或接口,而是 entt::registry 实例——它既是实体池、组件存储,也是系统调度的上下文。你不需要、也不该为不同逻辑域创建多个 registry(比如 render_registry、physics_registry),否则组件无法跨系统访问,查询失效,缓存局部性崩坏。
常见错误现象:entt::orphaned 断言触发、get<T>() 报 std::out_of_range、视图(view)返回空范围,本质都是实体在别的 registry 里注册了组件,而你在查另一个。
- 所有组件类型必须在同一个
entt::registry实例中注册(哪怕只用一次registry.ctx().emplace<T>()也算) - 多线程下若需并发读写,用
registry.try_lock()或外部互斥,不要拆 registry - 模块化组织靠
entt::runtime_view或自定义标签组件(如struct RenderTag {};),不是靠多个 registry
用 entt::view 而不是手写 for 循环遍历实体
entt::view 不是语法糖,它是 ENT 基于 archetype(类型布局)做的内存连续访问优化。手动用 for (auto entity : registry.storage<entt::entity>()) 遍历,会跳着读内存,Cache Miss 高;而 view<C1, C2>() 拿到的是按组件类型打包存放的连续 chunk,SIMD 友好,性能差一个数量级。
使用场景:每帧更新逻辑系统(如移动、碰撞、渲染)必须用 view;初始化批量创建可放宽,但后期扩展难维护。
立即学习“C++免费学习笔记(深入)”;
- 避免嵌套 view:不要
for (auto [e, c] : view1) { for (auto [e2, c2] : view2) { ... } },改用view<C1, C2, C3>()一次拿到全量交集 - 如果组件可能为空(比如可选渲染组件),用
registry.view<Position, std::optional<Sprite>>(),而非两个独立 view 手动 join - 注意 const 正确性:
view<const Transform, Velocity>()中Transform只读,Velocity可写,混合访问要显式声明
组件不能有虚函数、非平凡析构、或指针成员(除非你接管生命周期)
ENTT 默认把组件当 POD 存储:直接 memcpy、不调构造/析构、不保证对象顺序。一旦组件含虚表、std::string、std::vector、裸指针,轻则内存泄漏,重则 registry.destroy(entity) 后野指针残留,崩溃位置飘忽。
错误信息典型表现:double free or corruption、pure virtual method called、ASan 报 heap-use-after-free 在 destroy() 后几帧才触发。
- 推荐方案:组件只存数据(
float x, y;),资源引用用entt::resource_handle<Texture>或 ID(uint32_t texture_id;) - 真需要 RAII?用
registry.on_destroy<T>.connect<MyGuard>(&MyGuard::cleanup)手动补析构逻辑 - 想存
std::shared_ptr?可以,但别依赖它自动释放——ENTT 不管智能指针内部计数,只负责拷贝指针本身
系统调度靠手动控制顺序,ENTT 不提供默认 tick 管道
ENTT 没有 SystemBase::update() 或 World::tick() 这种内置调度器。所谓“系统”,就是你写的普通函数,何时调、调几次、参数怎么传,全由你自己组织。这灵活,也容易漏掉关键步骤。
常见疏漏:物理系统用了 Velocity 和 Position,但忘了在它之前运行输入系统更新 Velocity;或者渲染系统读 Transform,却在动画系统写完前就执行,画面延迟一帧。
- 最简可靠做法:单线程下用固定顺序函数调用,例如
input_system(reg); physics_system(reg); animation_system(reg); render_system(reg); - 避免在系统内调
registry.create()或registry.destroy()—— 这会破坏当前 view 迭代器,改用deferred队列或下一帧处理 - 想做依赖注入?ENTT 提供
registry.ctx()存全局上下文,比如registry.ctx().emplace<DeltaTime>(dt);,系统内用registry.ctx().get<DeltaTime>()读取
复杂点在于跨帧状态同步和异步加载——比如一个实体刚被网络模块 create() 出来,它的渲染组件还没来得及分配,这时视图查询不到它。这种时序耦合没法靠框架自动解,得靠明确的阶段标记(如 struct PendingRender {};)和两遍遍历处理。


















