diff --git a/CMakeLists.txt b/CMakeLists.txt index 7104233..1cbce94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ add_executable(${TARGET} src/game/scene/title_scene.cpp src/game/scene/helps_scene.cpp src/game/scene/menu_scene.cpp + src/game/scene/end_scene.cpp src/game/component/player_component.cpp src/game/component/ai_component.cpp src/game/component/state/player_state.cpp diff --git a/Tiled地图解析思路.md b/Tiled地图解析思路.md index 0c4159e..7321790 100644 --- a/Tiled地图解析思路.md +++ b/Tiled地图解析思路.md @@ -61,6 +61,9 @@ } ``` - 图块碰撞编辑器:识别为碰撞盒,限一个图形,只支持矩形和圆形 + - 自定义形状:默认为触发器,两种主要功能 + - 1. 进入下一关:tag : next_level,名称(Tiled自带字段)为下一关地图名字,例如 level2 + - 2. 游戏胜利:tag: win - 两层皆可 - solid (bool) : 静止不可进入的图块/物体 diff --git a/assets/maps/level1.tmj b/assets/maps/level1.tmj index 3b1a311..e42b6dd 100644 --- a/assets/maps/level1.tmj +++ b/assets/maps/level1.tmj @@ -93,8 +93,8 @@ 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 77, 193, 193, 193, 193, 193, 193, 193, 295, 335, 291, 193, 193, 193, 193, 193, 193, 193, 295, 258, 291, 193, 193, 502, 502, 193, 335, 193, 333, 193, 502, 529, 530, 502, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 502, 181, 183, 79, 181, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 183, 183, 79, 79, 79, 79, 79, 79, 79, 183, 79, 79, 79, 79, 81, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 360, 0, 0, 0, 0, 0, 0, 0, 0, 0, 258, 0, 0, 0, 0, 0, 0, 360, 0, 358, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 183, 181, 79, 79, 79, 79, 79, 79, 79, 79, 183, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 79, 183, 79, 79, 79, 79, 183, 79, 79, 79, 183, 79, 81, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 385, 0, 0, 0, 0, 0, 0, 0, 0, 0, 258, 0, 0, 0, 0, 0, 0, 385, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 183, 79, 79, - 79, 79, 183, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 591, 0, 0, 0, 0, 0, 0, 0, 258, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 599, 591, 0, 599, 599, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 79, 79, 181, 79, 79, 79, 79, 79, 79, 183, 79, 181, 181, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 183, 79, 79, - 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 258, 0, 0, 0, 0, 591, 0, 0, 586, 1, 0, 0, 599, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 79, 183, 79, + 79, 79, 183, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 591, 1, 0, 0, 0, 0, 0, 0, 258, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 599, 591, 1, 599, 599, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 79, 79, 181, 79, 79, 79, 79, 79, 79, 183, 79, 181, 181, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 183, 79, 79, + 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 258, 0, 0, 0, 0, 591, 1, 0, 586, 1, 0, 0, 599, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 241, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 79, 183, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 77, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 258, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 241, 181, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 181, 79, 81, 0, 0, 0, 77, 0, 576, 1, 0, 0, 583, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 504, 505, 0, 0, 0, 578, 1, 0, 579, 0, 0, 0, 241, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 191, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 502, 193, 193, 504, 505, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 529, 530, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 502, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 81, 0, 0, 0, 0, 0, 0, 77, 79, 79, 79, 79, 183, diff --git a/assets/maps/level2.tmj b/assets/maps/level2.tmj index d4ea632..60e0d33 100644 --- a/assets/maps/level2.tmj +++ b/assets/maps/level2.tmj @@ -55,9 +55,9 @@ 79, 79, 79, 502, 0, 0, 0, 0, 0, 0, 403, 472, 472, 412, 472, 472, 418, 419, 420, 412, 514, 515, 516, 472, 472, 472, 437, 472, 472, 472, 472, 472, 472, 440, 441, 472, 472, 472, 472, 405, 0, 0, 0, 0, 0, 77, 181, 79, 79, 183, 183, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 502, 0, 0, 0, 0, 0, 191, 193, 193, 193, 502, 193, 193, 193, 195, 412, 539, 540, 541, 472, 472, 461, 462, 463, 418, 419, 420, 504, 505, 465, 466, 472, 472, 472, 472, 430, 431, 179, 0, 35, 36, 37, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 181, 79, 183, 79, 79, 504, 505, 502, 179, 179, 179, 179, 241, 79, 502, 79, 79, 79, 79, 502, 502, 193, 193, 193, 193, 193, 193, 193, 193, 502, 193, 502, 193, 529, 530, 193, 193, 193, 502, 193, 193, 193, 193, 502, 502, 0, 179, 77, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, - 79, 79, 79, 529, 530, 502, 29, 29, 29, 29, 502, 79, 79, 79, 79, 79, 79, 79, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 183, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 181, 504, 505, 502, 502, 502, 502, 79, 79, 79, 79, 181, 79, 79, 79, 79, 79, 79, - 79, 79, 183, 79, 79, 79, 79, 502, 79, 79, 79, 79, 79, 79, 181, 79, 79, 79, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 181, 2684354639, 529, 530, 504, 505, 502, 181, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, - 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 2684354639, 181, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 181, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 2684354639, 183, 2684354639, 2684354639, 2684354639, 2684354639, 504, 505, 529, 530, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79], + 79, 79, 79, 529, 530, 502, 29, 29, 29, 29, 502, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 183, 79, 79, 79, 79, 79, 79, 79, 181, 504, 505, 502, 502, 502, 502, 79, 79, 79, 79, 181, 79, 79, 79, 79, 79, 79, + 79, 79, 183, 79, 79, 79, 79, 502, 79, 79, 79, 79, 79, 79, 181, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 181, 79, 529, 530, 504, 505, 502, 181, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, + 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 181, 79, 79, 79, 79, 79, 79, 181, 79, 79, 79, 79, 79, 79, 79, 79, 183, 79, 79, 79, 79, 504, 505, 529, 530, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79], "height":25, "id":1, "name":"main", @@ -86,6 +86,101 @@ "width":32, "x":32, "y":128 + }, + { + "height":73, + "id":2, + "name":"win", + "rotation":0, + "type":"", + "visible":true, + "width":14, + "x":881, + "y":71 + }, + { + "gid":576, + "height":41, + "id":3, + "name":"eagle", + "rotation":0, + "type":"", + "visible":true, + "width":40, + "x":519, + "y":140.5 + }, + { + "gid":578, + "height":32, + "id":4, + "name":"frog", + "rotation":0, + "type":"", + "visible":true, + "width":35, + "x":810.5, + "y":164.5 + }, + { + "gid":579, + "height":28, + "id":5, + "name":"opossum", + "rotation":0, + "type":"", + "visible":true, + "width":36, + "x":462, + "y":335 + }, + { + "gid":581, + "height":13, + "id":6, + "name":"gem", + "rotation":0, + "type":"", + "visible":true, + "width":15, + "x":656, + "y":112 + }, + { + "gid":581, + "height":13, + "id":7, + "name":"gem", + "rotation":0, + "type":"", + "visible":true, + "width":15, + "x":688, + "y":112 + }, + { + "gid":581, + "height":13, + "id":8, + "name":"gem", + "rotation":0, + "type":"", + "visible":true, + "width":15, + "x":720, + "y":112 + }, + { + "gid":580, + "height":21, + "id":9, + "name":"fruit", + "rotation":0, + "type":"", + "visible":true, + "width":21, + "x":127, + "y":320 }], "opacity":1, "type":"objectgroup", @@ -94,7 +189,7 @@ "y":0 }], "nextlayerid":5, - "nextobjectid":2, + "nextobjectid":10, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.11.2", diff --git a/assets/save.json b/assets/save.json index 8c12115..730e76f 100644 --- a/assets/save.json +++ b/assets/save.json @@ -1,5 +1,5 @@ { - "high_score": 20, + "high_score": 40, "level_health": 1, "level_score": 15, "map_path": "assets/maps/level2.tmj", diff --git a/src/engine/render/camera.cpp b/src/engine/render/camera.cpp index 9cf8bf5..4c37eea 100644 --- a/src/engine/render/camera.cpp +++ b/src/engine/render/camera.cpp @@ -44,7 +44,7 @@ void Camera::move(const glm::vec2 &offset) clampPosition(); } -void Camera::setLimitBounds(const engine::utils::Rect& bounds) +void Camera::setLimitBounds(std::optional bounds) { limit_bounds_ = bounds; clampPosition(); // 设置边界后,立即应用限制 diff --git a/src/engine/render/camera.h b/src/engine/render/camera.h index 308a49b..9d28a1f 100644 --- a/src/engine/render/camera.h +++ b/src/engine/render/camera.h @@ -32,7 +32,7 @@ public: glm::vec2 screenToWorld(const glm::vec2& screen_pos) const; ///< @brief 屏幕坐标转世界坐标 void setPosition(const glm::vec2& position); ///< @brief 设置相机位置 - void setLimitBounds(const engine::utils::Rect& bounds); ///< @brief 设置限制相机的移动范围 + void setLimitBounds(std::optional bounds); ///< @brief 设置限制相机的移动范围 void setTarget(engine::component::TransformComponent* target); ///< @brief 设置跟随目标变换组件 const glm::vec2& getPosition() const; ///< @brief 获取相机位置 diff --git a/src/game/data/session_data.cpp b/src/game/data/session_data.cpp index 67d9846..836a7f3 100644 --- a/src/game/data/session_data.cpp +++ b/src/game/data/session_data.cpp @@ -31,6 +31,7 @@ void SessionData::reset() { current_score_ = 0; level_health_ = 3; level_score_ = 0; + is_win_ = false; map_path_ = "assets/maps/level1.tmj"; spdlog::info("SessionData reset."); } @@ -89,7 +90,7 @@ bool SessionData::loadFromFile(const std::string& filename) { current_score_ = level_score_ = j.value("level_score", 0); current_health_ = level_health_ = j.value("level_health", 3); max_health_ = j.value("max_health", 3); // 使用合理的默认值 - high_score_ = j.value("high_score", 0); + high_score_ = glm::max(j.value("high_score", 0), high_score_); // 文件中的最高分,与当前最高分取最大值 map_path_ = j.value("map_path", "assets/maps/level1.tmj"); // 默认起始地图 spdlog::info("游戏数据成功加载: {}", filename); @@ -101,4 +102,39 @@ bool SessionData::loadFromFile(const std::string& filename) { } } +bool SessionData::syncHighScore(const std::string& filename) +{ + try { + // 打开文件进行读取 + std::fstream fs(filename); + if (!fs.is_open()) { + spdlog::warn("找不到文件: {}, 无法进行同步", filename); + return false; + } + + // 从文件解析 JSON 数据 + nlohmann::json j; + fs >> j; + auto high_score_in_file = j.value("high_score", 0); + + // 根据文件中的最高分和当前最高分来决定处理方式 + if (high_score_in_file < high_score_) { // 文件中的最高分 低于 当前最高分 + j["high_score"] = high_score_; + fs.seekp(0); // 文件指针回到文件开头 + fs << j.dump(4); // 将JSON对象写入文件 + spdlog::debug("最高分高于存档文件,已将最高分保存到存档中"); + } else if (high_score_in_file > high_score_) { // 文件中的最高分 高于 当前最高分 + high_score_ = high_score_in_file; + spdlog::debug("存档文件中的最高分高于当前最高分,已更新当前最高分"); + } else { + spdlog::debug("存档文件中的最高分与当前最高分相同,无需更新"); + } + fs.close(); + return true; + } catch (const std::exception& e) { + spdlog::error("同步最高分时出现错误 {}: {}", filename, e.what()); + return false; + } +} + } // namespace game::state \ No newline at end of file diff --git a/src/game/data/session_data.h b/src/game/data/session_data.h index 4f1e0ca..a922314 100644 --- a/src/game/data/session_data.h +++ b/src/game/data/session_data.h @@ -17,6 +17,7 @@ private: int max_health_ = 3; int current_score_ = 0; int high_score_ = 0; + bool is_win_ = false; int level_health_ = 3; ///< @brief 进入关卡时的生命值(读/存档用) int level_score_ = 0; ///< @brief 进入关卡时的得分(读/存档用) @@ -40,6 +41,7 @@ public: int getLevelHealth() const { return level_health_; } int getLevelScore() const { return level_score_; } const std::string& getMapPath() const { return map_path_; } + bool getIsWin() const { return is_win_; } // --- Setters --- void setCurrentHealth(int health); @@ -49,11 +51,13 @@ public: void setLevelHealth(int level_health) {level_health_ = level_health; } void setLevelScore(int level_score) {level_score_ = level_score; } void setMapPath(const std::string& map_path) { map_path_ = map_path; } + void setIsWin(bool is_win) { is_win_ = is_win; } void reset(); ///< @brief 重置游戏数据以准备开始新游戏(保留最高分) void setNextLevel(const std::string& map_path); ///< @brief 设置下一个场景信息(地图、关卡开始时的得分生命) bool saveToFile(const std::string& filename) const; ///< @brief 将当前游戏数据保存到JSON文件(存档) bool loadFromFile(const std::string& filename); ///< @brief 从JSON文件中读取游戏数据(读档) + bool syncHighScore(const std::string& filename); ///< @brief 同步最高分(文件与当前分数取最大值) }; } // namespace game::state \ No newline at end of file diff --git a/src/game/scene/end_scene.cpp b/src/game/scene/end_scene.cpp new file mode 100644 index 0000000..ce1916f --- /dev/null +++ b/src/game/scene/end_scene.cpp @@ -0,0 +1,143 @@ +#include "end_scene.h" +#include "title_scene.h" +#include "game_scene.h" +#include "../data/session_data.h" +#include "../../engine/core/context.h" +#include "../../engine/core/game_state.h" +#include "../../engine/input/input_manager.h" +#include "../../engine/scene/scene_manager.h" +#include "../../engine/ui/ui_manager.h" +#include "../../engine/ui/ui_label.h" +#include "../../engine/ui/ui_button.h" +#include "../../engine/utils/math.h" +#include + +namespace game::scene { + +EndScene::EndScene(engine::core::Context& context, + engine::scene::SceneManager& scene_manager, + std::shared_ptr session_data) + : engine::scene::Scene("EndScene", context, scene_manager), + session_data_(std::move(session_data)) { + if (!session_data_) { + spdlog::error("错误:结束场景收到了空的游戏数据!"); + } + spdlog::trace("EndScene (胜利:{}) 创建.", session_data_->getIsWin() ? "是" : "否"); +} + +void EndScene::init() { + if (is_initialized_) { + return; + } + + // 设置游戏状态为 GameOver + context_.getGameState().setState(engine::core::State::GameOver); + + createUI(); + + Scene::init(); + spdlog::info("EndScene 初始化完成。"); +} + +void EndScene::createUI() { + auto window_size = context_.getGameState().getLogicalSize(); + if (!ui_manager_->init(window_size)) { + spdlog::error("错误:结束场景的 UI 管理器初始化失败!"); + return; + } + auto is_win = session_data_->getIsWin(); + + // --- 主文字标签 --- + std::string main_message = is_win ? "YOU WIN! CONGRATS!" : "YOU DIED! TRY AGAIN!"; + // 赢了是绿色,输了是红色 + engine::utils::FColor message_color = is_win ? engine::utils::FColor{0.0f, 1.0f, 0.0f, 1.0f} : + engine::utils::FColor{1.0f, 0.0f, 0.0f, 1.0f}; + + auto main_label = std::make_unique(context_.getTextRenderer(), + main_message, + "assets/fonts/VonwaonBitmap-16px.ttf", + 48, + message_color); + // 标签居中 + glm::vec2 label_size = main_label->getSize(); + glm::vec2 main_label_pos = {(window_size.x - label_size.x) / 2.0f, window_size.y * 0.3f}; + main_label->setPosition(main_label_pos); + ui_manager_->addElement(std::move(main_label)); + + + // --- 得分标签 --- + int current_score = session_data_->getCurrentScore(); + int high_score = session_data_->getHighScore(); + engine::utils::FColor score_color = {1.0f, 1.0f, 1.0f, 1.0f}; + int score_font_size = 24; + + // 当前得分 + std::string score_text = "Score: " + std::to_string(current_score); + auto score_label = std::make_unique(context_.getTextRenderer(), + score_text, + "assets/fonts/VonwaonBitmap-16px.ttf", + score_font_size, + score_color); + glm::vec2 score_label_size = score_label->getSize(); + // x方向居中,y方向在主标签下方20像素 + glm::vec2 score_label_pos = {(window_size.x - score_label_size.x) / 2.0f, main_label_pos.y + label_size.y + 20.0f}; + score_label->setPosition(score_label_pos); + ui_manager_->addElement(std::move(score_label)); + + // --- 最高分 --- + std::string high_score_text = "High Score: " + std::to_string(high_score); + auto high_score_label = std::make_unique(context_.getTextRenderer(), + high_score_text, + "assets/fonts/VonwaonBitmap-16px.ttf", + score_font_size, + score_color); + glm::vec2 high_score_label_size = high_score_label->getSize(); + // x方向居中,y方向在当前得分下方10像素 + glm::vec2 high_score_label_pos = {(window_size.x - high_score_label_size.x) / 2.0f, score_label_pos.y + score_label_size.y + 10.0f}; + high_score_label->setPosition(high_score_label_pos); + ui_manager_->addElement(std::move(high_score_label)); + + // --- UI按钮 --- + glm::vec2 button_size = {120.0f, 40.0f}; // 让按钮更大一点 + float button_spacing = 20.0f; + float total_button_width = button_size.x * 2 + button_spacing; + + // 按钮放在右下角,与边缘间隔30像素 + float buttons_x = window_size.x - total_button_width - 30.0f; + float buttons_y = window_size.y - button_size.y - 30.0f; + + // Back Button + auto back_button = std::make_unique(context_, + "assets/textures/UI/buttons/Back1.png", + "assets/textures/UI/buttons/Back2.png", + "assets/textures/UI/buttons/Back3.png", + glm::vec2{buttons_x, buttons_y}, + button_size, + [this]() { this->onBackClick(); }); + ui_manager_->addElement(std::move(back_button)); + + // Restart Button + buttons_x += button_size.x + button_spacing; + auto restart_button = std::make_unique(context_, + "assets/textures/UI/buttons/Restart1.png", + "assets/textures/UI/buttons/Restart2.png", + "assets/textures/UI/buttons/Restart3.png", + glm::vec2{buttons_x, buttons_y}, + button_size, + [this]() { this->onRestartClick(); }); + ui_manager_->addElement(std::move(restart_button)); +} + +void EndScene::onBackClick() { + spdlog::info("返回按钮被点击。"); + scene_manager_.requestReplaceScene(std::make_unique(context_, scene_manager_, session_data_)); +} + +void EndScene::onRestartClick() { + spdlog::info("重新开始按钮被点击。"); + // 重新开始游戏 + session_data_->reset(); + scene_manager_.requestReplaceScene(std::make_unique(context_, scene_manager_, session_data_)); +} + +} // namespace game::scene \ No newline at end of file diff --git a/src/game/scene/end_scene.h b/src/game/scene/end_scene.h new file mode 100644 index 0000000..e0f104d --- /dev/null +++ b/src/game/scene/end_scene.h @@ -0,0 +1,71 @@ +#pragma once +#include "../../engine/scene/scene.h" +#include +#include + +// 前置声明 +namespace engine::core { + class Context; +} +namespace engine::scene { + class SceneManager; +} +namespace game::data { + class SessionData; +} +namespace engine::ui { + class UIManager; + class Label; + class Button; +} +namespace engine::input { + class InputManager; +} +namespace engine::render{ + class Renderer; + class Camera; +} + + +namespace game::scene { + +/** + * @class EndScene + * @brief 显示游戏结束(胜利或失败)信息的叠加场景。 + * + * 提供重新开始或返回主菜单的选项。 + */ +class EndScene final : public engine::scene::Scene { +private: + std::shared_ptr session_data_; + +public: + /** + * @brief 构造函数 + * @param context 引擎上下文 + * @param scene_manager 场景管理器 + * @param session_data 指向游戏数据状态的共享指针 + */ + EndScene(engine::core::Context& context, + engine::scene::SceneManager& scene_manager, + std::shared_ptr session_data); + + ~EndScene() override = default; + + // --- 核心循环方法 --- + void init() override; + + // 禁止拷贝和移动 + EndScene(const EndScene&) = delete; + EndScene& operator=(const EndScene&) = delete; + EndScene(EndScene&&) = delete; + EndScene& operator=(EndScene&&) = delete; + +private: + // 用于创建UI和按钮回调的私有辅助方法 + void createUI(); + void onBackClick(); + void onRestartClick(); +}; + +} // namespace game::scene diff --git a/src/game/scene/game_scene.cpp b/src/game/scene/game_scene.cpp index c01a21f..58b8be6 100644 --- a/src/game/scene/game_scene.cpp +++ b/src/game/scene/game_scene.cpp @@ -1,5 +1,6 @@ #include "game_scene.h" #include "menu_scene.h" +#include "end_scene.h" #include "../component/player_component.h" #include "../../engine/core/context.h" #include "../../engine/core/game_state.h" @@ -52,6 +53,7 @@ void GameScene::init() { } spdlog::trace("GameScene 初始化开始..."); context_.getGameState().setState(engine::core::State::Playing); + game_session_data_->syncHighScore("assets/save.json"); // 更新最高分 if (!initLevel()) { spdlog::error("关卡初始化失败,无法继续。"); @@ -75,7 +77,7 @@ void GameScene::init() { } // 播放背景音乐 (循环,淡入1秒) - // context_.getAudioPlayer().playMusic("assets/audio/hurry_up_and_run.ogg", true, 1000); + context_.getAudioPlayer().playMusic("assets/audio/hurry_up_and_run.ogg", true, 1000); Scene::init(); spdlog::trace("GameScene 初始化完成。"); @@ -85,6 +87,17 @@ void GameScene::update(float delta_time) { Scene::update(delta_time); handleObjectCollisions(); handleTileTriggers(); + + // 玩家掉出地图下方则判断为失败 + if (player_ ) { + auto pos = player_->getComponent()->getPosition(); + auto world_rect = context_.getPhysicsEngine().getWorldBounds(); + // 多100像素冗余量 + if (world_rect && pos.y > world_rect->position.y + world_rect->size.y + 100.0f) { + spdlog::debug("玩家掉出地图下方,游戏失败"); + showEndScene(false); + } + } } void GameScene::render() { @@ -156,6 +169,15 @@ bool GameScene::initPlayer() return false; } + // 从SessionData中更新玩家生命值 + if (auto health_component = player_->getComponent(); health_component) { + health_component->setMaxHealth(game_session_data_->getMaxHealth()); + health_component->setCurrentHealth(game_session_data_->getCurrentHealth()); + } else { + spdlog::error("玩家对象缺少 HealthComponent 组件,无法设置生命值"); + return false; + } + // 相机跟随玩家 auto* player_transform = player_->getComponent(); if (!player_transform) { @@ -247,6 +269,12 @@ void GameScene::handleObjectCollisions() } else if (obj2->getName() == "player" && obj1->getTag() == "next_level") { toNextLevel(obj1); } + // 处理玩家与结束触发器碰撞 + else if (obj1->getName() == "player" && obj2->getName() == "win") { + showEndScene(true); + } else if (obj2->getName() == "player" && obj1->getName() == "win") { + showEndScene(true); + } } } @@ -340,6 +368,14 @@ void GameScene::toNextLevel(engine::object::GameObject *trigger) scene_manager_.requestReplaceScene(std::move(next_scene)); } +void GameScene::showEndScene(bool is_win) +{ + spdlog::debug("显示结束场景,游戏 {}", is_win ? "胜利" : "失败"); + game_session_data_->setIsWin(is_win); + auto end_scene = std::make_unique(context_, scene_manager_, game_session_data_); + scene_manager_.requestPushScene(std::move(end_scene)); +} + void GameScene::createEffect(const glm::vec2& center_pos, const std::string &tag) { // --- 创建游戏对象和变换组件 --- @@ -412,11 +448,13 @@ void GameScene::createHealthUI() { auto bg_icon = std::make_unique(empty_heart_tex, icon_pos, icon_size); health_panel_->addChild(std::move(bg_icon)); } - for (int i = 0; i < current_health; ++i) { // 创建前景图标 + for (int i = 0; i < max_health; ++i) { // 创建前景图标 glm::vec2 icon_pos = {start_x + i * (icon_width + spacing), start_y}; glm::vec2 icon_size = {icon_width, icon_height}; auto fg_icon = std::make_unique(full_heart_tex, icon_pos, icon_size); + bool is_visible = (i < current_health); // 前景图标的可见性取决于当前生命值 + fg_icon->setVisible(is_visible); // 设置前景图标的可见性 health_panel_->addChild(std::move(fg_icon)); } // 将UIPanel添加到UI管理器中 @@ -429,7 +467,6 @@ void GameScene::updateHealthWithUI() spdlog::error("玩家对象或 HealthPanel 不存在,无法更新生命值UI"); return; } - // 获取当前生命值并更新游戏数据 auto current_health = player_->getComponent()->getCurrentHealth(); game_session_data_->setCurrentHealth(current_health); diff --git a/src/game/scene/game_scene.h b/src/game/scene/game_scene.h index 413376b..4bc3f60 100644 --- a/src/game/scene/game_scene.h +++ b/src/game/scene/game_scene.h @@ -54,6 +54,7 @@ private: void playerVSItemCollision(engine::object::GameObject* player, engine::object::GameObject* item); ///< @brief 玩家与道具碰撞处理 void toNextLevel(engine::object::GameObject* trigger); ///< @brief 进入下一个关卡 + void showEndScene(bool is_win); ///< @brief 显示结束场景 /// @brief 根据关卡名称获取对应的地图文件路径 std::string levelNameToPath(const std::string& level_name) const { return "assets/maps/" + level_name + ".tmj"; } diff --git a/src/game/scene/menu_scene.cpp b/src/game/scene/menu_scene.cpp index 18bad6c..85a3549 100644 --- a/src/game/scene/menu_scene.cpp +++ b/src/game/scene/menu_scene.cpp @@ -143,6 +143,7 @@ void MenuScene::onBackClicked() { void MenuScene::onQuitClicked() { spdlog::debug("退出按钮被点击。请求应用程序退出。"); + session_data_->syncHighScore("assets/save.json"); // 退出前先同步最高分 context_.getInputManager().setShouldQuit(true); // 输入管理器设置退出标志 } diff --git a/src/game/scene/title_scene.cpp b/src/game/scene/title_scene.cpp index ab47144..a52db53 100644 --- a/src/game/scene/title_scene.cpp +++ b/src/game/scene/title_scene.cpp @@ -45,6 +45,11 @@ void TitleScene::init() { spdlog::error("加载背景失败"); return; } + session_data_->syncHighScore("assets/save.json"); // 更新最高分 + + // 重置相机坐标,不限制边界 + context_.getCamera().setPosition(glm::vec2(0.0f, 0.0f)); + context_.getCamera().setLimitBounds(std::nullopt); // 若无这一行,从GameScene返回到标题场景时,相机会限制在地图边界内 // 创建 UI 元素 createUI(); @@ -69,7 +74,7 @@ void TitleScene::createUI() { context_.getAudioPlayer().setSoundVolume(0.5f); // 设置音效音量为50% // 设置背景音乐 - // context_.getAudioPlayer().playMusic("assets/audio/platformer_level03_loop.ogg"); + context_.getAudioPlayer().playMusic("assets/audio/platformer_level03_loop.ogg"); // 创建标题图片 (假设不知道大小) auto title_image = std::make_unique("assets/textures/UI/title-screen.png"); @@ -203,6 +208,7 @@ void TitleScene::onHelpsClick() { void TitleScene::onQuitClick() { spdlog::debug("退出按钮被点击。"); + session_data_->syncHighScore("assets/save.json"); // 退出前先同步最高分 context_.getInputManager().setShouldQuit(true); } diff --git a/src/main.cpp b/src/main.cpp index 9864134..dacba1b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,7 @@ #include int main(int /* argc */, char* /* argv */[]) { - spdlog::set_level(spdlog::level::info); + spdlog::set_level(spdlog::level::off); engine::core::GameApp app; app.run();