新的挑战ECS框架-开篇-EnTT基础

EnTT基础

目标:

  • 了解三件套:entt::registry / entt::entity / component

  • 学会用entt::view遍历"符合组件条件"的实体集合

  • 了解entt::hashed_string(哈希字符串)的用途与常见问题

  • 简单了解registry.ctx()是什么

设计思路

ECS框架,本质就是拿空间换时间,如果是传统的OOP架构,那么我们会在类之间进行跳转,所以有一个缓存cache命中率的问题,因为cpu喜欢读一块一块的内存,而不是分散开来的,oop就会有这个问题,比如不同游戏对象之间,update、render是先跑完update然后再轮到render,这种是会不断跳转读取,而ECS的思维,分为实体、组件、系统,实体就是一个ID/句柄,就是一个非负整数,组件就是简单的纯数据结构,系统相当于一个函数,其会负责如何更新,这种方式再更新的时候读取就是一块一块的连续内存了,极大的提升了性能。

  • 实体(entity)不是一个“带方法的对象”,更像是一个ID/句柄
  • 组件(component)是纯数据:位置、速度、血量、阵营……
  • 系统(system)负责“如何更新”:它会遍历一批拥有指定组件的实体,批量处理

这是个吃后期的架构,越到后面收益越明显:当你要新增玩法(比如“阻挡”“弹道”“技能”),往往是新增组件 + 新增系统,而不是在一堆类的继承链里打补丁。

实现

实体 Entity

entt::registry registry;

entt::entity player = registry.create(); // 一个实体

建立一个实体很简单,需要使用entt::registry,这是"世界/数据库",我就理解是一个注册表,所有实体得先注册,用于创建实体、增删组件、查询视图。

组件 Component

struct position {
    float x;
    float y;
};

struct velocity {
    float dx;
    float dy;
};

组件,只存放数据,简单的数据结构。这里的 emplace<T> 可以理解成:“给这个实体添加一个 T 组件,并初始化它的数据”。

registry.emplace<position>(player, 10.f, 20.f);
registry.emplace<velocity>(player, 1.f, 0.5f);

系统 System

这里我们会采用 view 遍历实体集合,当我们想找出“所有带 tag + position 的实体”,就可以建立一个 view:

auto view = registry.view<const tag, const position>();

view.each([](const auto& entity_tag, const auto& pos) {
    if (entity_tag.id == "player"_hs) {
        spdlog::info("找到玩家,位置: ({}, {})", pos.x, pos.y);
    }
});

这段代码要牢记,后面游戏系统,都会沿用这个结构,用view找到目标实体,然后批处理。

嗯,我应该记住了,*_*!

哈希字符串:高效,节能,稳如狗

怎么像打广告,简单来说就是用整型数字来比较,就是把std::string 映射成整数,性能好。

entt::hashed_string可隐式转换成entt::id_type,这是个32位的无符号整型。

果然C++就是不讲道理,从语法层面可以直接改

我们可以采用 _hs 字面量来生成哈希值

using namespace entt::literals;
registry.emplace<tag>(player, "player"_hs, "player");

这里这个tag组件有一个id和std::string。这里有个问题要注意,就是hashed_string是不拥有原始字符串的值的,它好像是字符串指针,有点类似string_view,不保证生命周期,所以会存一个id,一个value值 std::string

struct tag {
    entt::id_type id;
    std::string value;
};

我们可以通过函数``registry.get(player)` 拿到组件,就能同时打印哈希值与原始文本:

auto player_tag = registry.get<tag>(player);
spdlog::info("玩家标签的哈希值: {}", player_tag.id);
spdlog::info("玩家标签的原始文本: {}", player_tag.value);

上下文变量(registry.ctx())

依赖注入,可以存放一些全局/共享数据;先记住它的定位:当你不想把某个全局对象做成单例,又希望系统之间能共享时,ctx 往往是一个更干净的选择

章节总结

了解entt基础知识,如何挂组件,如何通过view获取相应的实体,了解哈希字符串高效查找的原理,注意哈希字符串不保证生命周期的问题

posted @ 2026-03-16 21:32  wenyiGamecpp  阅读(439)  评论(0)    收藏  举报