i007.cc

i007.cc

优先队列-降维打击

05.价值资料

介绍下: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 把频繁共现的组件强制排布在同一内存区域

cpp
// 声明一个 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 系统

组件生命周期钩子,非常实用:

cpp
// 每次给 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、编辑器、网络序列化里很有价值:

cpp
// 注册类型信息
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 快照和恢复机制,游戏存档、网络状态同步的基础设施:

cpp
// 序列化
entt::snapshot{registry}
    .entities(output)
    .component<Position, Velocity, Health>(output);

// 反序列化
entt::snapshot_loader{registry}
    .entities(input)
    .component<Position, Velocity, Health>(input);

 


EnTT 的实际 API 长什么样

cpp
#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 的思路。实际项目里我们用的是自研框架,但设计思路一脉相承。”

这样回答既展示了理解深度,又不会说”我用过但其实不熟”。

发表回复