diff --git a/CMakeLists.txt b/CMakeLists.txt index 184ba34..f31fba2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable(${TARGET} src/main.cpp src/engine/core/game_app.cpp src/engine/core/time.cpp + src/engine/core/config.cpp src/engine/resource/resource_manager.cpp src/engine/resource/texture_manager.cpp src/engine/resource/audio_manager.cpp diff --git a/assets/config.json b/assets/config.json new file mode 100644 index 0000000..bd0fbf0 --- /dev/null +++ b/assets/config.json @@ -0,0 +1,48 @@ +{ + "window": { + "title": "SunnyLand", + "width": 1280, + "height": 720, + "resizable": true + }, + "graphics": { + "vsync": true + }, + "performance": { + "target_fps": 144 + }, + "audio": { + "music_volume": 0.5, + "sound_volume": 0.5 + }, + "input_mappings": { + "pause": [ + "P", + "Escape" + ], + "move_down": [ + "S", + "Down" + ], + "jump": [ + "J", + "Space" + ], + "move_up": [ + "W", + "Up" + ], + "move_right": [ + "D", + "Right" + ], + "attack": [ + "K", + "MouseLeft" + ], + "move_left": [ + "A", + "Left" + ] + } +} \ No newline at end of file diff --git a/src/engine/core/config.cpp b/src/engine/core/config.cpp new file mode 100644 index 0000000..7e2e8e6 --- /dev/null +++ b/src/engine/core/config.cpp @@ -0,0 +1,119 @@ +#include "config.h" +#include +#include +#include "spdlog/spdlog.h" + +namespace engine::core { + +Config::Config(const std::string &filepath) +{ + loadFromFile(filepath); +} + +bool Config::loadFromFile(const std::string& filepath) { + std::ifstream file(filepath); + if (!file.is_open()) { + spdlog::warn("配置文件 '{}' 未找到。使用默认设置并创建默认配置文件。", filepath); + if (!saveToFile(filepath)) { + spdlog::error("无法创建默认配置文件 '{}'。", filepath); + return false; + } + return false; // 文件不存在,使用默认值 + } + + try { + nlohmann::json j; + file >> j; + fromJson(j); + spdlog::info("成功从 '{}' 加载配置。", filepath); + return true; + } catch (const std::exception& e) { + spdlog::error("读取配置文件 '{}' 时出错:{}。使用默认设置。", filepath, e.what()); + } + return false; +} + +bool Config::saveToFile(const std::string& filepath) { + std::ofstream file(filepath); + if (!file.is_open()) { + spdlog::error("无法打开配置文件 '{}' 进行写入。", filepath); + return false; + } + + try { + nlohmann::ordered_json j = toJson(); + file << j.dump(4); + spdlog::info("成功将配置保存到 '{}'。", filepath); + return true; + } catch (const std::exception& e) { + spdlog::error("写入配置文件 '{}' 时出错:{}", filepath, e.what()); + } + return false; +} + +void Config::fromJson(const nlohmann::json& j) { + if (j.contains("window")) { + const auto& window_config = j["window"]; + window_title_ = window_config.value("title", window_title_); + window_width_ = window_config.value("width", window_width_); + window_height_ = window_config.value("height", window_height_); + window_resizable_ = window_config.value("resizable", window_resizable_); + } + if (j.contains("graphics")) { + const auto& graphics_config = j["graphics"]; + vsync_enabled_ = graphics_config.value("vsync", vsync_enabled_); + } + if (j.contains("performance")) { + const auto& perf_config = j["performance"]; + target_fps_ = perf_config.value("target_fps", target_fps_); + if (target_fps_ < 0) { + spdlog::warn("目标 FPS 不能为负数。设置为 0(无限制)。"); + target_fps_ = 0; + } + } + if (j.contains("audio")) { + const auto& audio_config = j["audio"]; + music_volume_ = audio_config.value("music_volume", music_volume_); + sound_volume_ = audio_config.value("sound_volume", sound_volume_); + } + + // 从 JSON 加载 input_mappings + if (j.contains("input_mappings") && j["input_mappings"].is_object()) { + const auto& mappings_json = j["input_mappings"]; + try { + // 直接尝试从 JSON 对象转换为 map> + auto input_mappings = mappings_json.get>>(); + // 如果成功,则将 input_mappings 移动到 input_mappings_ + input_mappings_ = std::move(input_mappings); + spdlog::trace("成功从配置加载输入映射。"); + } catch (const std::exception& e) { + spdlog::warn("配置加载警告:解析 'input_mappings' 时发生异常。使用默认映射。错误:{}", e.what()); + } + } else { + spdlog::trace("配置跟踪:未找到 'input_mappings' 部分或不是对象。使用头文件中定义的默认映射。"); + } +} + +nlohmann::ordered_json Config::toJson() const { + return nlohmann::ordered_json{ + {"window", { + {"title", window_title_}, + {"width", window_width_}, + {"height", window_height_}, + {"resizable", window_resizable_} + }}, + {"graphics", { + {"vsync", vsync_enabled_} + }}, + {"performance", { + {"target_fps", target_fps_} + }}, + {"audio", { + {"music_volume", music_volume_}, + {"sound_volume", sound_volume_} + }}, + {"input_mappings", input_mappings_} + }; +} + +} // namespace engine::core \ No newline at end of file diff --git a/src/engine/core/config.h b/src/engine/core/config.h new file mode 100644 index 0000000..593df30 --- /dev/null +++ b/src/engine/core/config.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include // nlohmann_json 提供的前向声明 + +namespace engine::core { + +/** + * @brief 管理应用程序的配置设置。 + * + * 提供配置项的默认值,并支持从 JSON 文件加载/保存配置。 + * 如果加载失败或文件不存在,将使用默认值。 + */ +class Config final { +public: + // --- 默认配置值 --- (为了方便拓展,全部设置为公有) + // 窗口设置 + std::string window_title_ = "SunnyLand"; + int window_width_ = 1280; + int window_height_ = 720; + bool window_resizable_ = true; + + // 图形设置 + bool vsync_enabled_ = true; ///< @brief 是否启用垂直同步 + + // 性能设置 + int target_fps_ = 144; ///< @brief 目标 FPS 设置,0 表示不限制 + + // 音频设置 + float music_volume_ = 0.5f; + float sound_volume_ = 0.5f; + + // 存储动作名称到 SDL Scancode 名称列表的映射 + std::unordered_map> input_mappings_ = { + // 提供一些合理的默认值,以防配置文件加载失败或缺少此部分 + {"move_left", {"A", "Left"}}, + {"move_right", {"D", "Right"}}, + {"move_up", {"W", "Up"}}, + {"move_down", {"S", "Down"}}, + {"jump", {"J", "Space"}}, + {"attack", {"K", "MouseLeft"}}, + {"pause", {"P", "Escape"}}, + // 可以继续添加更多默认动作 + }; + + explicit Config(const std::string& filepath); ///< @brief 构造函数,指定配置文件路径。 + + // 删除拷贝和移动语义 + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + Config(Config&&) = delete; + Config& operator=(Config&&) = delete; + + bool loadFromFile(const std::string& filepath); ///< @brief 从指定的 JSON 文件加载配置。成功返回 true,否则返回 false。 + [[nodiscard]] bool saveToFile(const std::string& filepath); ///< @brief 将当前配置保存到指定的 JSON 文件。成功返回 true,否则返回 false。 + +private: + void fromJson(const nlohmann::json& j); ///< @brief 从 JSON 对象反序列化配置。 + nlohmann::ordered_json toJson() const; ///< @brief 将当前配置转换为 JSON 对象(按顺序)。 +}; + +} // namespace engine::core \ No newline at end of file diff --git a/src/engine/core/game_app.cpp b/src/engine/core/game_app.cpp index 9634b24..62d46e8 100644 --- a/src/engine/core/game_app.cpp +++ b/src/engine/core/game_app.cpp @@ -3,6 +3,7 @@ #include "../resource/resource_manager.h" #include "../render/renderer.h" #include "../render/camera.h" +#include "config.h" #include #include @@ -22,7 +23,7 @@ void GameApp::run() { spdlog::error("初始化失败,无法运行游戏。"); return; } - time_->setTargetFps(144); // 设置目标帧率(临时,未来会从配置文件读取) + while (is_running_) { time_->update(); float delta_time = time_->getDeltaTime(); @@ -39,6 +40,7 @@ void GameApp::run() { bool GameApp::init() { spdlog::trace("初始化 GameApp ..."); + if (!initConfig()) return false; if (!initSDL()) return false; if (!initTime()) return false; if (!initResourceManager()) return false; @@ -96,13 +98,26 @@ void GameApp::close() { is_running_ = false; } -bool GameApp::initSDL() { +bool GameApp::initConfig() +{ + try { + config_ = std::make_unique("assets/config.json"); + } catch (const std::exception& e) { + spdlog::error("初始化配置失败: {}", e.what()); + return false; + } + spdlog::trace("配置初始化成功。"); + return true; +} + +bool GameApp::initSDL() +{ if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { spdlog::error("SDL 初始化失败! SDL错误: {}", SDL_GetError()); return false; } - window_ = SDL_CreateWindow("SunnyLand", 1280, 720, SDL_WINDOW_RESIZABLE); + window_ = SDL_CreateWindow(config_->window_title_.c_str(), config_->window_width_, config_->window_height_, SDL_WINDOW_RESIZABLE); if (window_ == nullptr) { spdlog::error("无法创建窗口! SDL错误: {}", SDL_GetError()); return false; @@ -114,8 +129,13 @@ bool GameApp::initSDL() { return false; } - // 设置逻辑分辨率 - SDL_SetRenderLogicalPresentation(sdl_renderer_, 640, 360, SDL_LOGICAL_PRESENTATION_LETTERBOX); + // 设置 VSync (注意: VSync 开启时,驱动程序会尝试将帧率限制到显示器刷新率,有可能会覆盖我们手动设置的 target_fps) + int vsync_mode = config_->vsync_enabled_ ? SDL_RENDERER_VSYNC_ADAPTIVE : SDL_RENDERER_VSYNC_DISABLED; + SDL_SetRenderVSync(sdl_renderer_, vsync_mode); + spdlog::trace("VSync 设置为: {}", config_->vsync_enabled_ ? "Enabled" : "Disabled"); + + // 设置逻辑分辨率为窗口大小的一半(针对像素游戏) + SDL_SetRenderLogicalPresentation(sdl_renderer_, config_->window_width_ / 2, config_->window_height_ / 2, SDL_LOGICAL_PRESENTATION_LETTERBOX); spdlog::trace("SDL 初始化成功。"); return true; } @@ -127,6 +147,7 @@ bool GameApp::initTime() { spdlog::error("初始化时间管理失败: {}", e.what()); return false; } + time_->setTargetFps(config_->target_fps_); spdlog::trace("时间管理初始化成功。"); return true; } @@ -155,7 +176,7 @@ bool GameApp::initRenderer() { bool GameApp::initCamera() { try { - camera_ = std::make_unique(glm::vec2(640, 360)); + camera_ = std::make_unique(glm::vec2(config_->window_width_ / 2, config_->window_height_ / 2)); } catch (const std::exception& e) { spdlog::error("初始化相机失败: {}", e.what()); return false; diff --git a/src/engine/core/game_app.h b/src/engine/core/game_app.h index 41cd959..5b77b03 100644 --- a/src/engine/core/game_app.h +++ b/src/engine/core/game_app.h @@ -16,6 +16,7 @@ class Camera; namespace engine::core { // 命名空间的最佳实践:与文件路径一致 class Time; +class Config; /** * @brief 主游戏应用程序类,初始化SDL,管理游戏循环。 @@ -31,7 +32,7 @@ private: std::unique_ptr resource_manager_; std::unique_ptr renderer_; std::unique_ptr camera_; - + std::unique_ptr config_; public: GameApp(); ~GameApp(); @@ -55,6 +56,7 @@ private: void close(); // 各模块的初始化/创建函数,在init()中调用 + [[nodiscard]] bool initConfig(); [[nodiscard]] bool initSDL(); [[nodiscard]] bool initTime(); [[nodiscard]] bool initResourceManager();