diff --git a/CMakeLists.txt b/CMakeLists.txt index 99be4e1..08870c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/engine/core/game_app.cpp b/src/engine/core/game_app.cpp index bdc9c60..e0acaa4 100644 --- a/src/engine/core/game_app.cpp +++ b/src/engine/core/game_app.cpp @@ -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); diff --git a/src/engine/render/renderer.cpp b/src/engine/render/renderer.cpp index c09cb21..2cb005a 100644 --- a/src/engine/render/renderer.cpp +++ b/src/engine/render/renderer.cpp @@ -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_); diff --git a/src/engine/render/renderer.h b/src/engine/render/renderer.h index ce434c9..9c536a2 100644 --- a/src/engine/render/renderer.h +++ b/src/engine/render/renderer.h @@ -1,11 +1,12 @@ #pragma once #include "sprite.h" +#include "../utils/math.h" #include #include // For std::optional -#include 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& 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 函数 diff --git a/src/engine/scene/scene.cpp b/src/engine/scene/scene.cpp index 26ecf17..13e269e 100644 --- a/src/engine/scene/scene.cpp +++ b/src/engine/scene/scene.cpp @@ -4,13 +4,18 @@ #include "../core/context.h" #include "../physics/physics_engine.h" #include "../render/camera.h" +#include "../ui/ui_manager.h" #include // for std::remove_if #include 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()), + 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()) { diff --git a/src/engine/scene/scene.h b/src/engine/scene/scene.h index 98836f5..e3edf90 100644 --- a/src/engine/scene/scene.h +++ b/src/engine/scene/scene.h @@ -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 ui_manager_; ///< @brief UI管理器(初始化时自动创建) + bool is_initialized_ = false; ///< @brief 场景是否已初始化(非当前场景很可能未被删除,因此需要初始化标志避免重复初始化) std::vector> game_objects_; ///< @brief 场景中的游戏对象 std::vector> pending_additions_; ///< @brief 待添加的游戏对象(延时添加) diff --git a/src/engine/ui/ui_element.cpp b/src/engine/ui/ui_element.cpp new file mode 100644 index 0000000..73a5f2d --- /dev/null +++ b/src/engine/ui/ui_element.cpp @@ -0,0 +1,101 @@ +#include "ui_element.h" +#include "../core/context.h" +#include +#include +#include + +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 child) { + if (child) { + child->setParent(this); // 设置父指针 + children_.push_back(std::move(child)); + } +} + +std::unique_ptr UIElement::removeChild(UIElement* child_ptr) { + // 使用 std::remove_if 和 lambda 表达式自定义比较的方式移除 + auto it = std::find_if(children_.begin(), children_.end(), + [child_ptr](const std::unique_ptr& p) { + return p.get() == child_ptr; + }); + + if (it != children_.end()) { + std::unique_ptr 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 \ No newline at end of file diff --git a/src/engine/ui/ui_element.h b/src/engine/ui/ui_element.h new file mode 100644 index 0000000..4057170 --- /dev/null +++ b/src/engine/ui/ui_element.h @@ -0,0 +1,79 @@ +#pragma once +#include +#include +#include +#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> 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 child); ///< @brief 添加子元素 + std::unique_ptr 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>& 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 \ No newline at end of file diff --git a/src/engine/ui/ui_manager.cpp b/src/engine/ui/ui_manager.cpp new file mode 100644 index 0000000..073844d --- /dev/null +++ b/src/engine/ui/ui_manager.cpp @@ -0,0 +1,63 @@ +#include "ui_manager.h" +#include "ui_panel.h" +#include "ui_element.h" +#include + +namespace engine::ui { + +UIManager::~UIManager() = default; + +UIManager::UIManager() { + // 创建一个无特定大小和位置的Panel,它的子元素将基于它定位。 + root_element_ = std::make_unique(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 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 \ No newline at end of file diff --git a/src/engine/ui/ui_manager.h b/src/engine/ui/ui_manager.h new file mode 100644 index 0000000..91312fc --- /dev/null +++ b/src/engine/ui/ui_manager.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include + +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 root_element_; ///< @brief 一个UIPanel作为根节点(UI元素) + +public: + UIManager(); ///< @brief 构造函数将创建默认的根节点。 + ~UIManager(); + + + [[nodiscard]] bool init(const glm::vec2& window_size); ///< @brief 初始化UI管理器,设置根元素的大小。 + void addElement(std::unique_ptr 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 \ No newline at end of file diff --git a/src/engine/ui/ui_panel.cpp b/src/engine/ui/ui_panel.cpp new file mode 100644 index 0000000..f5f38ea --- /dev/null +++ b/src/engine/ui/ui_panel.cpp @@ -0,0 +1,25 @@ +#include "ui_panel.h" +#include "../core/context.h" +#include "../render/renderer.h" +#include +#include + +namespace engine::ui { + +UIPanel::UIPanel(const glm::vec2& position, const glm::vec2& size, const std::optional& 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 \ No newline at end of file diff --git a/src/engine/ui/ui_panel.h b/src/engine/ui/ui_panel.h new file mode 100644 index 0000000..01314f6 --- /dev/null +++ b/src/engine/ui/ui_panel.h @@ -0,0 +1,35 @@ +#pragma once +#include "ui_element.h" +#include +#include "../utils/math.h" + +namespace engine::ui { + +/** + * @brief 用于分组其他UI元素的容器UI元素 + * + * Panel通常用于布局和组织。 + * 可以选择是否绘制背景色(纯色)。 + */ +class UIPanel final : public UIElement { + std::optional 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& background_color = std::nullopt); + + void setBackgroundColor(const std::optional& background_color) { background_color_ = background_color; } + const std::optional& getBackgroundColor() const { return background_color_; } + + void render(engine::core::Context& context) override; +}; + +} // namespace engine::ui \ No newline at end of file diff --git a/src/engine/utils/math.h b/src/engine/utils/math.h index ca42b7e..0a1efb6 100644 --- a/src/engine/utils/math.h +++ b/src/engine/utils/math.h @@ -9,4 +9,12 @@ struct Rect glm::vec2 size; }; +struct FColor +{ + float r; + float g; + float b; + float a; +}; + } // namespace engine::utils \ No newline at end of file diff --git a/src/game/scene/game_scene.cpp b/src/game/scene/game_scene.cpp index 6eb8242..9d8802c 100644 --- a/src/game/scene/game_scene.cpp +++ b/src/game/scene/game_scene.cpp @@ -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 -#include 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(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 \ No newline at end of file diff --git a/src/game/scene/game_scene.h b/src/game/scene/game_scene.h index ce2aae3..9cccc67 100644 --- a/src/game/scene/game_scene.h +++ b/src/game/scene/game_scene.h @@ -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 \ No newline at end of file