ECS框架-ECS框架引入
ECS框架
前几章我们完成了“输入信号+事件总线+场景切换+资源系统”。使用传统的GameObject + Component(面向对象)写法无法支撑过多的游戏对象,这里我们将会引入ECS架构。
- Entity(实体):只是一个“唯一标识符”
- Component(组件):纯粹的数据
- System(系统):纯粹的逻辑(在拥有特定组件的实体集合上运行)
- Registry:管理实体与组件的容器(EnTT 的
entt::registry)
这里UI依然保持“自组合+继承”的树形结构不变,让UI模块与游戏ECS模块隔离。

可以看到,我们的UI和ECS系统是平级的,Scene内部同时驱动UI、ECS
目标
- 理解ECS的三要素与
entt::registry的职责 Scene中引入entt::registry,把场景变成“ECS世界容器”- 把旧的“组件类(带update/render)”改为“纯数据组件”
- 实现3个最小系统:移动、动画、渲染
面向对象的方法会涉及组件之间互相引用、生命周期复杂、查询成本高……当实体数量上来后,这些问题会被放大。而转到ECS架构,它更适合大量同类对象的批处理:
- 组件是 POD/数据结构,存储更紧凑
- 系统用
view<...>一次性拿到一组实体,循环里做同一类事情 - 系统逻辑从组件里抽离出来,组件之间更少互相依赖

