当前位置: 首页 > news >正文

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}; // 纹理idstd::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)
{// 获取感兴趣的实体 viewauto 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单元中。

http://www.jsqmd.com/news/538336/

相关文章:

  • Qwen2.5-VL视觉定位Chord一文详解:多目标检测+自然语言理解能力解析
  • wvp-GB28181-pro:基于Knife4j的国标视频平台API文档解决方案
  • 从RMS误差到厘米级定位:深入拆解RTK和PPP背后的‘黑科技’(附多路径、钟差等关键因素避坑指南)
  • LFM2.5-1.2B-Thinking-GGUF效果展示:32K上下文下跨PDF章节引用准确性验证
  • 收藏!国内大厂大模型人才招聘真相,小白/程序员入门必看
  • 高频电子线路:电容三点式振荡原理、Multisim14.0 仿真及 Word 讲解
  • 从黑白到彩色:DeOldify让历史照片重现光彩,操作简单效果好
  • 小白也能懂!铭凡 MS-A2 改装 RTX 4000 Ada 显卡教程,轻松搞定 AI 与 VMware 实验室
  • 绝地求生压枪难题?5分钟掌握罗技鼠标宏终极解决方案
  • 如何高效解决Windows内存占用过高问题?Mem Reduct极简深度优化指南
  • 步进电机发热严重?4相5线电机停转保护的3个关键细节
  • 2026年实测5款最好用的微信图文排版工具 公众号编辑器推荐 - 鹅鹅鹅ee
  • Llama-3.2V-11B-cot入门必看:新手友好型视觉推理工具完整使用指南
  • 如何让2015年前的MacBook Pro用上最新macOS?OpenCore Legacy Patcher完全指南
  • 超声波手持式气象站 超声波手持式气象仪
  • 智能客服实战:Dify框架下的向量数据库选型与性能优化指南
  • Flux.1-Dev深海幻境风格探索:卷积神经网络特征可视化艺术再创作
  • # 发散创新:基于Python的自动化渗透测试脚本设计与实战演练在现代网络安全攻防对抗中,**自动化渗
  • 数据驱动决策的误区与对策:大数据专家经验分享
  • Java 并发数据库操作与同步:提升性能的实践指南
  • TensorRT性能调优实战指南:从瓶颈诊断到引擎优化
  • LFM2.5-1.2B-Thinking-GGUF入门指南:无需CUDA、不依赖HuggingFace的极简部署路径
  • GTE文本向量在医疗文本处理中的应用:实体识别与分类实战
  • Python从入门到精通(第06章):循环结构与流程控制
  • ChatTTS实战:从WAV到PT的高效转换技术解析
  • Eclipse 重构菜单详解
  • 如何用SmartSlicer颠覆精灵图切割效率?5分钟掌握智能提取技术
  • 别再死记硬背了!用这6个真实案例拆解Web文件上传漏洞的防御与攻击逻辑
  • DeOldify效果惊艳案例:抗美援朝老兵黑白合影AI上色后首次彩色呈现
  • FireRedASR-AED-L从零部署:无需Python环境,Docker镜像开箱即用指南