From 99d4a8bbccbabc131a9af7f49f39ca5f81adeafc Mon Sep 17 00:00:00 2001 From: Ziyu Date: Sat, 14 Jun 2025 16:46:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=8C=89=E9=92=AE=E5=8F=8A?= =?UTF-8?q?=E5=8F=AF=E4=BA=A4=E4=BA=92UI=E5=85=83=E7=B4=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 5 ++ src/engine/ui/state/ui_hover_state.cpp | 30 ++++++++ src/engine/ui/state/ui_hover_state.h | 22 ++++++ src/engine/ui/state/ui_normal_state.cpp | 28 ++++++++ src/engine/ui/state/ui_normal_state.h | 22 ++++++ src/engine/ui/state/ui_pressed_state.cpp | 34 +++++++++ src/engine/ui/state/ui_pressed_state.h | 22 ++++++ src/engine/ui/state/ui_state.h | 44 ++++++++++++ src/engine/ui/ui_button.cpp | 33 +++++++++ src/engine/ui/ui_button.h | 45 ++++++++++++ src/engine/ui/ui_interactive.cpp | 90 ++++++++++++++++++++++++ src/engine/ui/ui_interactive.h | 53 ++++++++++++++ src/game/scene/game_scene.cpp | 19 +++++ src/game/scene/game_scene.h | 4 ++ 14 files changed, 451 insertions(+) create mode 100644 src/engine/ui/state/ui_hover_state.cpp create mode 100644 src/engine/ui/state/ui_hover_state.h create mode 100644 src/engine/ui/state/ui_normal_state.cpp create mode 100644 src/engine/ui/state/ui_normal_state.h create mode 100644 src/engine/ui/state/ui_pressed_state.cpp create mode 100644 src/engine/ui/state/ui_pressed_state.h create mode 100644 src/engine/ui/state/ui_state.h create mode 100644 src/engine/ui/ui_button.cpp create mode 100644 src/engine/ui/ui_button.h create mode 100644 src/engine/ui/ui_interactive.cpp create mode 100644 src/engine/ui/ui_interactive.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 54bad7d..f6a4c2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/engine/ui/state/ui_hover_state.cpp b/src/engine/ui/state/ui_hover_state.cpp new file mode 100644 index 0000000..550416c --- /dev/null +++ b/src/engine/ui/state/ui_hover_state.cpp @@ -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 + +namespace engine::ui::state { + +void UIHoverState::enter() +{ + owner_->setSprite("hover"); + spdlog::debug("切换到悬停状态"); +} + +std::unique_ptr 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(owner_); + } + if (input_manager.isActionPressed("MouseLeftClick")) { // 如果鼠标按下,则返回按下状态 + return std::make_unique(owner_); + } + return nullptr; +} + +} // namespace engine::ui::state \ No newline at end of file diff --git a/src/engine/ui/state/ui_hover_state.h b/src/engine/ui/state/ui_hover_state.h new file mode 100644 index 0000000..035d2df --- /dev/null +++ b/src/engine/ui/state/ui_hover_state.h @@ -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 handleInput(engine::core::Context& context) override; +}; + +} // namespace engine::ui::state \ No newline at end of file diff --git a/src/engine/ui/state/ui_normal_state.cpp b/src/engine/ui/state/ui_normal_state.cpp new file mode 100644 index 0000000..5f7509a --- /dev/null +++ b/src/engine/ui/state/ui_normal_state.cpp @@ -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 + +namespace engine::ui::state { + +void UINormalState::enter() +{ + owner_->setSprite("normal"); + spdlog::debug("切换到正常状态"); +} + +std::unique_ptr 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(owner_); + } + return nullptr; +} + +} // namespace engine::ui::state \ No newline at end of file diff --git a/src/engine/ui/state/ui_normal_state.h b/src/engine/ui/state/ui_normal_state.h new file mode 100644 index 0000000..6366f9b --- /dev/null +++ b/src/engine/ui/state/ui_normal_state.h @@ -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 handleInput(engine::core::Context& context) override; +}; + +} // namespace engine::ui::state \ No newline at end of file diff --git a/src/engine/ui/state/ui_pressed_state.cpp b/src/engine/ui/state/ui_pressed_state.cpp new file mode 100644 index 0000000..d7473e3 --- /dev/null +++ b/src/engine/ui/state/ui_pressed_state.cpp @@ -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 + +namespace engine::ui::state { + +void UIPressedState::enter() +{ + owner_->setSprite("pressed"); + owner_->playSound("pressed"); + spdlog::debug("切换到按下状态"); +} + +std::unique_ptr 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(owner_); + } else { // 松开鼠标时,如果还在UI元素内,则触发点击事件 + owner_->clicked(); + return std::make_unique(owner_); + } + } + + return nullptr; +} + +} // namespace engine::ui::state \ No newline at end of file diff --git a/src/engine/ui/state/ui_pressed_state.h b/src/engine/ui/state/ui_pressed_state.h new file mode 100644 index 0000000..e0273ef --- /dev/null +++ b/src/engine/ui/state/ui_pressed_state.h @@ -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 handleInput(engine::core::Context& context) override; +}; + +} // namespace engine::ui::state \ No newline at end of file diff --git a/src/engine/ui/state/ui_state.h b/src/engine/ui/state/ui_state.h new file mode 100644 index 0000000..1c7e497 --- /dev/null +++ b/src/engine/ui/state/ui_state.h @@ -0,0 +1,44 @@ +#pragma once +#include + +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 handleInput(engine::core::Context& context) = 0; +}; + +} // namespace engine::ui::state \ No newline at end of file diff --git a/src/engine/ui/ui_button.cpp b/src/engine/ui/ui_button.cpp new file mode 100644 index 0000000..b9a0061 --- /dev/null +++ b/src/engine/ui/ui_button.cpp @@ -0,0 +1,33 @@ +#include "ui_button.h" +#include "state/ui_normal_state.h" +#include + +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 callback) + : UIInteractive(context, position, size), callback_(std::move(callback)) +{ + addSprite("normal", std::make_unique(normal_sprite_id)); + addSprite("hover", std::make_unique(hover_sprite_id)); + addSprite("pressed", std::make_unique(pressed_sprite_id)); + + // 设置默认状态为"normal" + setState(std::make_unique(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 \ No newline at end of file diff --git a/src/engine/ui/ui_button.h b/src/engine/ui/ui_button.h new file mode 100644 index 0000000..d5d9b32 --- /dev/null +++ b/src/engine/ui/ui_button.h @@ -0,0 +1,45 @@ +#pragma once +#include "ui_interactive.h" +#include +#include + +namespace engine::ui { + +/** + * @brief 按钮UI元素 + * + * 继承自UIInteractive,用于创建可交互的按钮。 + * 支持三种状态:正常、悬停、按下。 + * 支持回调函数,当按钮被点击时调用。 + */ +class UIButton final: public UIInteractive { +private: + std::function 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 callback = nullptr); + ~UIButton() override = default; + + void clicked() override; ///< @brief 重写基类方法,当按钮被点击时调用回调函数 + + void setCallback(std::function callback) { callback_ = std::move(callback); } ///< @brief 设置点击回调函数 + std::function getCallback() const { return callback_; } ///< @brief 获取点击回调函数 + +}; + +} // namespace engine::ui \ No newline at end of file diff --git a/src/engine/ui/ui_interactive.cpp b/src/engine/ui/ui_interactive.cpp new file mode 100644 index 0000000..813b46e --- /dev/null +++ b/src/engine/ui/ui_interactive.cpp @@ -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 + +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 state) +{ + if (!state) { + spdlog::warn("尝试设置空的状态!"); + return; + } + + state_ = std::move(state); + state_->enter(); +} + +void UIInteractive::addSprite(const std::string &name, std::unique_ptr 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 \ No newline at end of file diff --git a/src/engine/ui/ui_interactive.h b/src/engine/ui/ui_interactive.h new file mode 100644 index 0000000..cd15797 --- /dev/null +++ b/src/engine/ui/ui_interactive.h @@ -0,0 +1,53 @@ +#pragma once +#include "ui_element.h" +#include "state/ui_state.h" +#include "../render/sprite.h" // 需要引入头文件而不是前置声明(map容器创建时可能会检查内部元素是否有析构定义) +#include +#include +#include + +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 state_; ///< @brief 当前状态 + std::unordered_map> sprites_; ///< @brief 精灵集合 + std::unordered_map 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 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 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 \ No newline at end of file diff --git a/src/game/scene/game_scene.cpp b/src/game/scene/game_scene.cpp index 80f3c06..d4a3321 100644 --- a/src/game/scene/game_scene.cpp +++ b/src/game/scene/game_scene.cpp @@ -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(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 \ No newline at end of file diff --git a/src/game/scene/game_scene.h b/src/game/scene/game_scene.h index 4491527..52266ea 100644 --- a/src/game/scene/game_scene.h +++ b/src/game/scene/game_scene.h @@ -72,6 +72,10 @@ private: void healWithUI(int amount); ///< @brief 增加生命,同时更新UI void updateHealthWithUI(); ///< @brief 更新生命值UI (只适用最大生命值不变的情况) + // --- 测试函数 --- + void createTestButton(); ///< @brief 创建测试按钮 + void testButtonClicked(); ///< @brief 测试按钮点击事件 + }; } // namespace game::scene \ No newline at end of file