From f515afde7e1df798d39500e1cb0fef8eb7f82c90 Mon Sep 17 00:00:00 2001 From: Ziyu Date: Sat, 14 Jun 2025 10:28:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=94=9F=E5=91=BD=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E4=B8=8E=E5=BE=97=E5=88=86=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 2 + src/engine/component/health_component.cpp | 5 +- src/engine/component/health_component.h | 2 +- src/engine/render/text_renderer.cpp | 4 +- src/engine/render/text_renderer.h | 5 +- src/engine/ui/ui_image.cpp | 40 +++++++++ src/engine/ui/ui_image.h | 52 +++++++++++ src/engine/ui/ui_label.cpp | 58 ++++++++++++ src/engine/ui/ui_label.h | 60 +++++++++++++ src/game/scene/game_scene.cpp | 103 +++++++++++++++++++--- src/game/scene/game_scene.h | 18 +++- 11 files changed, 329 insertions(+), 20 deletions(-) create mode 100644 src/engine/ui/ui_image.cpp create mode 100644 src/engine/ui/ui_image.h create mode 100644 src/engine/ui/ui_label.cpp create mode 100644 src/engine/ui/ui_label.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 08870c2..54bad7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,8 @@ add_executable(${TARGET} src/engine/ui/ui_manager.cpp src/engine/ui/ui_element.cpp src/engine/ui/ui_panel.cpp + src/engine/ui/ui_image.cpp + src/engine/ui/ui_label.cpp src/game/scene/game_scene.cpp src/game/component/player_component.cpp src/game/component/ai_component.cpp diff --git a/src/engine/component/health_component.cpp b/src/engine/component/health_component.cpp index cefe26b..367b664 100644 --- a/src/engine/component/health_component.cpp +++ b/src/engine/component/health_component.cpp @@ -43,15 +43,16 @@ bool HealthComponent::takeDamage(int damage_amount) { return true; // 造成伤害,返回true } -void HealthComponent::heal(int heal_amount) { +int HealthComponent::heal(int heal_amount) { if (heal_amount <= 0 || !isAlive()) { - return; // 不治疗或已经死亡 + return current_health_; // 不治疗或已经死亡 } current_health_ += heal_amount; current_health_ = std::min(max_health_, current_health_); // 防止超过最大生命值 spdlog::debug("游戏对象 '{}' 治疗了 {} 点,当前生命值: {}/{}。", owner_ ? owner_->getName() : "Unknown", heal_amount, current_health_, max_health_); + return current_health_; } void HealthComponent::setInvincible(float duration) diff --git a/src/engine/component/health_component.h b/src/engine/component/health_component.h index b4ff6a4..7c04600 100644 --- a/src/engine/component/health_component.h +++ b/src/engine/component/health_component.h @@ -38,7 +38,7 @@ public: * @return bool 如果成功造成伤害,则返回 true,否则返回 false。 */ bool takeDamage(int damage_amount); - void heal(int heal_amount); ///< @brief 治疗 GameObject,增加当前生命值(不超过最大生命值)。 + int heal(int heal_amount); ///< @brief 治疗 GameObject,增加当前生命值(不超过最大生命值),返回治疗后生命值 // --- Getters and Setters --- bool isAlive() const { return current_health_ > 0; } ///< @brief 检查 GameObject 是否存活(当前生命值大于 0)。 diff --git a/src/engine/render/text_renderer.cpp b/src/engine/render/text_renderer.cpp index 73a940d..895be2f 100644 --- a/src/engine/render/text_renderer.cpp +++ b/src/engine/render/text_renderer.cpp @@ -44,7 +44,7 @@ void TextRenderer::close() } void TextRenderer::drawUIText(const std::string &text, const std::string &font_id, int font_size, - const glm::vec2 &position, const SDL_FColor &color) + const glm::vec2 &position, const engine::utils::FColor &color) { /* 构造函数已经保证了必要指针不会为空,这里不需要再检查 */ TTF_Font* font = resource_manager_->getFont(font_id, font_size); @@ -77,7 +77,7 @@ void TextRenderer::drawUIText(const std::string &text, const std::string &font_i } void TextRenderer::drawText(const Camera &camera, const std::string &text, const std::string &font_id, int font_size, - const glm::vec2 &position, const SDL_FColor &color) + const glm::vec2 &position, const engine::utils::FColor &color) { // 应用相机变换 glm::vec2 position_screen = camera.worldToScreen(position); diff --git a/src/engine/render/text_renderer.h b/src/engine/render/text_renderer.h index b384b3a..b2be857 100644 --- a/src/engine/render/text_renderer.h +++ b/src/engine/render/text_renderer.h @@ -2,6 +2,7 @@ #include #include #include +#include "../utils/math.h" struct TTF_TextEngine; @@ -48,7 +49,7 @@ public: * @param color 文本颜色。(默认为白色) */ void drawUIText(const std::string& text, const std::string& font_id, int font_size, - const glm::vec2& position, const SDL_FColor& color = {1.0f, 1.0f, 1.0f, 1.0f}); + const glm::vec2& position, const engine::utils::FColor& color = {1.0f, 1.0f, 1.0f, 1.0f}); /** * @brief 绘制地图上的字符串。 @@ -61,7 +62,7 @@ public: * @param color 文本颜色。 */ void drawText(const Camera& camera, const std::string& text, const std::string& font_id, int font_size, - const glm::vec2& position, const SDL_FColor& color = {1.0f, 1.0f, 1.0f, 1.0f}); + const glm::vec2& position, const engine::utils::FColor& color = {1.0f, 1.0f, 1.0f, 1.0f}); /** * @brief 获取文本的尺寸。 diff --git a/src/engine/ui/ui_image.cpp b/src/engine/ui/ui_image.cpp new file mode 100644 index 0000000..fd64fe3 --- /dev/null +++ b/src/engine/ui/ui_image.cpp @@ -0,0 +1,40 @@ +#include "ui_image.h" +#include "../render/renderer.h" +#include "../render/sprite.h" +#include "../core/context.h" +#include + +namespace engine::ui { + +UIImage::UIImage(const std::string& texture_id, + const glm::vec2& position, + const glm::vec2& size, + const std::optional& source_rect, + bool is_flipped) + : UIElement(position, size), + sprite_(texture_id, source_rect, is_flipped) +{ + if (texture_id.empty()) { + spdlog::warn("创建了一个空纹理ID的UIImage。"); + } + spdlog::trace("UIImage 构造完成"); +} + +void UIImage::render(engine::core::Context& context) { + if (!visible_ || sprite_.getTextureId().empty()) { + return; // 如果不可见或没有分配纹理则不渲染 + } + + // 渲染自身 + auto position = getScreenPosition(); + if (size_.x == 0.0f && size_.y == 0.0f) { // 如果尺寸为0,则使用纹理的原始尺寸 + context.getRenderer().drawUISprite(sprite_, position); + } else { + context.getRenderer().drawUISprite(sprite_, position, size_); + } + + // 渲染子元素(调用基类方法) + UIElement::render(context); +} + +} // namespace engine::ui \ No newline at end of file diff --git a/src/engine/ui/ui_image.h b/src/engine/ui/ui_image.h new file mode 100644 index 0000000..7bddfe5 --- /dev/null +++ b/src/engine/ui/ui_image.h @@ -0,0 +1,52 @@ +#pragma once +#include "ui_element.h" +#include "../render/sprite.h" +#include +#include +#include + +namespace engine::ui { + +/** + * @brief 一个用于显示纹理或部分纹理的UI元素。 + * + * 继承自UIElement并添加了渲染图像的功能。 + */ +class UIImage final : public UIElement { +protected: + engine::render::Sprite sprite_; + +public: + /** + * @brief 构造一个UIImage对象。 + * + * @param texture_id 要显示的纹理ID。 + * @param position 图像的局部位置。 + * @param size 图像元素的大小。(如果为{0,0},则使用纹理的原始尺寸) + * @param source_rect 可选:要绘制的纹理部分。(如果为空,则使用纹理的整个区域) + * @param is_flipped 可选:精灵是否应该水平翻转。 + */ + UIImage(const std::string& texture_id, + const glm::vec2& position = {0.0f, 0.0f}, + const glm::vec2& size = {0.0f, 0.0f}, + const std::optional& source_rect = std::nullopt, + bool is_flipped = false); + + // --- 核心方法 --- + void render(engine::core::Context& context) override; + + // --- Setters & Getters --- + const engine::render::Sprite& getSprite() const { return sprite_; } + void setSprite(const engine::render::Sprite& sprite) { sprite_ = sprite; } + + const std::string& getTextureId() const { return sprite_.getTextureId(); } + void setTextureId(const std::string& texture_id) { sprite_.setTextureId(texture_id); } + + const std::optional& getSourceRect() const { return sprite_.getSourceRect(); } + void setSourceRect(const std::optional& source_rect) { sprite_.setSourceRect(source_rect); } + + bool isFlipped() const { return sprite_.isFlipped(); } + void setFlipped(bool flipped) { sprite_.setFlipped(flipped); } +}; + +} // namespace engine::ui \ No newline at end of file diff --git a/src/engine/ui/ui_label.cpp b/src/engine/ui/ui_label.cpp new file mode 100644 index 0000000..348da52 --- /dev/null +++ b/src/engine/ui/ui_label.cpp @@ -0,0 +1,58 @@ +#include "ui_label.h" +#include "../core/context.h" +#include "../render/text_renderer.h" +#include + +namespace engine::ui { + +UILabel::UILabel(engine::render::TextRenderer& text_renderer, + const std::string& text, + const std::string& font_id, + int font_size, + const engine::utils::FColor& text_color, + const glm::vec2& position) + : UIElement(position), + text_renderer_(text_renderer), + text_(text), + font_id_(font_id), + font_size_(font_size), + text_fcolor_(text_color) { + // 获取文本渲染尺寸 + size_ = text_renderer_.getTextSize(text_, font_id_, font_size_); + spdlog::trace("UILabel 构造完成"); +} + +void UILabel::render(engine::core::Context& context) { + if (!visible_ || text_.empty()) return; + + text_renderer_.drawUIText(text_, font_id_, font_size_, getScreenPosition(), text_fcolor_); + + // 渲染子元素(调用基类方法) + UIElement::render(context); +} + +void UILabel::setText(const std::string &text) +{ + 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::setTextFColor(const engine::utils::FColor &text_fcolor) +{ + text_fcolor_ = text_fcolor; + /* 颜色变化不影响尺寸 */ +} + +} // namespace engine::ui \ No newline at end of file diff --git a/src/engine/ui/ui_label.h b/src/engine/ui/ui_label.h new file mode 100644 index 0000000..3eb6b09 --- /dev/null +++ b/src/engine/ui/ui_label.h @@ -0,0 +1,60 @@ +#pragma once +#include "ui_element.h" +#include "../utils/math.h" +#include "../render/text_renderer.h" + +namespace engine::ui { + +/** + * @brief UILabel 类用于创建和管理用户界面中的文本标签 + * + * UILabel 继承自 UIElement,提供了文本渲染功能。 + * 它可以设置文本内容、字体ID、字体大小和文本颜色。 + * + * @note 需要一个文本渲染器来获取和更新文本尺寸。 + */ +class UILabel final : public UIElement { +private: + engine::render::TextRenderer& text_renderer_; ///< @brief 需要文本渲染器,用于获取/更新文本尺寸 + + std::string text_; ///< @brief 文本内容 + std::string font_id_; ///< @brief 字体ID + int font_size_; ///< @brief 字体大小 + engine::utils::FColor text_fcolor_ = {1.0f, 1.0f, 1.0f, 1.0f}; + /* 可添加其他内容,例如边框、底色 */ + +public: + /** + * @brief 构造一个UILabel + * + * @param text_renderer 文本渲染器 + * @param text 文本内容 + * @param font_id 字体ID + * @param font_size 字体大小 + * @param text_color 文本颜色 + */ + UILabel(engine::render::TextRenderer& text_renderer, + const std::string& text, + const std::string& font_id, + int font_size = 16, + const engine::utils::FColor& text_color = {1.0f, 1.0f, 1.0f, 1.0f}, + const glm::vec2& position = {0.0f, 0.0f}); + + // --- 核心方法 --- + void render(engine::core::Context& context) override; + + // --- Setters & Getters --- + 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& getTextFColor() const { return text_fcolor_; } + + void setText(const std::string& text); ///< @brief 设置文本内容, 同时更新尺寸 + void setFontId(const std::string& font_id); ///< @brief 设置字体ID, 同时更新尺寸 + void setFontSize(int font_size); ///< @brief 设置字体大小, 同时更新尺寸 + void setTextFColor(const engine::utils::FColor& text_fcolor); + +}; + + +} // namespace engine::ui \ No newline at end of file diff --git a/src/game/scene/game_scene.cpp b/src/game/scene/game_scene.cpp index 9d8802c..80f3c06 100644 --- a/src/game/scene/game_scene.cpp +++ b/src/game/scene/game_scene.cpp @@ -19,6 +19,8 @@ #include "../../engine/audio/audio_player.h" #include "../../engine/ui/ui_manager.h" #include "../../engine/ui/ui_panel.h" +#include "../../engine/ui/ui_label.h" +#include "../../engine/ui/ui_image.h" #include "../../engine/utils/math.h" #include "../component/ai_component.h" #include "../component/ai/patrol_behavior.h" @@ -198,10 +200,8 @@ bool GameScene::initUI() { if (!ui_manager_->init(glm::vec2(640.0f, 360.0f))) return false; - // 创建一个透明的方形UIPanel - ui_manager_->addElement(std::make_unique(glm::vec2(100.0f, 100.0f), - glm::vec2(200.0f, 200.0f), - engine::utils::FColor{0.5f, 0.0f, 0.0f, 0.3f})); + createScoreUI(); + createHealthUI(); return true; } @@ -227,10 +227,10 @@ void GameScene::handleObjectCollisions() } // 处理玩家与"hazard"对象碰撞 else if (obj1->getName() == "player" && obj2->getTag() == "hazard") { - obj1->getComponent()->takeDamage(1); + handlePlayerDamage(1); spdlog::debug("玩家 {} 受到了 HAZARD 对象伤害", obj1->getName()); } else if (obj2->getName() == "player" && obj1->getTag() == "hazard") { - obj2->getComponent()->takeDamage(1); + handlePlayerDamage(1); spdlog::debug("玩家 {} 受到了 HAZARD 对象伤害", obj2->getName()); } // 处理玩家与关底触发器碰撞 @@ -269,8 +269,8 @@ void GameScene::handlePlayerDamage(int damage) spdlog::info("玩家 {} 死亡", player_->getName()); // TODO: 可能的死亡逻辑处理 } - // 更新游戏数据(生命值) - game_session_data_->setCurrentHealth(player_component->getHealthComponent()->getCurrentHealth()); + // 更新生命值及HealthUI + updateHealthWithUI(); } void GameScene::playerVSEnemyCollision(engine::object::GameObject *player, engine::object::GameObject *enemy) @@ -301,7 +301,7 @@ void GameScene::playerVSEnemyCollision(engine::object::GameObject *player, engin // 播放音效 (此音效完全可以放在玩家的音频组件中,这里示例另一种用法:直接用AudioPlayer播放,传入文件路径) context_.getAudioPlayer().playSound("assets/audio/punch2a.mp3"); // 加分 - game_session_data_->addScore(10); + addScoreWithUI(10); } // 踩踏判断失败,玩家受伤 else { @@ -310,12 +310,12 @@ void GameScene::playerVSEnemyCollision(engine::object::GameObject *player, engin } } -void GameScene::playerVSItemCollision(engine::object::GameObject * player, engine::object::GameObject * item) +void GameScene::playerVSItemCollision(engine::object::GameObject*, engine::object::GameObject * item) { if (item->getName() == "fruit") { - player->getComponent()->heal(1); // 加血 + healWithUI(1); // 加血 } else if (item->getName() == "gem") { - game_session_data_->addScore(5); + addScoreWithUI(5); // 加5分 } item->setNeedRemove(true); // 标记道具为待删除状态 auto item_aabb = item->getComponent()->getWorldAABB(); @@ -368,4 +368,83 @@ void GameScene::createEffect(const glm::vec2& center_pos, const std::string &tag spdlog::debug("创建特效: {}", tag); } +void GameScene::createScoreUI() { + // 创建得分标签 + auto score_text = "Score: " + std::to_string(game_session_data_->getCurrentScore()); + auto score_label = std::make_unique(context_.getTextRenderer(), + score_text, + "assets/fonts/VonwaonBitmap-16px.ttf", + 16); + score_label_ = score_label.get(); // 成员变量赋值(获取裸指针) + auto screen_size = ui_manager_->getRootElement()->getSize(); // 获取屏幕尺寸 + score_label_->setPosition(glm::vec2(screen_size.x - 100.0f, 10.0f)); + ui_manager_->addElement(std::move(score_label)); +} + +void GameScene::createHealthUI() { + int max_health = game_session_data_->getMaxHealth(); + int current_health = game_session_data_->getCurrentHealth(); + float start_x = 10.0f; + float start_y = 10.0f; + float icon_width = 20.0f; + float icon_height = 18.0f; + float spacing = 5.0f; + std::string full_heart_tex = "assets/textures/UI/Heart.png"; + std::string empty_heart_tex = "assets/textures/UI/Heart-bg.png"; + + // 创建一个默认的UIPanel (不需要背景色,因此大小无所谓,只用于定位) + auto health_panel = std::make_unique(); + health_panel_ = health_panel.get(); // 成员变量赋值(获取裸指针) + + // --- 根据最大生命值,循环创建生命值图标(添加到UIPanel中) --- + for (int i = 0; i < max_health; ++i) { // 创建背景图标 + glm::vec2 icon_pos = {start_x + i * (icon_width + spacing), start_y}; + glm::vec2 icon_size = {icon_width, icon_height}; + + auto bg_icon = std::make_unique(empty_heart_tex, icon_pos, icon_size); + health_panel_->addChild(std::move(bg_icon)); + } + for (int i = 0; i < current_health; ++i) { // 创建前景图标 + glm::vec2 icon_pos = {start_x + i * (icon_width + spacing), start_y}; + glm::vec2 icon_size = {icon_width, icon_height}; + + auto fg_icon = std::make_unique(full_heart_tex, icon_pos, icon_size); + health_panel_->addChild(std::move(fg_icon)); + } + // 将UIPanel添加到UI管理器中 + ui_manager_->addElement(std::move(health_panel)); +} + +void GameScene::updateHealthWithUI() +{ + if (!player_ || !health_panel_) { + spdlog::error("玩家对象或 HealthPanel 不存在,无法更新生命值UI"); + return; + } + + // 获取当前生命值并更新游戏数据 + auto current_health = player_->getComponent()->getCurrentHealth(); + game_session_data_->setCurrentHealth(current_health); + auto max_health = game_session_data_->getMaxHealth(); + + // 前景图标是后添加的,因此设置后半段的可见性即可 + for (auto i = max_health; i < max_health * 2; ++i) { + health_panel_->getChildren()[i]->setVisible(i - max_health < current_health); + } +} + +void GameScene::addScoreWithUI(int score) +{ + game_session_data_->addScore(score); + auto score_text = "Score: " + std::to_string(game_session_data_->getCurrentScore()); + spdlog::info("得分: {}", score_text); + score_label_->setText(score_text); +} + +void GameScene::healWithUI(int amount) +{ + player_->getComponent()->heal(amount); + updateHealthWithUI(); // 更新生命值与UI +} + } // namespace game::scene \ No newline at end of file diff --git a/src/game/scene/game_scene.h b/src/game/scene/game_scene.h index 9cccc67..4491527 100644 --- a/src/game/scene/game_scene.h +++ b/src/game/scene/game_scene.h @@ -12,6 +12,11 @@ namespace game::data { class SessionData; } +namespace engine::ui { + class UILabel; + class UIPanel; +} + namespace game::scene { /** @@ -19,7 +24,10 @@ namespace game::scene { */ class GameScene final: public engine::scene::Scene { std::shared_ptr game_session_data_; ///< @brief 场景间共享数据,因此用shared_ptr - engine::object::GameObject* player_ = nullptr; ///< @brief 保存玩家对象的指针,方便访问 + engine::object::GameObject* player_ = nullptr; ///< @brief 保存玩家对象的指针,方便访问 + + engine::ui::UILabel* score_label_ = nullptr; ///< @brief 得分标签 (生命周期由UIManager管理,因此使用裸指针) + engine::ui::UIPanel* health_panel_ = nullptr; ///< @brief 生命值图标面板 public: GameScene(engine::core::Context& context, @@ -56,6 +64,14 @@ private: * @param tag 特效标签(决定特效类型,例如"enemy","item") */ void createEffect(const glm::vec2& center_pos, const std::string& tag); + + // --- UI 相关函数 --- + void createScoreUI(); ///< @brief 创建得分UI + void createHealthUI(); ///< @brief 创建生命值UI (或最大生命值改变时重设) + void addScoreWithUI(int score); ///< @brief 增加得分,同时更新UI + void healWithUI(int amount); ///< @brief 增加生命,同时更新UI + void updateHealthWithUI(); ///< @brief 更新生命值UI (只适用最大生命值不变的情况) + }; } // namespace game::scene \ No newline at end of file