完成按钮及可交互UI元素
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -72,6 +72,10 @@ private:
|
||||
void healWithUI(int amount); ///< @brief 增加生命,同时更新UI
|
||||
void updateHealthWithUI(); ///< @brief 更新生命值UI (只适用最大生命值不变的情况)
|
||||
|
||||
// --- 测试函数 ---
|
||||
void createTestButton(); ///< @brief 创建测试按钮
|
||||
void testButtonClicked(); ///< @brief 测试按钮点击事件
|
||||
|
||||
};
|
||||
|
||||
} // namespace game::scene
|
||||
Reference in New Issue
Block a user