组件部分 (C)
从带行为的类变成纯数据struct,如
TransformComponent:位置/缩放/旋转VelocityComponent:速度SpriteComponent:贴图信息 + 渲染尺寸/偏移AnimationComponent:动画帧数据 + 当前播放状态
// src/engine/component/transform_component.h
struct TransformComponent {
glm::vec2 position_{};
glm::vec2 scale_{1.0f};
float rotation_{};
TransformComponent(glm::vec2 position = {0.0f,0.0f},
glm::vec2 scale = {1.0f,1.0f},
float rotation = 0.0f
)
: position_(std::move(position)), scale_(std::move(scale)), rotation_(rotation) {}
};
// src/engine/component/velocity_component.h
struct VelocityComponent {
glm::vec2 velocity_ {};
};
// src/engine/component/sprite_component.h
struct Sprite {
entt::id_type texture_id_{entt::null}; // 纹理id
std::string texture_path_; // 纹理路径
engine::utils::Rect src_rect_{}; // 纹理矩形
bool is_flipped{false}; // 是否翻转
Sprite() = default;
/**
* @brief 构造函数(通过纹理路径texture_path构造)
* @param texture_path 纹理路径
* @param source_rect 纹理矩形
* @param is_flipped 是否翻转
*
*/
Sprite(std::string_view texture_path,
engine::utils::Rect source_rect,
bool is_flipped = false)
: texture_path_(texture_path.data()),
texture_id_(entt::hashed_string(texture_path.data())),
src_rect_(std::move(source_rect)),
is_flipped(is_flipped)
{}
/**
* @brief 构造函数(通过纹理id texture_id构造)
* @param texture_id 纹理id
* @param source_rect 纹理矩形
* @param is_flipped 是否翻转
* @note 需要保证texture_id_在构造时已经存在于资源管理器中
*/
Sprite(entt::id_type texture_id,
engine::utils::Rect source_rect,
bool is_flipped = false)
: texture_id_(texture_id),
src_rect_(std::move(source_rect)),
is_flipped(is_flipped)
{}
};
struct SpriteComponent {
Sprite sprite_;
glm::vec2 size_{0.0f};
glm::vec2 offset_{0.0f};
bool is_visible{true};
SpriteComponent() = default;
SpriteComponent(Sprite sprite,
glm::vec2 size = glm::vec2(0.0f),
glm::vec2 offset = glm::vec2(0.0f),
bool is_visible = true)
: sprite_(std::move(sprite)),
size_(std::move(size)),
offset_(std::move(offset)),
is_visible(is_visible)
{
if (glm::all(glm::equal(size, glm::vec2(0.0f)))) {
size_ = glm::vec2(sprite_.src_rect_.size.x, sprite_.src_rect_.size.y);
}
}
};
// src/engine/component/animation_component.h
struct AnimationFrame {
engine::utils::Rect source_rect_{};
float duration_ms_{100.0f}; // 帧间隔 (ms)
AnimationFrame(engine::utils::Rect source_rect, float duration_ms = 100.0f)
: source_rect_(source_rect), duration_ms_(duration_ms) {}
};
struct Animation {
std::vector<AnimationFrame> frames_;
float total_duration_ms_{};
bool loop_{true};
/**
* @brief 构造函数
* @param frames 动画帧列表
* @param loop 是否循环播放
* @param total_duration_ms_ 总时长 (ms)
*/
Animation(std::vector<AnimationFrame> frames, bool loop = true)
: frames_(std::move(frames)), loop_(loop) {
for (const auto& frame : frames_) {
total_duration_ms_ += frame.duration_ms_;
}
}
};
struct AnimationComponent {
std::unordered_map<entt::id_type, Animation> animations_;
entt::id_type current_animation_id_{entt::null}; // 当前播放的动画名称
size_t current_frame_index_{}; // 当前播放的帧索引
float current_time_ms_{}; // 已经过的时间 (ms)
float speed_{1.0f}; // 播放速度
AnimationComponent(std::unordered_map<entt::id_type, Animation> animations,
entt::id_type current_animation_id,
size_t current_frame_index = 0,
float current_time_ms = 0.0f,
float speed = 1.0f) :
animations_(std::move(animations)),
current_animation_id_(current_animation_id),
current_frame_index_(current_frame_index),
current_time_ms_(current_time_ms),
speed_(speed)
{}
};
现在,组件不提供update()等成员函数——行为全部移到系统中
实体部分(E)
接着我们在Scene类中引入entt::registry,场景成为ECS世界
场景Scene中删除原来关于GameObject游戏对象所有的相关函数,新增entt::registry registry_,用其管理场景内的所有实体与组件。
// src/engine/scene/scene.h
class Scene {
protected:
entt::registry registry_;
// ...
public:
entt::registry& getRegistry() { return registry_; }
};
Scene::clean()中直接registry_.clear(),清空本场景中的所有实体与组件
系统部分(S)
把逻辑集中到src/engine/system/,新增移动、渲染、动画系统
MovementSystem:速度驱动位移
该系统关心两个组件:VelocityComponent+TransformComponent
class MovementSystem {
public:
void update(entt::registry& registry, float delta_time);
};
void MovementSystem::update(entt::registry ®istry, float delta_time)
{
// 获取感兴趣的实体 view
auto view = registry.view<engine::component::VelocityComponent, engine::component::TransformComponent>();
// 遍历实体
for (auto entity : view) {
const auto &velocity = view.get<engine::component::VelocityComponent>(entity);
auto &transform = view.get<engine::component::TransformComponent>(entity);
// 更新位置
transform.position_ += velocity.velocity_ * delta_time;
}
}
RenderSystem
关心:TransformComponent + SpriteComponent
void RenderSystem::update(entt::registry ®istry, render::Renderer &renderer, const render::Camera &camera)
{
auto view = registry.view<component::TransformComponent, component::SpriteComponent>();
for (auto entity : view) {
const auto& transform = view.get<component::TransformComponent>(entity);
const auto& sprite = view.get<component::SpriteComponent>(entity);
auto position = transform.position_ + sprite.offset_; // 位置 = 位置 + 偏移量
auto size = sprite.size_ * transform.scale_; // 大小 = 精灵大小 * 缩放
renderer.drawSprite(camera, sprite.sprite_, position, size, transform.rotation_);
}
}
这个drawSprite(...)的参数原本是传入scale的,现在直接传size了,因为SpriteComponent.size_与TransformComponent.scale_已经将最终大小设置完了
AnimationSystem
关心两个组件:AnimationComponent+SpriteComponent,动画组件中有动画映射表(key为动画名哈希ID),记录当前帧、播放时间等状态;系统每帧推进计时器,并在精灵组件中设置当前帧的src_rect_
void AnimationSystem::update(entt::registry ®istry, float delta_time)
{
auto view = registry.view<component::AnimationComponent, component::SpriteComponent>();
for (auto entity : view){
auto &anim_comp = registry.get<component::AnimationComponent>(entity);
auto &sprite_comp = registry.get<component::SpriteComponent>(entity);
// 动画如果不存在就跳过
auto it = anim_comp.animations_.find(anim_comp.current_animation_id_);
if (it == anim_comp.animations_.end()){
continue;
}
// 获取当前动画
auto& current_animation = it->second;
// 如果没有帧就跳过
if (current_animation.frames_.empty()){
continue;
}
// 更新当前的播放时间
anim_comp.current_time_ms_ += delta_time * 1000 * anim_comp.speed_;
// 获取当前帧
const auto& current_frame = current_animation.frames_[anim_comp.current_frame_index_];
// 进行判断,如果播放时间超过当前帧的持续时间,就切换到下一帧
if(anim_comp.current_time_ms_ >= current_frame.duration_ms_){
anim_comp.current_time_ms_ -= current_frame.duration_ms_;
anim_comp.current_frame_index_++;
// 动画播放完成
if (anim_comp.current_frame_index_ >= current_animation.frames_.size()){
if(current_animation.loop_) {
anim_comp.current_frame_index_ = 0;
} else {
// 不循环,停在最后一帧
anim_comp.current_frame_index_ = current_animation.frames_.size() - 1;
}
}
}
// 更新精灵组件的纹理
const auto& next_frame = current_animation.frames_[anim_comp.current_frame_index_];
sprite_comp.sprite_.src_rect_ = next_frame.source_rect_;
}
}
GameScene中创建ECS最小闭环
创建实体 ---> 系统更新 ---> 系统渲染,GameScene现在不再往场景里添加GameObject,而是直接对registry_创建实体并添加组件:
// src/game/scene/game_scene.cpp
auto entity = registry_.create(); // 创建一个实体
// 往实体中添加组件
registry_.emplace<engine::component::TransformComponent>(entity, glm::vec2(100, 100));
registry_.emplace<engine::component::VelocityComponent>(entity, glm::vec2(10, 10));
registry_.emplace<engine::component::SpriteComponent>(entity,
engine::component::Sprite("assets/textures/Units/Archer.png", engine::utils::Rect(0, 0, 192, 192)));
然后在 update() 里按顺序运行系统(先移动、再动画),在 render() 里运行渲染系统:
movement_system_->update(registry_, delta_time)animation_system_->update(registry_, delta_time)render_system_->update(registry_, context_.getRenderer(), context_.getCamera())
最后仍然调用 基类中的Scene::update/render 让 UI 正常工作,这就是“UI 与 ECS 隔离”的落地方式。
章节总结
这节我们区别了引擎中的Sprite与Image,引入了资源哈希ID,Sprite这个名字就冲突了,精灵更适合是渲染组件数据,而UI中更像是贴图Image,所以我们修改了UI中的engine::render::Sprite为engine::render::Image,Renderer的UI接口同步改为drawUIImage(...),现在的render文件夹下的原sprite头文件改为image,这样整体来说更清晰:
- ECS:
component::SpriteComponent→RenderSystem→Renderer::drawSprite(...) - UI:
ui::UIImage(内部持有render::Image)→Renderer::drawUIImage(...)
Scene内引入entt::registry,场景成为ECS世界容器,旧的GameObject容器删除;现在GameScene直接创建entity并emplace组件,用系统驱动运行与绘制
遇到的问题
说来惭愧,都是一些常犯的错误,首先就是GameScene中关于智能指针的声明问题,再强调一下,如果析构函数写在头文件中,就不能通过前向声明的方式了,需要知道其完整的定义,不然就要把析构函数放到cpp单元中。

浙公网安备 33010602011771号