完成UI管理器与UI元素

This commit is contained in:
Ziyu
2025-06-16 14:44:47 +08:00
parent f16cac3097
commit 8b10d40822
15 changed files with 424 additions and 24 deletions
+3
View File
@@ -60,6 +60,9 @@ add_executable(${TARGET}
src/engine/scene/scene.cpp
src/engine/scene/scene_manager.cpp
src/engine/scene/level_loader.cpp
src/engine/ui/ui_manager.cpp
src/engine/ui/ui_element.cpp
src/engine/ui/ui_panel.cpp
src/game/scene/game_scene.cpp
src/game/component/player_component.cpp
src/game/component/ai_component.cpp
+3
View File
@@ -148,6 +148,9 @@ bool GameApp::initSDL()
return false;
}
// 设置渲染器支持透明色
SDL_SetRenderDrawBlendMode(sdl_renderer_, SDL_BLENDMODE_BLEND);
// 设置 VSync (注意: VSync 开启时,驱动程序会尝试将帧率限制到显示器刷新率,有可能会覆盖我们手动设置的 target_fps)
int vsync_mode = config_->vsync_enabled_ ? SDL_RENDERER_VSYNC_ADAPTIVE : SDL_RENDERER_VSYNC_DISABLED;
SDL_SetRenderVSync(sdl_renderer_, vsync_mode);
+10
View File
@@ -159,6 +159,16 @@ void Renderer::clearScreen() {
}
}
void Renderer::drawUIFilledRect(const engine::utils::Rect &rect, const engine::utils::FColor &color)
{
setDrawColorFloat(color.r, color.g, color.b, color.a);
SDL_FRect sdl_rect = {rect.position.x, rect.position.y, rect.size.x, rect.size.y};
if (!SDL_RenderFillRect(renderer_, &sdl_rect)) {
spdlog::error("绘制填充矩形失败:{}", SDL_GetError());
}
setDrawColor(0, 0, 0, 1.0f);
}
void Renderer::present()
{
SDL_RenderPresent(renderer_);
+9 -1
View File
@@ -1,11 +1,12 @@
#pragma once
#include "sprite.h"
#include "../utils/math.h"
#include <string>
#include <optional> // For std::optional
#include <glm/glm.hpp>
struct SDL_Renderer;
struct SDL_FRect;
struct SDL_FColor;
namespace engine::resource {
class ResourceManager;
@@ -67,6 +68,13 @@ public:
*/
void drawUISprite(const Sprite& sprite, const glm::vec2& position, const std::optional<glm::vec2>& size = std::nullopt);
/**
* @brief 绘制填充矩形
*
* @param rect 矩形区域
* @param color 填充颜色
*/
void drawUIFilledRect(const engine::utils::Rect& rect, const engine::utils::FColor& color);
void present(); ///< @brief 更新屏幕,包装 SDL_RenderPresent 函数
void clearScreen(); ///< @brief 清屏,包装 SDL_RenderClear 函数
+15 -1
View File
@@ -4,13 +4,18 @@
#include "../core/context.h"
#include "../physics/physics_engine.h"
#include "../render/camera.h"
#include "../ui/ui_manager.h"
#include <algorithm> // for std::remove_if
#include <spdlog/spdlog.h>
namespace engine::scene {
Scene::Scene(std::string name, engine::core::Context& context, engine::scene::SceneManager& scene_manager)
: scene_name_(std::move(name)), context_(context), scene_manager_(scene_manager), is_initialized_(false) {
: scene_name_(std::move(name)),
context_(context),
scene_manager_(scene_manager),
ui_manager_(std::make_unique<engine::ui::UIManager>()),
is_initialized_(false) {
spdlog::trace("场景 '{}' 构造完成。", scene_name_);
}
@@ -42,6 +47,9 @@ void Scene::update(float delta_time) {
}
}
// 更新UI管理器
ui_manager_->update(delta_time, context_);
processPendingAdditions(); // 处理待添加(延时添加)的游戏对象
}
@@ -51,11 +59,17 @@ void Scene::render() {
for (const auto& obj : game_objects_) {
if (obj) obj->render(context_);
}
// 渲染UI管理器
ui_manager_->render(context_);
}
void Scene::handleInput() {
if (!is_initialized_) return;
// 处理UI管理器输入
if (ui_manager_->handleInput(context_)) return; // 如果输入事件被UI处理则返回,不再处理游戏对象输入
// 遍历所有游戏对象,并删除需要移除的对象
for (auto it = game_objects_.begin(); it != game_objects_.end();) {
if (*it && !(*it)->isNeedRemove()) {
+4 -7
View File
@@ -7,13 +7,8 @@ namespace engine::core {
class Context;
}
namespace engine::render {
class Renderer;
class Camera;
}
namespace engine::input {
class InputManager;
namespace engine::ui {
class UIManager;
}
namespace engine::object {
@@ -34,6 +29,8 @@ protected:
std::string scene_name_; ///< @brief 场景名称
engine::core::Context& context_; ///< @brief 上下文引用(隐式,构造时传入)
engine::scene::SceneManager& scene_manager_; ///< @brief 场景管理器引用(构造时传入)
std::unique_ptr<engine::ui::UIManager> ui_manager_; ///< @brief UI管理器(初始化时自动创建)
bool is_initialized_ = false; ///< @brief 场景是否已初始化(非当前场景很可能未被删除,因此需要初始化标志避免重复初始化)
std::vector<std::unique_ptr<engine::object::GameObject>> game_objects_; ///< @brief 场景中的游戏对象
std::vector<std::unique_ptr<engine::object::GameObject>> pending_additions_; ///< @brief 待添加的游戏对象(延时添加)
+101
View File
@@ -0,0 +1,101 @@
#include "ui_element.h"
#include "../core/context.h"
#include <algorithm>
#include <utility>
#include <spdlog/spdlog.h>
namespace engine::ui {
UIElement::UIElement(const glm::vec2& position, const glm::vec2& size)
: position_(position), size_(size) {
}
bool UIElement::handleInput(engine::core::Context& context) {
// 如果元素不可见,直接返回 false
if (!visible_) return false;
// 遍历所有子节点,并删除标记了移除的元素
for (auto it = children_.begin(); it != children_.end();) {
if (*it && !(*it)->isNeedRemove()) {
if ((*it)->handleInput(context)) return true;
++it;
} else {
it = children_.erase(it);
}
}
// 事件未被消耗,返回假
return false;
}
void UIElement::update(float delta_time, engine::core::Context& context) {
if (!visible_) return;
// 遍历所有子节点,并删除标记了移除的元素
for (auto it = children_.begin(); it != children_.end();) {
if (*it && !(*it)->isNeedRemove()) {
(*it)->update(delta_time, context);
++it;
} else {
it = children_.erase(it);
}
}
}
void UIElement::render(engine::core::Context& context) {
if (!visible_) return;
// 渲染子元素
for (const auto& child : children_) {
if (child) child->render(context);
}
}
void UIElement::addChild(std::unique_ptr<UIElement> child) {
if (child) {
child->setParent(this); // 设置父指针
children_.push_back(std::move(child));
}
}
std::unique_ptr<UIElement> UIElement::removeChild(UIElement* child_ptr) {
// 使用 std::remove_if 和 lambda 表达式自定义比较的方式移除
auto it = std::find_if(children_.begin(), children_.end(),
[child_ptr](const std::unique_ptr<UIElement>& p) {
return p.get() == child_ptr;
});
if (it != children_.end()) {
std::unique_ptr<UIElement> removed_child = std::move(*it);
children_.erase(it);
removed_child->setParent(nullptr); // 清除父指针
return removed_child; // 返回被移除的子元素(可以挂载到别处)
}
return nullptr; // 未找到子元素
}
void UIElement::removeAllChildren() {
for (auto& child : children_) {
child->setParent(nullptr); // 清除父指针
}
children_.clear();
}
glm::vec2 UIElement::getScreenPosition() const {
if (parent_) {
return parent_->getScreenPosition() + position_;
}
return position_; // 根元素的位置已经是相对屏幕的绝对位置
}
engine::utils::Rect UIElement::getBounds() const {
auto abs_pos = getScreenPosition();
return engine::utils::Rect(abs_pos, size_);
}
bool UIElement::isPointInside(const glm::vec2& point) const {
auto bounds = getBounds();
return (point.x >= bounds.position.x && point.x < (bounds.position.x + bounds.size.x) &&
point.y >= bounds.position.y && point.y < (bounds.position.y + bounds.size.y));
}
} // namespace engine::ui
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#include <SDL3/SDL_rect.h>
#include <memory>
#include <vector>
#include "../utils/math.h"
namespace engine::core {
class Context;
}
namespace engine::ui {
/**
* @brief 所有UI元素的基类
*
* 定义了位置、大小、可见性、状态等通用属性。
* 管理子元素的层次结构。
* 提供事件处理、更新和渲染的虚方法。
*/
class UIElement {
protected:
glm::vec2 position_; ///< @brief 相对于父元素的局部位置
glm::vec2 size_; ///< @brief 元素大小
bool visible_ = true; ///< @brief 元素当前是否可见
bool need_remove_ = false; ///< @brief 是否需要移除(延迟删除)
UIElement* parent_ = nullptr; ///< @brief 指向父节点的非拥有指针
std::vector<std::unique_ptr<UIElement>> children_; ///< @brief 子元素列表(容器)
public:
/**
* @brief 构造UIElement
* @param position 初始局部位置
* @param size 初始大小
*/
explicit UIElement(const glm::vec2& position = {0.0f, 0.0f}, const glm::vec2& size = {0.0f, 0.0f});
/**
* @brief 虚析构函数,确保派生类正确清理
*/
virtual ~UIElement() = default;
// --- 核心虚循环方法 --- (没有使用init和clean,注意构造函数和析构函数的使用)
virtual bool handleInput(engine::core::Context& context);
virtual void update(float delta_time, engine::core::Context& context);
virtual void render(engine::core::Context& context);
// --- 层次结构管理 ---
void addChild(std::unique_ptr<UIElement> child); ///< @brief 添加子元素
std::unique_ptr<UIElement> removeChild(UIElement* child_ptr); ///< @brief 将指定子元素从列表中移除,并返回其智能指针
void removeAllChildren(); ///< @brief 移除所有子元素
// --- Getters and Setters ---
const glm::vec2& getSize() const { return size_; } ///< @brief 获取元素大小
const glm::vec2& getPosition() const { return position_; } ///< @brief 获取元素位置(相对于父节点)
bool isVisible() const { return visible_; } ///< @brief 检查元素是否可见
bool isNeedRemove() const { return need_remove_; } ///< @brief 检查元素是否需要移除
UIElement* getParent() const { return parent_; } ///< @brief 获取父元素
const std::vector<std::unique_ptr<UIElement>>& getChildren() const { return children_; } ///< @brief 获取子元素列表
void setSize(const glm::vec2& size) { size_ = size; } ///< @brief 设置元素大小
void setVisible(bool visible) { visible_ = visible; } ///< @brief 设置元素的可见性
void setParent(UIElement* parent) { parent_ = parent; } ///< @brief 设置父节点
void setPosition(const glm::vec2& position) { position_ = position; } ///< @brief 设置元素位置(相对于父节点)
void setNeedRemove(bool need_remove) { need_remove_ = need_remove; } ///< @brief 设置元素是否需要移除
// --- 辅助方法 ---
engine::utils::Rect getBounds() const; ///< @brief 获取(计算)元素的边界(屏幕坐标)
glm::vec2 getScreenPosition() const; ///< @brief 获取(计算)元素在屏幕上位置
bool isPointInside(const glm::vec2& point) const; ///< @brief 检查给定点是否在元素的边界内
// --- 禁用拷贝和移动语义 ---
UIElement(const UIElement&) = delete;
UIElement& operator=(const UIElement&) = delete;
UIElement(UIElement&&) = delete;
UIElement& operator=(UIElement&&) = delete;
};
} // namespace engine::ui
+63
View File
@@ -0,0 +1,63 @@
#include "ui_manager.h"
#include "ui_panel.h"
#include "ui_element.h"
#include <spdlog/spdlog.h>
namespace engine::ui {
UIManager::~UIManager() = default;
UIManager::UIManager() {
// 创建一个无特定大小和位置的Panel,它的子元素将基于它定位。
root_element_ = std::make_unique<UIPanel>(glm::vec2{0.0f, 0.0f}, glm::vec2{0.0f, 0.0f});
spdlog::trace("UI管理器构造完成。");
}
bool UIManager::init(const glm::vec2& window_size) {
root_element_->setSize(window_size);
spdlog::trace("UI管理器已初始化根面板。");
return true;
}
void UIManager::addElement(std::unique_ptr<UIElement> element) {
if (root_element_) {
root_element_->addChild(std::move(element));
} else {
spdlog::error("无法添加元素:root_element_ 为空!");
}
}
void UIManager::clearElements() {
if (root_element_) {
root_element_->removeAllChildren();
spdlog::trace("所有UI元素已从UI管理器中清除。");
}
}
bool UIManager::handleInput(engine::core::Context& context) {
if (root_element_ && root_element_->isVisible()) {
// 从根元素开始向下分发事件
if (root_element_->handleInput(context)) return true;
}
return false;
}
void UIManager::update(float delta_time, engine::core::Context& context) {
if (root_element_ && root_element_->isVisible()) {
// 从根元素开始向下更新
root_element_->update(delta_time, context);
}
}
void UIManager::render(engine::core::Context& context) {
if (root_element_ && root_element_->isVisible()) {
// 从根元素开始向下渲染
root_element_->render(context);
}
}
UIPanel* UIManager::getRootElement() const {
return root_element_.get();
}
} // namespace engine::ui
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <memory>
#include <vector>
#include <glm/vec2.hpp>
namespace engine::core {
class Context;
}
namespace engine::ui {
class UIElement;
class UIPanel; // UIPanel 将作为根元素
}
namespace engine::ui {
/**
* @brief 管理特定场景中的UI元素集合。
*
* 负责UI元素的生命周期管理(通过根元素)、渲染调用和输入事件分发。
* 每个需要UI的场景(如菜单、游戏HUD)应该拥有一个UIManager实例。
*/
class UIManager final {
private:
std::unique_ptr<UIPanel> root_element_; ///< @brief 一个UIPanel作为根节点(UI元素)
public:
UIManager(); ///< @brief 构造函数将创建默认的根节点。
~UIManager();
[[nodiscard]] bool init(const glm::vec2& window_size); ///< @brief 初始化UI管理器,设置根元素的大小。
void addElement(std::unique_ptr<UIElement> element); ///< @brief 添加一个UI元素到根节点的child_容器中。
UIPanel* getRootElement() const; ///< @brief 获取根UIPanel元素的指针。
void clearElements(); ///< @brief 清除所有UI元素,通常用于重置UI状态。
// --- 核心循环方法 ---
bool handleInput(engine::core::Context&); ///< @brief 处理输入事件,如果事件被处理则返回true。
void update(float delta_time, engine::core::Context&);
void render(engine::core::Context&);
// 禁止拷贝和移动构造/赋值
UIManager(const UIManager&) = delete;
UIManager& operator=(const UIManager&) = delete;
UIManager(UIManager&&) = delete;
UIManager& operator=(UIManager&&) = delete;
};
} // namespace engine::ui
+25
View File
@@ -0,0 +1,25 @@
#include "ui_panel.h"
#include "../core/context.h"
#include "../render/renderer.h"
#include <SDL3/SDL_pixels.h>
#include <spdlog/spdlog.h>
namespace engine::ui {
UIPanel::UIPanel(const glm::vec2& position, const glm::vec2& size, const std::optional<engine::utils::FColor>& background_color)
: UIElement(position, size), background_color_(background_color)
{
spdlog::trace("UIPanel 构造完成。");
}
void UIPanel::render(engine::core::Context& context) {
if (!visible_) return;
if (background_color_) {
context.getRenderer().drawUIFilledRect(getBounds(), background_color_.value());
}
UIElement::render(context); // 调用基类渲染方法(绘制子节点)
}
} // namespace engine::ui
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "ui_element.h"
#include <optional>
#include "../utils/math.h"
namespace engine::ui {
/**
* @brief 用于分组其他UI元素的容器UI元素
*
* Panel通常用于布局和组织。
* 可以选择是否绘制背景色(纯色)。
*/
class UIPanel final : public UIElement {
std::optional<engine::utils::FColor> background_color_; ///< @brief 可选背景色
public:
/**
* @brief 构造一个Panel
*
* @param position Panel的局部位置
* @param size Panel的大小
* @param background_color 背景色
*/
explicit UIPanel(const glm::vec2& position = {0.0f, 0.0f},
const glm::vec2& size = {0.0f, 0.0f},
const std::optional<engine::utils::FColor>& background_color = std::nullopt);
void setBackgroundColor(const std::optional<engine::utils::FColor>& background_color) { background_color_ = background_color; }
const std::optional<engine::utils::FColor>& getBackgroundColor() const { return background_color_; }
void render(engine::core::Context& context) override;
};
} // namespace engine::ui
+8
View File
@@ -9,4 +9,12 @@ struct Rect
glm::vec2 size;
};
struct FColor
{
float r;
float g;
float b;
float a;
};
} // namespace engine::utils
+19 -12
View File
@@ -17,13 +17,15 @@
#include "../../engine/render/animation.h"
#include "../../engine/render/text_renderer.h"
#include "../../engine/audio/audio_player.h"
#include "../../engine/ui/ui_manager.h"
#include "../../engine/ui/ui_panel.h"
#include "../../engine/utils/math.h"
#include "../component/ai_component.h"
#include "../component/ai/patrol_behavior.h"
#include "../component/ai/updown_behavior.h"
#include "../component/ai/jump_behavior.h"
#include "../data/session_data.h"
#include <spdlog/spdlog.h>
#include <SDL3/SDL_rect.h>
namespace game::scene {
@@ -60,7 +62,11 @@ void GameScene::init() {
context_.getInputManager().setShouldQuit(true);
return;
}
if (!initUI()) {
spdlog::error("UI初始化失败,无法继续。");
context_.getInputManager().setShouldQuit(true);
return;
}
// 设置音量
context_.getAudioPlayer().setMusicVolume(0.2f); // 设置背景音乐音量为20%
context_.getAudioPlayer().setSoundVolume(0.5f); // 设置音效音量为50%
@@ -79,7 +85,6 @@ void GameScene::update(float delta_time) {
void GameScene::render() {
Scene::render();
testTextRenderer();
}
void GameScene::handleInput() {
@@ -189,6 +194,17 @@ bool GameScene::initEnemyAndItem()
return success;
}
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}));
return true;
}
void GameScene::handleObjectCollisions()
{
// 从物理引擎中获取碰撞对
@@ -352,13 +368,4 @@ void GameScene::createEffect(const glm::vec2& center_pos, const std::string &tag
spdlog::debug("创建特效: {}", tag);
}
void GameScene::testTextRenderer()
{
auto& text_renderer = context_.getTextRenderer();
const auto& camera = context_.getCamera();
// UI和地图各渲染一次,测试是否正常
text_renderer.drawUIText("UI Text", "assets/fonts/VonwaonBitmap-16px.ttf", 32, glm::vec2(100.0f), {0, 1.0f, 0, 1.0f});
text_renderer.drawText(camera, "Map Text", "assets/fonts/VonwaonBitmap-16px.ttf", 32, glm::vec2(200.0f));
}
} // namespace game::scene
+1 -3
View File
@@ -37,6 +37,7 @@ private:
[[nodiscard]] bool initLevel(); ///< @brief 初始化关卡
[[nodiscard]] bool initPlayer(); ///< @brief 初始化玩家
[[nodiscard]] bool initEnemyAndItem(); ///< @brief 初始化敌人和道具
[[nodiscard]] bool initUI(); ///< @brief 初始化UI
void handleObjectCollisions(); ///< @brief 处理游戏对象间的碰撞逻辑(从PhysicsEngine获取信息)
void handleTileTriggers(); ///< @brief 处理瓦片触发事件(从PhysicsEngine获取信息)
@@ -55,9 +56,6 @@ private:
* @param tag 特效标签(决定特效类型,例如"enemy","item")
*/
void createEffect(const glm::vec2& center_pos, const std::string& tag);
// 测试函数
void testTextRenderer();
};
} // namespace game::scene