介绍下:EnTT
EnTT 是什么
EnTT 是 Michele “skypjack” Caini 开发的一个纯头文件 C++17 ECS 库,全名 Entity-Component-System Template Library。它是目前 C++ 游戏开发领域最流行的 ECS 实现,Minecraft Bedrock Edition 就用了它,此外被数千个游戏和引擎项目采用。
GitHub:skypjack/entt,Star 数超过 10k。
和你刚才看的框架对比
你刚才看的是手写的简化版,对照着看 EnTT 会清晰很多。
相同的核心:Sparse Set 作为底层数据结构,Entity 带 generation 防悬空引用,Pool 存连续组件数据。
EnTT 额外做的几件重要的事:
1. Group 优化(最关键的性能特性)
普通 view 的问题:迭代 Position 的 pool,每个 entity 还要去别的 pool 查 Velocity,虽然是 O(1),但这些查询是随机内存访问。
EnTT 的 Group 把频繁共现的组件强制排布在同一内存区域:
// 声明一个 group:Position 和 Velocity 一定一起出现
auto group = registry.group<Position>(entt::get<Velocity>);
// 迭代时,Position 和 Velocity 的数据是真正相邻的
// CPU 一次 cache line 可以同时加载两个组件,接近零额外访问开销
for (auto entity : group) {
auto& [pos, vel] = group.get<Position, Velocity>(entity);
pos.x += vel.vx * dt;
}
代价是 Group 会改变 pool 的内部排列,有一定建立成本,适合高频迭代的核心 System。
2. Signal / Observer 系统
组件生命周期钩子,非常实用:
// 每次给 entity 加 Health 组件时触发 registry.on_construct<Health>().connect<&on_health_added>(); // 每次 Health 组件被销毁时触发 registry.on_destroy<Health>().connect<&on_health_removed>(); // 每次 Health 被修改时触发 registry.on_update<Health>().connect<&on_health_changed>();
游戏里的典型用法:加 Renderable 组件时自动向渲染器注册,移除时自动注销,不需要 System 主动轮询。
3. 运行时组件(meta 反射系统)
EnTT 内置了一套轻量反射机制,允许在运行时按字符串名称操作组件,这在游戏 scripting、编辑器、网络序列化里很有价值:
// 注册类型信息
entt::meta<Position>()
.type("Position"_hs)
.data<&Position::x>("x"_hs)
.data<&Position::y>("y"_hs);
// 运行时按名字创建和操作组件(Lua 脚本层常用)
auto type = entt::resolve("Position"_hs);
auto instance = type.construct();
type.data("x"_hs).set(instance, 1.0f);
4. Snapshot / Serialization
内置了 entity 快照和恢复机制,游戏存档、网络状态同步的基础设施:
// 序列化
entt::snapshot{registry}
.entities(output)
.component<Position, Velocity, Health>(output);
// 反序列化
entt::snapshot_loader{registry}
.entities(input)
.component<Position, Velocity, Health>(input);
EnTT 的实际 API 长什么样
#include <entt/entt.hpp>
// 组件:纯数据
struct Position { float x, y; };
struct Velocity { float vx, vy; };
struct Health { int hp; };
int main() {
entt::registry registry;
// 创建实体,链式添加组件
auto player = registry.create();
registry.emplace<Position>(player, 0.f, 0.f);
registry.emplace<Velocity>(player, 1.f, 0.5f);
registry.emplace<Health> (player, 100);
// view:同时有 Position 和 Velocity 的所有 entity
auto view = registry.view<Position, Velocity>();
view.each([](auto entity, Position& pos, Velocity& vel) {
pos.x += vel.vx * 0.016f;
pos.y += vel.vy * 0.016f;
});
// 也可以用 range-for
for (auto [entity, pos, vel] : view.each()) {
pos.x += vel.vx * 0.016f;
}
// 运行时修改:替换组件内容
registry.patch<Health>(player, [](Health& h) { h.hp -= 10; });
// 检查、删除
if (registry.all_of<Health>(player)) {
registry.remove<Health>(player);
}
// 销毁 entity 及其所有组件
registry.destroy(player);
}
EnTT 的设计哲学
零开销抽象:整个库 header-only,大量使用模板,编译器可以内联所有东西,运行时没有虚函数、没有类型擦除开销(除非你用 meta 反射)。
不强制 System 结构:EnTT 只管 Entity 和 Component 的存储,System 怎么写完全由你决定——可以是普通函数、类、协程,随意。这和 Unity DOTS 那种强制 ISystem 接口的设计不同,更灵活也更轻量。
组合而非继承:这就是 ECS 的本质,EnTT 把它做到了 API 层面的极致简洁。
面试里怎么用这个知识
如果 EA 面试官问”你熟悉什么 ECS 框架”,可以说:
“了解 EnTT 的设计。它的核心是 Sparse Set,兼顾 O(1) 查找和连续内存迭代。我自己也实现过类似的简化版,理解 generation 机制防悬空引用、Group 优化解决多组件迭代 cache miss 的思路。实际项目里我们用的是自研框架,但设计思路一脉相承。”
这样回答既展示了理解深度,又不会说”我用过但其实不熟”。
