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

C++游戏开发之旅29

问题概述

这章完成游戏UI的绘制,绘制生命UI和得分UI。

完成的任务:

  • 创建UIImage控件显示图标
  • 创建UILabel控件显示文本
  • 组合控件构建生命值显示
  • 实现数据绑定动态更新

第一部分:UI控件扩展

上一章完成了UIPanel,这节完成最常用的两个控件UIImageUILabel

1.UIImage

精灵UI实现的功能很简单,就是在UI层级中目标区域绘制一个精灵。

src/engine/ui/ui_image.h

#pragma once 
#include "ui_element.h"
#include "../render/sprite.h"
#include <string>
#include <optional>
#include <SDL3/SDL_rect.h>namespace engine::ui {class UIImage : public UIElement {
private:engine::render::Sprite sprite_;
public:/*** @brief 精灵ui的构造函数* @param texture_id 精灵的纹理id* @param position 精灵的位置* @param size 精灵的大小 {0, 0} 表示使用纹理源矩形的大小* @param src_rect 精灵的纹理源矩形* @param is_flipped 是否翻转*/UIImage(const std::string& texture_id,const glm::vec2& position = {0, 0},const glm::vec2& size = {0, 0},const std::optional<SDL_FRect>& src_rect = std::nullopt,bool is_flipped = false);~UIImage() override = default;void render(engine::core::Context& context) override;// get setconst engine::render::Sprite& getSprite() const { return sprite_; }void setSprite(const engine::render::Sprite& sprite) { sprite_ = sprite; }};}

UIImage主要是持有一个sprite精灵,并且有一个关键函数render,绘制ui精灵。

可实现的功能:

  • 根据size来决定绘制的图片大小,如果是{0,0}则绘制源矩形大小

  • 位置是相对于父节点的位置计算得到

  • sprite精灵可决定是否裁剪以及缩放等功能

src/engine/ui/ui_image.cpp

#include "ui_image.h"
#include <spdlog/spdlog.h>
#include "../core/context.h"
#include "../render/renderer.h"namespace engine::ui{UIImage::UIImage(const std::string &texture_id,const glm::vec2 &position, const glm::vec2 &size,const std::optional<SDL_FRect> &src_rect,bool is_flipped): UIElement(position, size), sprite_(render::Sprite(texture_id, src_rect, is_flipped)){spdlog::trace("UIImage构造成功");}void UIImage::render(engine::core::Context &context){if(!visible_ || sprite_.getTextureId() == "") return; // 如果不可见或没有纹理,则不渲染auto position = getScreenPosition();if(size_ == glm::vec2(0, 0)){context.getRenderer().drawUISprite(sprite_, position_);} else {context.getRenderer().drawUISprite(sprite_, position_, size_);}UIElement::render(context);}
}

2.UILabel

