完成按钮及可交互UI元素

This commit is contained in:
Ziyu
2025-06-16 14:44:47 +08:00
parent f515afde7e
commit 99d4a8bbcc
14 changed files with 451 additions and 0 deletions
+5
View File
@@ -62,9 +62,14 @@ add_executable(${TARGET}
src/engine/scene/level_loader.cpp
src/engine/ui/ui_manager.cpp
src/engine/ui/ui_element.cpp
src/engine/ui/ui_interactive.cpp
src/engine/ui/ui_panel.cpp
src/engine/ui/ui_image.cpp
src/engine/ui/ui_label.cpp
src/engine/ui/ui_button.cpp
src/engine/ui/state/ui_normal_state.cpp
src/engine/ui/state/ui_pressed_state.cpp
src/engine/ui/state/ui_hover_state.cpp
src/game/scene/game_scene.cpp
src/game/component/player_component.cpp
src/game/component/ai_component.cpp
+30
View File
@@ -0,0 +1,30 @@
#include "ui_hover_state.h"
#include "ui_normal_state.h"
#include "ui_pressed_state.h"
#include "../ui_interactive.h"
#include "../../input/input_manager.h"
#include "../../core/context.h"
#include <spdlog/spdlog.h>
namespace engine::ui::state {
void UIHoverState::enter()
{
owner_->setSprite("hover");
spdlog::debug("切换到悬停状态");
}
std::unique_ptr<UIState> UIHoverState::handleInput(engine::core::Context& context)
{
auto& input_manager = context.getInputManager();
auto mouse_pos = input_manager.getLogicalMousePosition();
if (!owner_->isPointInside(mouse_pos)) { // 如果鼠标不在UI元素内,则返回正常状态
return std::make_unique<UINormalState>(owner_);
}
if (input_manager.isActionPressed("MouseLeftClick")) { // 如果鼠标按下,则返回按下状态
return std::make_unique<UIPressedState>(owner_);
}
return nullptr;
}
} // namespace engine::ui::state
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "ui_state.h"
namespace engine::ui::state {
/**
* @brief 悬停状态
*
* 当鼠标悬停在UI元素上时,会切换到该状态。
*/
class UIHoverState final: public UIState {
friend class engine::ui::UIInteractive;
public:
UIHoverState(engine::ui::UIInteractive* owner) : UIState(owner) {}
~UIHoverState() override = default;
private:
void enter() override;
std::unique_ptr<UIState> handleInput(engine::core::Context& context) override;
};
} // namespace engine::ui::state
+28
View File
@@ -0,0 +1,28 @@
#include "ui_normal_state.h"
#include "ui_hover_state.h"
#include "../ui_interactive.h"
#include "../../input/input_manager.h"
#include "../../core/context.h"
#include "../../audio/audio_player.h"
#include <spdlog/spdlog.h>
namespace engine::ui::state {
void UINormalState::enter()
{
owner_->setSprite("normal");
spdlog::debug("切换到正常状态");
}
std::unique_ptr<UIState> UINormalState::handleInput(engine::core::Context& context)
{
auto& input_manager = context.getInputManager();
auto mouse_pos = input_manager.getLogicalMousePosition();
if (owner_->isPointInside(mouse_pos)) { // 如果鼠标在UI元素内,则切换到悬停状态
owner_->playSound("hover");
return std::make_unique<engine::ui::state::UIHoverState>(owner_);
}
return nullptr;
}
} // namespace engine::ui::state
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "ui_state.h"
namespace engine::ui::state {
/**
* @brief 正常状态
*
* 正常状态是UI元素的默认状态。
*/
class UINormalState final: public UIState {
friend class engine::ui::UIInteractive;
public:
UINormalState(engine::ui::UIInteractive* owner) : UIState(owner) {}
~UINormalState() override = default;
private:
void enter() override;
std::unique_ptr<UIState> handleInput(engine::core::Context& context) override;
};
} // namespace engine::ui::state
+34
View File
@@ -0,0 +1,34 @@
#include "ui_pressed_state.h"
#include "ui_normal_state.h"
#include "ui_hover_state.h"
#include "../ui_interactive.h"
#include "../../input/input_manager.h"
#include "../../core/context.h"
#include <spdlog/spdlog.h>
namespace engine::ui::state {
void UIPressedState::enter()
{
owner_->setSprite("pressed");
owner_->playSound("pressed");
spdlog::debug("切换到按下状态");
}
std::unique_ptr<UIState> UIPressedState::handleInput(engine::core::Context& context)
{
auto& input_manager = context.getInputManager();
auto mouse_pos = input_manager.getLogicalMousePosition();
if (input_manager.isActionReleased("MouseLeftClick")) {
if (!owner_->isPointInside(mouse_pos)) { // 松开鼠标时,如果不在UI元素内,则切换到正常状态
return std::make_unique<engine::ui::state::UINormalState>(owner_);
} else { // 松开鼠标时,如果还在UI元素内,则触发点击事件
owner_->clicked();
return std::make_unique<engine::ui::state::UIHoverState>(owner_);
}
}
return nullptr;
}
} // namespace engine::ui::state
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "ui_state.h"
namespace engine::ui::state {
/**
* @brief 按下状态
*
* 当鼠标按下UI元素时,会切换到该状态。
*/
class UIPressedState final: public UIState {
friend class engine::ui::UIInteractive;
public:
UIPressedState(engine::ui::UIInteractive* owner) : UIState(owner) {}
~UIPressedState() override = default;
private:
void enter() override;
std::unique_ptr<UIState> handleInput(engine::core::Context& context) override;
};
} // namespace engine::ui::state
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <memory>
namespace engine::core {
class Context;
}
namespace engine::ui {
class UIInteractive;
}
namespace engine::ui::state {
/**
* @brief 可交互UI元素在特定状态下的行为接口。
*
* 该接口定义了所有具体UI状态必须实现的通用操作,
* 例如处理输入事件、更新状态逻辑以及确定视觉表现。
*/
class UIState {
friend class engine::ui::UIInteractive;
protected:
engine::ui::UIInteractive* owner_ = nullptr; ///< @brief 指向父节点
public:
/**
* @brief 构造函数传入父节点指针
*/
UIState(engine::ui::UIInteractive* owner) : owner_(owner) {}
virtual ~UIState() = default;
// 删除拷贝和移动构造函数/赋值运算符
UIState(const UIState&) = delete;
UIState& operator=(const UIState&) = delete;
UIState(UIState&&) = delete;
UIState& operator=(UIState&&) = delete;
protected:
// --- 核心方法 ---
virtual void enter() {}
virtual std::unique_ptr<UIState> handleInput(engine::core::Context& context) = 0;
};
} // namespace engine::ui::state
+33
View File
@@ -0,0 +1,33 @@
#include "ui_button.h"
#include "state/ui_normal_state.h"
#include <spdlog/spdlog.h>
namespace engine::ui {
UIButton::UIButton(engine::core::Context& context,
const std::string &normal_sprite_id,
const std::string &hover_sprite_id,
const std::string &pressed_sprite_id,
const glm::vec2 &position,
const glm::vec2 &size,
std::function<void()> callback)
: UIInteractive(context, position, size), callback_(std::move(callback))
{
addSprite("normal", std::make_unique<engine::render::Sprite>(normal_sprite_id));
addSprite("hover", std::make_unique<engine::render::Sprite>(hover_sprite_id));
addSprite("pressed", std::make_unique<engine::render::Sprite>(pressed_sprite_id));
// 设置默认状态为"normal"
setState(std::make_unique<engine::ui::state::UINormalState>(this));
// 设置默认音效
addSound("hover", "assets/audio/button_hover.wav");
addSound("pressed", "assets/audio/button_click.wav");
spdlog::trace("UIButton 构造完成");
}
void UIButton::clicked()
{
if (callback_) callback_();
}
} // namespace engine::ui
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "ui_interactive.h"
#include <functional>
#include <utility>
namespace engine::ui {
/**
* @brief 按钮UI元素
*
* 继承自UIInteractive,用于创建可交互的按钮。
* 支持三种状态:正常、悬停、按下。
* 支持回调函数,当按钮被点击时调用。
*/
class UIButton final: public UIInteractive {
private:
std::function<void()> callback_; ///< @brief 可自定义的函数(函数包装器)
public:
/**
* @brief 构造函数
* @param normal_sprite_id 正常状态的精灵ID
* @param hover_sprite_id 悬停状态的精灵ID
* @param pressed_sprite_id 按下状态的精灵ID
* @param position 位置
* @param size 大小
* @param callback 回调函数
*/
UIButton(engine::core::Context& context,
const std::string& normal_sprite_id,
const std::string& hover_sprite_id,
const std::string& pressed_sprite_id,
const glm::vec2& position = {0.0f, 0.0f},
const glm::vec2& size = {0.0f, 0.0f},
std::function<void()> callback = nullptr);
~UIButton() override = default;
void clicked() override; ///< @brief 重写基类方法,当按钮被点击时调用回调函数
void setCallback(std::function<void()> callback) { callback_ = std::move(callback); } ///< @brief 设置点击回调函数
std::function<void()> getCallback() const { return callback_; } ///< @brief 获取点击回调函数
};
} // namespace engine::ui
+90
View File
@@ -0,0 +1,90 @@
#include "ui_interactive.h"
#include "state/ui_state.h"
#include "../core/context.h"
#include "../render/renderer.h"
#include "../resource/resource_manager.h"
#include "../audio/audio_player.h"
#include <spdlog/spdlog.h>
namespace engine::ui {
UIInteractive::~UIInteractive() = default;
UIInteractive::UIInteractive(engine::core::Context &context, const glm::vec2 &position, const glm::vec2 &size)
: UIElement(position, size), context_(context)
{
spdlog::trace("UIInteractive 构造完成");
}
void UIInteractive::setState(std::unique_ptr<engine::ui::state::UIState> state)
{
if (!state) {
spdlog::warn("尝试设置空的状态!");
return;
}
state_ = std::move(state);
state_->enter();
}
void UIInteractive::addSprite(const std::string &name, std::unique_ptr<engine::render::Sprite> sprite)
{
// 可交互UI元素必须有一个size用于交互检测,因此如果参数列表中没有指定,则用图片大小作为size
if (size_.x == 0.0f && size_.y == 0.0f) {
size_ = context_.getResourceManager().getTextureSize(sprite->getTextureId());
}
// 添加精灵
sprites_[name] = std::move(sprite);
}
void UIInteractive::setSprite(const std::string &name)
{
if (sprites_.find(name) != sprites_.end()) {
current_sprite_ = sprites_[name].get();
} else {
spdlog::warn("Sprite '{}' 未找到", name);
}
}
void UIInteractive::addSound(const std::string &name, const std::string &path)
{
sounds_[name] = path;
}
void UIInteractive::playSound(const std::string &name)
{
if (sounds_.find(name) != sounds_.end()) {
context_.getAudioPlayer().playSound(sounds_[name]);
} else {
spdlog::error("Sound '{}' 未找到", name);
}
}
bool UIInteractive::handleInput(engine::core::Context &context)
{
if (UIElement::handleInput(context)) {
return true;
}
// 先更新子节点,再更新自己(状态)
if (state_ && interactive_) {
if (auto next_state = state_->handleInput(context); next_state) {
setState(std::move(next_state));
return true;
}
}
return false;
}
void UIInteractive::render(engine::core::Context &context)
{
if (!visible_ ) return;
// 先渲染自身
context.getRenderer().drawUISprite(*current_sprite_, getScreenPosition(), size_);
// 再渲染子元素(调用基类方法)
UIElement::render(context);
}
} // namespace engine::ui
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include "ui_element.h"
#include "state/ui_state.h"
#include "../render/sprite.h" // 需要引入头文件而不是前置声明(map容器创建时可能会检查内部元素是否有析构定义)
#include <memory>
#include <string>
#include <unordered_map>
namespace engine::core {
class Context;
}
namespace engine::ui {
/**
* @brief 可交互UI元素的基类,继承自UIElement
*
* 定义了可交互UI元素的通用属性和行为。
* 管理UI状态的切换和交互逻辑。
* 提供事件处理、更新和渲染的虚方法。
*/
class UIInteractive : public UIElement {
protected:
engine::core::Context& context_; ///< @brief 可交互元素很可能需要其他引擎组件
std::unique_ptr<engine::ui::state::UIState> state_; ///< @brief 当前状态
std::unordered_map<std::string, std::unique_ptr<engine::render::Sprite>> sprites_; ///< @brief 精灵集合
std::unordered_map<std::string, std::string> sounds_; ///< @brief 音效集合,key为音效名称,value为音效文件路径
engine::render::Sprite* current_sprite_ = nullptr; ///< @brief 当前显示的精灵
bool interactive_ = true; ///< @brief 是否可交互
public:
UIInteractive(engine::core::Context& context, const glm::vec2& position = {0.0f, 0.0f}, const glm::vec2& size = {0.0f, 0.0f});
~UIInteractive() override;
virtual void clicked() {} ///< @brief 如果有点击事件,则重写该方法
void addSprite(const std::string& name, std::unique_ptr<engine::render::Sprite> sprite);///< @brief 添加精灵
void setSprite(const std::string& name); ///< @brief 设置当前显示的精灵
void addSound(const std::string& name, const std::string& path); ///< @brief 添加音效
void playSound(const std::string& name); ///< @brief 播放音效
// --- Getters and Setters ---
void setState(std::unique_ptr<engine::ui::state::UIState> state); ///< @brief 设置当前状态
engine::ui::state::UIState* getState() const { return state_.get(); } ///< @brief 获取当前状态
void setInteractive(bool interactive) { interactive_ = interactive; } ///< @brief 设置是否可交互
bool isInteractive() const { return interactive_; } ///< @brief 获取是否可交互
// --- 核心方法 ---
bool handleInput(engine::core::Context& context) override;
void render(engine::core::Context& context) override;
};
} // namespace engine::ui
+19
View File
@@ -21,6 +21,7 @@
#include "../../engine/ui/ui_panel.h"
#include "../../engine/ui/ui_label.h"
#include "../../engine/ui/ui_image.h"
#include "../../engine/ui/ui_button.h"
#include "../../engine/utils/math.h"
#include "../component/ai_component.h"
#include "../component/ai/patrol_behavior.h"
@@ -202,6 +203,7 @@ bool GameScene::initUI()
createScoreUI();
createHealthUI();
createTestButton();
return true;
}
@@ -447,4 +449,21 @@ void GameScene::healWithUI(int amount)
updateHealthWithUI(); // 更新生命值与UI
}
void GameScene::createTestButton()
{
auto test_button = std::make_unique<engine::ui::UIButton>(context_,
"assets/textures/UI/buttons/Start1.png",
"assets/textures/UI/buttons/Start2.png",
"assets/textures/UI/buttons/Start3.png",
glm::vec2(100.0f, 100.0f),
glm::vec2(0.0f), // 采用图片大小
[this](){ this->testButtonClicked(); });
ui_manager_->addElement(std::move(test_button));
}
void GameScene::testButtonClicked()
{
spdlog::info("测试按钮被点击");
}
} // namespace game::scene
+4
View File
@@ -72,6 +72,10 @@ private:
void healWithUI(int amount); ///< @brief 增加生命,同时更新UI
void updateHealthWithUI(); ///< @brief 更新生命值UI (只适用最大生命值不变的情况)
// --- 测试函数 ---
void createTestButton(); ///< @brief 创建测试按钮
void testButtonClicked(); ///< @brief 测试按钮点击事件
};
} // namespace game::scene