完成生命图标与得分记录

This commit is contained in:
Ziyu
2025-06-16 14:44:47 +08:00
parent 8b10d40822
commit f515afde7e
11 changed files with 329 additions and 20 deletions
+3 -2
View File
@@ -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)
+1 -1
View File
@@ -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)。
+2 -2
View File
@@ -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);
+3 -2
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL_render.h>
#include <string>
#include <glm/vec2.hpp>
#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 获取文本的尺寸。
+40
View File
@@ -0,0 +1,40 @@
#include "ui_image.h"
#include "../render/renderer.h"
#include "../render/sprite.h"
#include "../core/context.h"
#include <spdlog/spdlog.h>
namespace engine::ui {
UIImage::UIImage(const std::string& texture_id,
const glm::vec2& position,
const glm::vec2& size,
const std::optional<SDL_FRect>& 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
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include "ui_element.h"
#include "../render/sprite.h"
#include <string>
#include <optional>
#include <SDL3/SDL_rect.h>
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<SDL_FRect>& 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<SDL_FRect>& getSourceRect() const { return sprite_.getSourceRect(); }
void setSourceRect(const std::optional<SDL_FRect>& source_rect) { sprite_.setSourceRect(source_rect); }
bool isFlipped() const { return sprite_.isFlipped(); }
void setFlipped(bool flipped) { sprite_.setFlipped(flipped); }
};
} // namespace engine::ui
+58
View File
@@ -0,0 +1,58 @@
#include "ui_label.h"
#include "../core/context.h"
#include "../render/text_renderer.h"
#include <spdlog/spdlog.h>
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
+60
View File
@@ -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
+91 -12
View File
@@ -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<engine::ui::UIPanel>(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<game::component::PlayerComponent>()->takeDamage(1);
handlePlayerDamage(1);
spdlog::debug("玩家 {} 受到了 HAZARD 对象伤害", obj1->getName());
} else if (obj2->getName() == "player" && obj1->getTag() == "hazard") {
obj2->getComponent<game::component::PlayerComponent>()->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<engine::component::HealthComponent>()->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<engine::component::ColliderComponent>()->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<engine::ui::UILabel>(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<engine::ui::UIPanel>();
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<engine::ui::UIImage>(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<engine::ui::UIImage>(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<engine::component::HealthComponent>()->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<engine::component::HealthComponent>()->heal(amount);
updateHealthWithUI(); // 更新生命值与UI
}
} // namespace game::scene
+17 -1
View File
@@ -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::data::SessionData> 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