UILabel同样继承于``UIElement,其封装了TextRenderer`的功能,使其成为一个正式UI控件。

src/engine/ui/ui_label.h

#pragma once 
#include "ui_element.h"
#include <string>namespace engine::render{
class TextRenderer;
}namespace engine::ui{
class UILabel : public UIElement{
private:render::TextRenderer* text_renderer_;std::string text_; // 文本内容std::string font_id_; // 字体int font_size_; // 字体大小engine::utils::FColor color_; // 字体颜色 public:UILabel(render::TextRenderer* text_renderer,const std::string& text_,const std::string& font_id_,int font_size_ = 16,const glm::vec2& position = {0,0},const engine::utils::FColor& color = {1,1,1,1});void render(engine::core::Context& context) override;// get set const std::string& getText() const { return text_; }const std::string& getFontId() const { return font_id_; }int getFontSize() const { return font_size_; }const engine::utils::FColor& getColor() const { return color_; }void setText(const std::string& text);void setFontId(const std::string& font_id);void setFontSize(int font_size);void setColor(const engine::utils::FColor& color);};
}

UILabel需要实现:

  • 文本绘制功能
  • 支持动态文本
  • 文本改变时可以更新大小
  • 颜色可修改

src/engine/ui/ui_label.cpp

#include "ui_label.h"
#include "../render/text_renderer.h"
#include <spdlog/spdlog.h>namespace engine::ui {UILabel::UILabel(render::TextRenderer *text_renderer,const std::string& text_, const std::string& font_id_, int font_size_, const glm::vec2& position, const engine::utils::FColor& color) : UIElement(position), text_renderer_(text_renderer), text_(text_), font_id_(font_id_), font_size_(font_size_), color_(color){if(text_renderer_ == nullptr){spdlog::error("UILabel缺少TextRenderer");return;}spdlog::trace("UILabel创建成功");}void UILabel::render(engine::core::Context &context){if(!visible_ || text_.empty()) return;auto position = getScreenPosition();text_renderer_->drawUIText(text_, font_id_, font_size_, position, color_);UIElement::render(context);}void UILabel::setText(const std::string &text){// 这里需要注意,我们的修改文本,这个label组件的size_是会改变的text_ = text; size_ = text_renderer_->getTextSize(text, font_id_, font_size_);}void UILabel::setFontId(const std::string &font_id){font_id_ = font_id;size_ = text_renderer_->getTextSize(text_, font_id_, font_size_);}void UILabel::setFontSize(int font_size){font_size_ = font_size;size_ = text_renderer_->getTextSize(text_, font_id_, font_size_);}void UILabel::setColor(const engine::utils::FColor &color){color_ = color;}
}

这里主要是一个render函数比较重要,这是我们是否可以显示文字的关键,我们调用了textrenderer的文字显示;还有几个较为重要的点就是,在动态改变文本内容、修改字体font、字体大小的时候都需要对UILabel的size大小进行设置.

第二部分:构建游戏HUD

接下来,我们需要去GameScene中绘制UI界面了。

3.GameScene中绘制HUD

namespace engine::ui {class UILabel;class UIPanel;
}class GameScene final: public engine::scene::Scene {// ...// UI元素生命周期由UIManager管理,因此使用裸指针缓存engine::ui::UILabel* score_label_ = nullptr;engine::ui::UIPanel* health_panel_ = nullptr;// ...
private:// --- UI 相关函数 ---void createScoreUI();void createHealthUI();// ...
};

这里GameScene中缓存了两个指针,score_label_*、health_panel_*,这里的两个指针只是用于访问,不负责释放,因为我们有关UI的部分都会加到ui_manager_中的root_element_形成树结构,由ui管理器进行统一的释放。

这里使用裸指针是安全的,因为这些UI元素的所有权在 UIManagerroot_element_ 中,它们的生命周期与 GameScene 相同。我们只持有指针用于访问,不负责管理内存。

4.得分UI创建

void GameScene::createScoreUI(){// 创建分数文本textauto score_text = "Score: " + std::to_string(game_session_data_->getCurrentScore());// 创建分数uilabelstd::unique_ptr<engine::ui::UILabel> ui_score = std::make_unique<engine::ui::UILabel>(&context_.getTextRenderer(), score_text,"assets/fonts/VonwaonBitmap-16px.ttf",16);// 缓存裸指针score_label_ = ui_score.get();// 获得label的尺寸, 调整位置auto score_label_size = ui_score->getSize();score_label_->setPosition(glm::vec2(context_.getCamera().getViewportSize().x - score_label_size.x - 10, 5));// 添加到UI管理器ui_manager_->addElement(std::move(ui_score));}

5.创建生命值UI

void GameScene::createHealthUI(){int max_health = game_session_data_->getMaxHealth();int cur_health = game_session_data_->getCurrentHealth();int startx = 10;int starty = 5;int icon_width = 20;int icon_height = 18;int icon_gap = 5;// 创建血量面板std::unique_ptr<engine::ui::UIPanel> panel_heal = std::make_unique<engine::ui::UIPanel>();// 缓存指针health_panel_ = panel_heal.get();// 创建血量背景for(int i = 0; i < game_session_data_->getMaxHealth(); ++i){std::unique_ptr<engine::ui::UIImage> ui_heart = std::make_unique<engine::ui::UIImage>("assets/textures/UI/Heart-bg.png",glm::vec2(startx + (icon_width + icon_gap) * i,starty),glm::vec2(icon_width, icon_height));panel_heal->addChild(std::move(ui_heart));}// 创建血量前景for(int i = 0; i < game_session_data_->getCurrentHealth(); ++i){std::unique_ptr<engine::ui::UIImage> ui_heart = std::make_unique<engine::ui::UIImage>("assets/textures/UI/Heart.png",glm::vec2(startx + (icon_width + icon_gap) * i,starty),glm::vec2(icon_width, icon_height));panel_heal->addChild(std::move(ui_heart));}// 添加到UI管理器ui_manager_->addElement(std::move(panel_heal));}

第三部分:数据绑定

我们在进行分数增加或是血量变化的时候要更新UI。

我们需要实现血量ui、分数score的创建以及更新。

void GameScene::createScoreUI()
{// 创建分数文本textauto score_text = "Score: " + std::to_string(game_session_data_->getCurrentScore());// 创建分数uilabelstd::unique_ptr<engine::ui::UILabel> ui_score = std::make_unique<engine::ui::UILabel>(&context_.getTextRenderer(), score_text,"assets/fonts/VonwaonBitmap-16px.ttf",16);// 缓存裸指针score_label_ = ui_score.get();// 获得label的尺寸, 调整位置auto score_label_size = ui_score->getSize();score_label_->setPosition(glm::vec2(context_.getCamera().getViewportSize().x - score_label_size.x - 40, 5));// 添加到UI管理器ui_manager_->addElement(std::move(ui_score));
}void GameScene::createHealthUI()
{int max_health = game_session_data_->getMaxHealth();int cur_health = game_session_data_->getCurrentHealth();int startx = 10;int starty = 5;int icon_width = 20;int icon_height = 18;int icon_gap = 5;// 创建血量面板std::unique_ptr<engine::ui::UIPanel> panel_heal = std::make_unique<engine::ui::UIPanel>();// 缓存指针health_panel_ = panel_heal.get();// 创建血量背景for(int i = 0; i < max_health; ++i){std::unique_ptr<engine::ui::UIImage> ui_heart = std::make_unique<engine::ui::UIImage>("assets/textures/UI/Heart-bg.png",glm::vec2(startx + (icon_width + icon_gap) * i,starty),glm::vec2(icon_width, icon_height));panel_heal->addChild(std::move(ui_heart));}// 创建血量前景for(int i = 0; i < cur_health; ++i){std::unique_ptr<engine::ui::UIImage> ui_heart = std::make_unique<engine::ui::UIImage>("assets/textures/UI/Heart.png",glm::vec2(startx + (icon_width + icon_gap) * i,starty),glm::vec2(icon_width, icon_height));panel_heal->addChild(std::move(ui_heart));}// 添加到UI管理器ui_manager_->addElement(std::move(panel_heal));
}void GameScene::healWithUI(int heal)
{auto player_hc = player_->getComponent<engine::component::HealthComponent>();player_hc->heal(heal);updateHealthUI();
}void GameScene::updateHealthUI()
{// 更新血量auto cur_health = player_->getComponent<engine::component::HealthComponent>()->getCurrentHealth();game_session_data_->setCurrentHealth(cur_health); // 更新数据auto max_health = game_session_data_->getMaxHealth();// 更新UI// 这里只需要考虑后半部分child成员,因为前半部分是背景for(int i = max_health; i < health_panel_->getChildren().size() ; ++i){bool visible = i - max_health < cur_health;health_panel_->getChildren()[i]->setVisible(visible);}}void GameScene::addScoreWithUI(int score)
{game_session_data_->addScore(score);auto score_text = "Score: " + std::to_string(game_session_data_->getCurrentScore());score_label_->setText(score_text);
}

一个关卡内需要实现数据的同步

组件层 实际生命值 current_health / 数据会话层 current_health 记录游戏状态 / UI 视觉显示层,视觉呈现

章节总结

这节实现了生命值ui和分数ui,通过UIImage和UILabel两个控件实现了场景中ui的绘制,此外对数据与会话层进行了绑定以及实时更新ui。

遇到的问题

1.这是上一节的问题,我们讲讲UIElement的生命周期。

因为项目遵从的是RAII特性,也就是资源获取即初始化,我们声明~UIElement() = default;(默认析构),或者完全不写析构函数,编译器会自动生成一个析构函数,这个自动生成的析构函数会做两件关键事:

  1. 按「成员变量声明的逆序」,调用每个非静态成员变量的析构函数;
  2. 调用基类的析构函数(如果有继承)。

简化版拆解UIElement的销毁流程。

// 简化版UIElement
class UIElement {
private:// 成员1:普通变量(无析构逻辑)int pos_x_ = 0;// 成员2:容器(有自定义析构)std::vector<std::unique_ptr<UIElement>> children_;public:~UIElement() = default;
};

当我们执行了delete ui;(销毁堆上的UIElement对象)时,编译器自动生成的析构逻辑如下:

// 编译器自动生成的析构逻辑
UIElement::~UIElement() {// 按逆序销毁成员变量(先销毁最后声明的成员)// 销毁 children_(vector容器)children_.~vector(); // 销毁 pos_x_(int,无析构,仅标记内存可用)// (int是内置类型,无析构函数,编译器仅释放其占用的内存)// 步骤3:调用基类析构(如果有,比如UIElement继承自BaseUI,则调用~BaseUI())
}

成员变量的析构会触发 链式销毁

重点在「成员变量的析构函数」会做额外工作,这才是资源释放的核心:

销毁 children_(vector 容器)

vector 的析构函数会做两件事:

  • 遍历容器内的所有元素(每个 unique_ptr<UIElement>),调用每个元素的析构函数;
  • 释放 vector 内部动态分配的缓冲区(存储这些 unique_ptr 的堆内存)。

容器内的 unique_ptr 析构

每个 unique_ptr<UIElement> 的析构函数会执行:

// unique_ptr自动生成的析构逻辑
template<class T>
unique_ptr<T>::~unique_ptr() {if (ptr_) { // ptr_是unique_ptr内部存储的指针delete ptr_; // 释放指向的UIElement对象(堆上)}
}

这一步会触发「子 UIElement 对象的析构」(递归执行上述所有步骤)。

释放 UIElement 对象本身的内存

所有成员变量销毁后,编译器会通知操作系统:「这块堆内存(UIElement 对象)可以回收了」,最终释放整个对象占用的内存。

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

相关文章:

  • 2026年方管标杆供应厂家最新推荐:Q235方管、Q355方管、无缝方管、钢结构方管、河南红宇供应链,品质方管适配多行业需求 - 海棠依旧大
  • ArcGIS Server发布的地图服务不显示地图的原因分析
  • ArcGIS中点转线面的方法
  • 2026年3月河南方管供应企业最新推荐榜单:镀锌方管、黑方管、镀锌方矩管、热镀锌方管、热镀方矩管、各类方管、方矩管采购选择指南 - 海棠依旧大
  • 基本元器件——比较器
  • 小型校园网的设计与组建
  • 3款降AI工具实测对比:价格差3倍效果差多少?结果出乎意料
  • C++跨平台开发实战
  • Git常用指令
  • 注塑机数据采集以及数据应用
  • 【Vibe Coding解惑】AI 写代码靠谱吗?真实案例解析
  • C语言预处理(通俗易懂)
  • 《Python基础教程》专栏总结篇
  • SSH超时断开连接时长控制ServerAliveInterval和TMOUT设置
  • 最大公约数gcd和Win32版本实现
  • Android笔记
  • 【AI】Interesting Applications
  • 【转子动力学】滚动轴承SFK6205故障(含外圈故障、内圈故障、滚动体故障、复合故障)柔性阶梯转子系统非线性动力学【含Matlab源码 15157期】
  • Linux命令快查
  • Size Limit 终极指南:多环境配置与性能预算管理
  • 【工具-===========】
  • PCL 根据时间索引提取扫描线【2026最新版】
  • leetcode 769, 768 最多能完成排序的块 单调栈建模
  • MMDrawerController状态恢复终极指南:确保iOS侧边栏数据永不丢失
  • 扒下满级“赛博打工人”的底裤:从 OpenClaw 爆火,看透 Agent、MCP 与 RAG 的底层逻辑
  • 高速下载b站视频的解决方案
  • AbMole丨Honokiol(和厚朴酚):一种具有多靶点调节活性的天然产物及其科研应用
  • Maven管理Oracle JDBC驱动
  • Mitutoyo三丰 无线蓝牙数据发送器 协议解析
  • LLM-Adapters核心功能解析:7种适配器如何让大模型微调效率提升90%