ECS框架-ECS框架引入

ECS框架

前几章我们完成了“输入信号+事件总线+场景切换+资源系统”。使用传统的GameObject + Component(面向对象)写法无法支撑过多的游戏对象,这里我们将会引入ECS架构。

  • Entity(实体):只是一个“唯一标识符”
  • Component(组件):纯粹的数据
  • System(系统):纯粹的逻辑(在拥有特定组件的实体集合上运行)
  • Registry:管理实体与组件的容器(EnTT 的 entt::registry

这里UI依然保持“自组合+继承”的树形结构不变,让UI模块与游戏ECS模块隔离。

image-20260325210215000

可以看到,我们的UI和ECS系统是平级的,Scene内部同时驱动UI、ECS

目标

  • 理解ECS的三要素与entt::registry的职责
  • Scene中引入entt::registry,把场景变成“ECS世界容器”
  • 把旧的“组件类(带update/render)”改为“纯数据组件”
  • 实现3个最小系统:移动、动画、渲染

面向对象的方法会涉及组件之间互相引用、生命周期复杂、查询成本高……当实体数量上来后,这些问题会被放大。而转到ECS架构,它更适合大量同类对象的批处理:

  • 组件是 POD/数据结构,存储更紧凑
  • 系统用 view<...> 一次性拿到一组实体,循环里做同一类事情
  • 系统逻辑从组件里抽离出来,组件之间更少互相依赖

image-20260325211136349


组件部分 (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 &registry, 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 &registry, 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 &registry, 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::Spriteengine::render::ImageRenderer的UI接口同步改为drawUIImage(...),现在的render文件夹下的原sprite头文件改为image,这样整体来说更清晰:

  • ECS:component::SpriteComponentRenderSystemRenderer::drawSprite(...)
  • UI:ui::UIImage(内部持有 render::Image)→ Renderer::drawUIImage(...)

Scene内引入entt::registry,场景成为ECS世界容器,旧的GameObject容器删除;现在GameScene直接创建entity并emplace组件,用系统驱动运行与绘制

遇到的问题

说来惭愧,都是一些常犯的错误,首先就是GameScene中关于智能指针的声明问题,再强调一下,如果析构函数写在头文件中,就不能通过前向声明的方式了,需要知道其完整的定义,不然就要把析构函数放到cpp单元中。

posted @ 2026-03-26 11:17  wenyiGamecpp  阅读(64)  评论(0)    收藏  举报