refactor(platform): 重构平台和模块配置系统
将平台相关配置分离到独立头文件 移除AppConfig中的模块配置,改为模块自行管理 统一使用SDL2作为窗口后端 优化Switch平台支持 添加模块配置接口 重构配置加载器以支持模块配置
This commit is contained in:
parent
453a057c7d
commit
8c56c29cd2
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file audio_config.h
|
||||
* @brief 音频模块配置
|
||||
*
|
||||
* 定义音频相关的配置数据结构,由 AudioModule 管理。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 音频配置数据结构
|
||||
*/
|
||||
struct AudioConfigData {
|
||||
bool enabled = true;
|
||||
int masterVolume = 100;
|
||||
int musicVolume = 100;
|
||||
int sfxVolume = 100;
|
||||
int voiceVolume = 100;
|
||||
int ambientVolume = 100;
|
||||
int frequency = 44100;
|
||||
int channels = 2;
|
||||
int chunkSize = 2048;
|
||||
int maxChannels = 16;
|
||||
bool spatialAudio = false;
|
||||
float listenerPosition[3] = {0.0f, 0.0f, 0.0f};
|
||||
|
||||
/**
|
||||
* @brief 验证音量值是否有效
|
||||
* @param volume 要验证的音量值
|
||||
* @return 如果音量在0-100范围内返回 true
|
||||
*/
|
||||
bool isValidVolume(int volume) const { return volume >= 0 && volume <= 100; }
|
||||
|
||||
/**
|
||||
* @brief 将音量值转换为浮点数
|
||||
* @param volume 音量值(0-100)
|
||||
* @return 浮点数音量值(0.0-1.0)
|
||||
*/
|
||||
float volumeToFloat(int volume) const { return static_cast<float>(volume) / 100.0f; }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1,180 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 窗口模式枚举
|
||||
// ============================================================================
|
||||
enum class WindowMode {
|
||||
Windowed,
|
||||
Fullscreen,
|
||||
Borderless
|
||||
};
|
||||
/**
|
||||
* @file app_config.h
|
||||
* @brief 应用级别配置
|
||||
*
|
||||
* 本文件仅包含应用级别的配置项,不包含任何模块特定配置。
|
||||
* 各模块应该在自己的模块文件中定义配置结构,并实现 IModuleConfig 接口。
|
||||
*
|
||||
* 模块配置通过 ModuleRegistry 注册,由 ConfigManager 统一管理。
|
||||
* 这种设计遵循开闭原则,新增模块无需修改引擎核心代码。
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// 窗口配置数据
|
||||
// ============================================================================
|
||||
struct WindowConfigData {
|
||||
std::string title = "Extra2D Application";
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
int minWidth = 320;
|
||||
int minHeight = 240;
|
||||
int maxWidth = 0;
|
||||
int maxHeight = 0;
|
||||
WindowMode mode = WindowMode::Windowed;
|
||||
bool resizable = true;
|
||||
bool borderless = false;
|
||||
bool alwaysOnTop = false;
|
||||
bool centered = true;
|
||||
int posX = -1;
|
||||
int posY = -1;
|
||||
bool hideOnClose = false;
|
||||
bool minimizeOnClose = true;
|
||||
float opacity = 1.0f;
|
||||
bool transparentFramebuffer = false;
|
||||
bool highDPI = true;
|
||||
float contentScale = 1.0f;
|
||||
bool vsync = true;
|
||||
int multisamples = 0;
|
||||
bool visible = true;
|
||||
bool decorated = true;
|
||||
|
||||
bool isSizeValid() const { return width > 0 && height > 0; }
|
||||
bool hasPosition() const { return posX >= 0 && posY >= 0; }
|
||||
float aspectRatio() const { return static_cast<float>(width) / static_cast<float>(height); }
|
||||
bool isFullscreen() const { return mode == WindowMode::Fullscreen; }
|
||||
bool isBorderless() const { return mode == WindowMode::Borderless || borderless; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 渲染配置数据
|
||||
// ============================================================================
|
||||
struct RenderConfigData {
|
||||
BackendType backend = BackendType::OpenGL;
|
||||
int targetFPS = 60;
|
||||
bool vsync = true;
|
||||
bool tripleBuffering = false;
|
||||
int multisamples = 0;
|
||||
bool sRGBFramebuffer = false;
|
||||
Color clearColor{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
int maxTextureSize = 0;
|
||||
int textureAnisotropy = 1;
|
||||
bool wireframeMode = false;
|
||||
bool depthTest = false;
|
||||
bool blending = true;
|
||||
bool dithering = false;
|
||||
int spriteBatchSize = 1000;
|
||||
int maxRenderTargets = 1;
|
||||
bool allowShaderHotReload = false;
|
||||
std::string shaderCachePath;
|
||||
|
||||
bool isMultisampleEnabled() const { return multisamples > 0; }
|
||||
bool isFPSCapped() const { return targetFPS > 0; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 音频配置数据
|
||||
// ============================================================================
|
||||
struct AudioConfigData {
|
||||
bool enabled = true;
|
||||
int masterVolume = 100;
|
||||
int musicVolume = 100;
|
||||
int sfxVolume = 100;
|
||||
int voiceVolume = 100;
|
||||
int ambientVolume = 100;
|
||||
int frequency = 44100;
|
||||
int channels = 2;
|
||||
int chunkSize = 2048;
|
||||
int maxChannels = 16;
|
||||
bool spatialAudio = false;
|
||||
float listenerPosition[3] = {0.0f, 0.0f, 0.0f};
|
||||
|
||||
bool isValidVolume(int volume) const { return volume >= 0 && volume <= 100; }
|
||||
float volumeToFloat(int volume) const { return static_cast<float>(volume) / 100.0f; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 调试配置数据
|
||||
// ============================================================================
|
||||
struct DebugConfigData {
|
||||
bool enabled = false;
|
||||
bool showFPS = false;
|
||||
bool showMemoryUsage = false;
|
||||
bool showRenderStats = false;
|
||||
bool showColliders = false;
|
||||
bool showGrid = false;
|
||||
bool logToFile = false;
|
||||
bool logToConsole = true;
|
||||
int logLevel = 2;
|
||||
bool breakOnAssert = true;
|
||||
bool enableProfiling = false;
|
||||
std::string logFilePath;
|
||||
std::vector<std::string> debugFlags;
|
||||
|
||||
bool hasDebugFlag(const std::string& flag) const;
|
||||
void addDebugFlag(const std::string& flag);
|
||||
void removeDebugFlag(const std::string& flag);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 输入配置数据
|
||||
// ============================================================================
|
||||
struct InputConfigData {
|
||||
bool enabled = true;
|
||||
bool rawMouseInput = false;
|
||||
float mouseSensitivity = 1.0f;
|
||||
bool invertMouseY = false;
|
||||
bool invertMouseX = false;
|
||||
float deadzone = 0.15f;
|
||||
float triggerThreshold = 0.5f;
|
||||
bool enableVibration = true;
|
||||
int maxGamepads = 4;
|
||||
bool autoConnectGamepads = true;
|
||||
std::string gamepadMappingFile;
|
||||
|
||||
bool isDeadzoneValid() const { return deadzone >= 0.0f && deadzone <= 1.0f; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 资源配置数据
|
||||
// ============================================================================
|
||||
struct ResourceConfigData {
|
||||
std::string assetRootPath = "assets";
|
||||
std::string cachePath = "cache";
|
||||
std::string savePath = "saves";
|
||||
std::string configPath = "config";
|
||||
std::string logPath = "logs";
|
||||
bool useAssetCache = true;
|
||||
int maxCacheSize = 512;
|
||||
bool hotReloadEnabled = false;
|
||||
float hotReloadInterval = 1.0f;
|
||||
bool compressTextures = false;
|
||||
bool preloadCommonAssets = true;
|
||||
std::vector<std::string> searchPaths;
|
||||
|
||||
void addSearchPath(const std::string& path);
|
||||
void removeSearchPath(const std::string& path);
|
||||
bool hasSearchPath(const std::string& path) const;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 应用统一配置
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 应用配置结构体
|
||||
* 仅包含应用级别的配置项,模块配置由各模块自行管理
|
||||
*/
|
||||
struct AppConfig {
|
||||
WindowConfigData window;
|
||||
RenderConfigData render;
|
||||
AudioConfigData audio;
|
||||
DebugConfigData debug;
|
||||
InputConfigData input;
|
||||
ResourceConfigData resource;
|
||||
std::string appName = "Extra2D App";
|
||||
std::string appVersion = "1.0.0";
|
||||
std::string organization = "";
|
||||
|
|
@ -193,12 +40,6 @@ struct AppConfig {
|
|||
*/
|
||||
bool validate() const;
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* @param platform 平台配置接口
|
||||
*/
|
||||
void applyPlatformConstraints(const PlatformConfig& platform);
|
||||
|
||||
/**
|
||||
* @brief 重置为默认值
|
||||
*/
|
||||
|
|
@ -215,12 +56,6 @@ struct AppConfig {
|
|||
* @return 如果所有必要字段都有效返回 true
|
||||
*/
|
||||
bool isValid() const { return validate(); }
|
||||
|
||||
/**
|
||||
* @brief 获取窗口宽高比
|
||||
* @return 窗口的宽高比
|
||||
*/
|
||||
float aspectRatio() const { return window.aspectRatio(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,17 @@
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 配置加载结果
|
||||
// ============================================================================
|
||||
/**
|
||||
* @file config_loader.h
|
||||
* @brief 配置加载器接口
|
||||
*
|
||||
* 配置加载器只负责加载应用级别的配置(AppConfig)。
|
||||
* 模块配置通过 ModuleRegistry 和各模块的 IModuleConfig 接口加载。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 配置加载结果
|
||||
*/
|
||||
struct ConfigLoadResult {
|
||||
bool success = false;
|
||||
std::string errorMessage;
|
||||
|
|
@ -24,9 +32,9 @@ struct ConfigLoadResult {
|
|||
bool hasError() const { return !success; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置保存结果
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 配置保存结果
|
||||
*/
|
||||
struct ConfigSaveResult {
|
||||
bool success = false;
|
||||
std::string errorMessage;
|
||||
|
|
@ -40,15 +48,15 @@ struct ConfigSaveResult {
|
|||
bool hasError() const { return !success; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置加载器抽象接口
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 配置加载器抽象接口
|
||||
*/
|
||||
class ConfigLoader {
|
||||
public:
|
||||
virtual ~ConfigLoader() = default;
|
||||
|
||||
/**
|
||||
* @brief 从文件加载配置
|
||||
* @brief 从文件加载应用配置
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
|
|
@ -56,7 +64,7 @@ public:
|
|||
virtual ConfigLoadResult load(const std::string& filepath, AppConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief 保存配置到文件
|
||||
* @brief 保存应用配置到文件
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 要保存的配置对象
|
||||
* @return 保存结果
|
||||
|
|
@ -78,6 +86,20 @@ public:
|
|||
*/
|
||||
virtual std::string saveToString(const AppConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief 从文件加载完整配置(包括模块配置)
|
||||
* @param filepath 配置文件路径
|
||||
* @return 加载结果
|
||||
*/
|
||||
virtual ConfigLoadResult loadWithModules(const std::string& filepath) = 0;
|
||||
|
||||
/**
|
||||
* @brief 保存完整配置(包括模块配置)到文件
|
||||
* @param filepath 配置文件路径
|
||||
* @return 保存结果
|
||||
*/
|
||||
virtual ConfigSaveResult saveWithModules(const std::string& filepath) = 0;
|
||||
|
||||
/**
|
||||
* @brief 获取支持的文件扩展名
|
||||
* @return 文件扩展名(不含点号,如 "json")
|
||||
|
|
@ -98,9 +120,9 @@ public:
|
|||
virtual UniquePtr<ConfigLoader> clone() const = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// JSON 配置加载器
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief JSON 配置加载器
|
||||
*/
|
||||
class JsonConfigLoader : public ConfigLoader {
|
||||
public:
|
||||
JsonConfigLoader() = default;
|
||||
|
|
@ -110,29 +132,16 @@ public:
|
|||
ConfigSaveResult save(const std::string& filepath, const AppConfig& config) override;
|
||||
ConfigLoadResult loadFromString(const std::string& content, AppConfig& config) override;
|
||||
std::string saveToString(const AppConfig& config) override;
|
||||
ConfigLoadResult loadWithModules(const std::string& filepath) override;
|
||||
ConfigSaveResult saveWithModules(const std::string& filepath) override;
|
||||
const char* extension() const override { return "json"; }
|
||||
bool supportsFile(const std::string& filepath) const override;
|
||||
UniquePtr<ConfigLoader> clone() const override;
|
||||
|
||||
private:
|
||||
ConfigLoadResult parseWindowConfig(const void* jsonValue, WindowConfigData& window);
|
||||
ConfigLoadResult parseRenderConfig(const void* jsonValue, RenderConfigData& render);
|
||||
ConfigLoadResult parseAudioConfig(const void* jsonValue, AudioConfigData& audio);
|
||||
ConfigLoadResult parseDebugConfig(const void* jsonValue, DebugConfigData& debug);
|
||||
ConfigLoadResult parseInputConfig(const void* jsonValue, InputConfigData& input);
|
||||
ConfigLoadResult parseResourceConfig(const void* jsonValue, ResourceConfigData& resource);
|
||||
|
||||
void serializeWindowConfig(void* jsonValue, const WindowConfigData& window);
|
||||
void serializeRenderConfig(void* jsonValue, const RenderConfigData& render);
|
||||
void serializeAudioConfig(void* jsonValue, const AudioConfigData& audio);
|
||||
void serializeDebugConfig(void* jsonValue, const DebugConfigData& debug);
|
||||
void serializeInputConfig(void* jsonValue, const InputConfigData& input);
|
||||
void serializeResourceConfig(void* jsonValue, const ResourceConfigData& resource);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// INI 配置加载器
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief INI 配置加载器
|
||||
*/
|
||||
class IniConfigLoader : public ConfigLoader {
|
||||
public:
|
||||
IniConfigLoader() = default;
|
||||
|
|
@ -142,6 +151,8 @@ public:
|
|||
ConfigSaveResult save(const std::string& filepath, const AppConfig& config) override;
|
||||
ConfigLoadResult loadFromString(const std::string& content, AppConfig& config) override;
|
||||
std::string saveToString(const AppConfig& config) override;
|
||||
ConfigLoadResult loadWithModules(const std::string& filepath) override;
|
||||
ConfigSaveResult saveWithModules(const std::string& filepath) override;
|
||||
const char* extension() const override { return "ini"; }
|
||||
bool supportsFile(const std::string& filepath) const override;
|
||||
UniquePtr<ConfigLoader> clone() const override;
|
||||
|
|
@ -153,9 +164,9 @@ private:
|
|||
ConfigLoadResult parseBool(const std::string& value, bool& result, const std::string& fieldName);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置加载器工厂
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 配置加载器工厂
|
||||
*/
|
||||
class ConfigLoaderFactory {
|
||||
public:
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 配置变更事件
|
||||
// ============================================================================
|
||||
/**
|
||||
* @file config_manager.h
|
||||
* @brief 配置管理器
|
||||
*
|
||||
* 配置管理器只管理应用级别的配置(AppConfig)。
|
||||
* 模块配置通过 ModuleRegistry 管理,各模块实现 IModuleConfig 接口。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 配置变更事件
|
||||
*/
|
||||
struct ConfigChangeEvent {
|
||||
std::string section;
|
||||
std::string field;
|
||||
|
|
@ -20,18 +28,16 @@ struct ConfigChangeEvent {
|
|||
std::string newValue;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置变更回调类型
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 配置变更回调类型
|
||||
*/
|
||||
using ConfigChangeCallback = Function<void(const ConfigChangeEvent &)>;
|
||||
|
||||
// ============================================================================
|
||||
// 配置管理器(单例)
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 配置管理器(单例)
|
||||
*/
|
||||
class ConfigManager {
|
||||
public:
|
||||
using ModuleConfigPtr = Ptr<void>;
|
||||
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* @return 配置管理器实例引用
|
||||
|
|
@ -70,6 +76,20 @@ public:
|
|||
*/
|
||||
ConfigSaveResult saveConfig(const std::string &filepath = "");
|
||||
|
||||
/**
|
||||
* @brief 加载完整配置(包括模块配置)
|
||||
* @param filepath 配置文件路径
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult loadConfigWithModules(const std::string &filepath = "");
|
||||
|
||||
/**
|
||||
* @brief 保存完整配置(包括模块配置)
|
||||
* @param filepath 配置文件路径
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult saveConfigWithModules(const std::string &filepath = "");
|
||||
|
||||
/**
|
||||
* @brief 重新加载配置
|
||||
* @return 加载结果
|
||||
|
|
@ -124,40 +144,6 @@ public:
|
|||
*/
|
||||
void clearChangeCallbacks();
|
||||
|
||||
/**
|
||||
* @brief 注册模块配置
|
||||
* @param moduleName 模块名称
|
||||
* @param config 模块配置指针
|
||||
*/
|
||||
void registerModuleConfig(const std::string &moduleName, Ptr<void> config);
|
||||
|
||||
/**
|
||||
* @brief 获取模块配置
|
||||
* @param moduleName 模块名称
|
||||
* @return 模块配置指针
|
||||
*/
|
||||
template <typename T>
|
||||
Ptr<T> getModuleConfig(const std::string &moduleName) const {
|
||||
auto it = m_moduleConfigs.find(moduleName);
|
||||
if (it != m_moduleConfigs.end()) {
|
||||
return std::static_pointer_cast<T>(it->second);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除模块配置
|
||||
* @param moduleName 模块名称
|
||||
*/
|
||||
void removeModuleConfig(const std::string &moduleName);
|
||||
|
||||
/**
|
||||
* @brief 检查模块配置是否存在
|
||||
* @param moduleName 模块名称
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool hasModuleConfig(const std::string &moduleName) const;
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(字符串)
|
||||
* @param section 配置节
|
||||
|
|
@ -285,8 +271,6 @@ private:
|
|||
ConfigManager &operator=(const ConfigManager &) = delete;
|
||||
|
||||
void notifyChangeCallbacks(const ConfigChangeEvent &event);
|
||||
void applyConfigToInternal(const AppConfig &config);
|
||||
void extractConfigFromInternal(AppConfig &config) const;
|
||||
|
||||
AppConfig m_appConfig;
|
||||
UniquePtr<PlatformConfig> m_platformConfig;
|
||||
|
|
@ -299,16 +283,13 @@ private:
|
|||
std::unordered_map<int, ConfigChangeCallback> m_changeCallbacks;
|
||||
int m_nextCallbackId = 1;
|
||||
|
||||
std::unordered_map<std::string, ModuleConfigPtr> m_moduleConfigs;
|
||||
std::unordered_map<std::string, std::string> m_rawValues;
|
||||
|
||||
bool m_autoSaveEnabled = false;
|
||||
float m_autoSaveInterval = 30.0f;
|
||||
float m_autoSaveTimer = 0.0f;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 便捷宏定义
|
||||
// ============================================================================
|
||||
#define CONFIG_MANAGER ConfigManager::instance()
|
||||
|
||||
} // namespace extra2d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,17 @@
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 平台类型枚举
|
||||
// ============================================================================
|
||||
/**
|
||||
* @file platform_config.h
|
||||
* @brief 平台配置接口
|
||||
*
|
||||
* 平台配置只提供平台能力信息,不再直接修改应用配置。
|
||||
* 各模块通过 IModuleConfig::applyPlatformConstraints() 处理平台约束。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 平台类型枚举
|
||||
*/
|
||||
enum class PlatformType {
|
||||
Auto,
|
||||
Windows,
|
||||
|
|
@ -16,9 +24,9 @@ enum class PlatformType {
|
|||
macOS
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 平台能力结构
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 平台能力结构
|
||||
*/
|
||||
struct PlatformCapabilities {
|
||||
bool supportsWindowed = true;
|
||||
bool supportsFullscreen = true;
|
||||
|
|
@ -46,9 +54,9 @@ struct PlatformCapabilities {
|
|||
bool isConsole() const { return !supportsWindowed && supportsGamepad; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 平台配置抽象接口
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 平台配置抽象接口
|
||||
*/
|
||||
class PlatformConfig {
|
||||
public:
|
||||
virtual ~PlatformConfig() = default;
|
||||
|
|
@ -56,20 +64,24 @@ public:
|
|||
virtual PlatformType platformType() const = 0;
|
||||
virtual const char* platformName() const = 0;
|
||||
virtual const PlatformCapabilities& capabilities() const = 0;
|
||||
virtual void applyConstraints(struct AppConfig& config) const = 0;
|
||||
virtual void applyDefaults(struct AppConfig& config) const = 0;
|
||||
virtual bool validateConfig(struct AppConfig& config) const = 0;
|
||||
|
||||
virtual int getRecommendedWidth() const = 0;
|
||||
virtual int getRecommendedHeight() const = 0;
|
||||
virtual bool isResolutionSupported(int width, int height) const = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 平台配置工厂函数声明
|
||||
// ============================================================================
|
||||
/**
|
||||
* @brief 创建平台配置实例
|
||||
* @param type 平台类型,默认为 Auto(自动检测)
|
||||
* @return 平台配置的智能指针
|
||||
*/
|
||||
UniquePtr<PlatformConfig> createPlatformConfig(PlatformType type = PlatformType::Auto);
|
||||
|
||||
/**
|
||||
* @brief 获取平台类型名称
|
||||
* @param type 平台类型枚举值
|
||||
* @return 平台名称字符串
|
||||
*/
|
||||
const char* getPlatformTypeName(PlatformType type);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file debug_config.h
|
||||
* @brief 调试模块配置
|
||||
*
|
||||
* 定义调试相关的配置数据结构,由 DebugModule 管理。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 调试配置数据结构
|
||||
*/
|
||||
struct DebugConfigData {
|
||||
bool enabled = false;
|
||||
bool showFPS = false;
|
||||
bool showMemoryUsage = false;
|
||||
bool showRenderStats = false;
|
||||
bool showColliders = false;
|
||||
bool showGrid = false;
|
||||
bool logToFile = false;
|
||||
bool logToConsole = true;
|
||||
int logLevel = 2;
|
||||
bool breakOnAssert = true;
|
||||
bool enableProfiling = false;
|
||||
std::string logFilePath;
|
||||
std::vector<std::string> debugFlags;
|
||||
|
||||
/**
|
||||
* @brief 检查是否存在指定的调试标志
|
||||
* @param flag 要检查的标志名称
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool hasDebugFlag(const std::string& flag) const;
|
||||
|
||||
/**
|
||||
* @brief 添加调试标志
|
||||
* @param flag 要添加的标志名称
|
||||
*/
|
||||
void addDebugFlag(const std::string& flag);
|
||||
|
||||
/**
|
||||
* @brief 移除调试标志
|
||||
* @param flag 要移除的标志名称
|
||||
*/
|
||||
void removeDebugFlag(const std::string& flag);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file render_config.h
|
||||
* @brief 渲染模块配置
|
||||
*
|
||||
* 定义渲染相关的配置数据结构,由 RenderModule 管理。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 渲染配置数据结构
|
||||
*/
|
||||
struct RenderConfigData {
|
||||
BackendType backend = BackendType::OpenGL;
|
||||
int targetFPS = 60;
|
||||
bool vsync = true;
|
||||
bool tripleBuffering = false;
|
||||
int multisamples = 0;
|
||||
bool sRGBFramebuffer = false;
|
||||
Color clearColor{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
int maxTextureSize = 0;
|
||||
int textureAnisotropy = 1;
|
||||
bool wireframeMode = false;
|
||||
bool depthTest = false;
|
||||
bool blending = true;
|
||||
bool dithering = false;
|
||||
int spriteBatchSize = 1000;
|
||||
int maxRenderTargets = 1;
|
||||
bool allowShaderHotReload = false;
|
||||
std::string shaderCachePath;
|
||||
|
||||
/**
|
||||
* @brief 检查是否启用多重采样
|
||||
* @return 如果多重采样数大于0返回 true
|
||||
*/
|
||||
bool isMultisampleEnabled() const { return multisamples > 0; }
|
||||
|
||||
/**
|
||||
* @brief 检查是否限制帧率
|
||||
* @return 如果设置了目标帧率返回 true
|
||||
*/
|
||||
bool isFPSCapped() const { return targetFPS > 0; }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <extra2d/graphics/render_config.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <extra2d/core/types.h>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file input_config.h
|
||||
* @brief 输入模块配置
|
||||
*
|
||||
* 定义输入相关的配置数据结构,由 InputModule 管理。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 输入配置数据结构
|
||||
*/
|
||||
struct InputConfigData {
|
||||
bool enabled = true;
|
||||
bool rawMouseInput = false;
|
||||
float mouseSensitivity = 1.0f;
|
||||
bool invertMouseY = false;
|
||||
bool invertMouseX = false;
|
||||
float deadzone = 0.15f;
|
||||
float triggerThreshold = 0.5f;
|
||||
bool enableVibration = true;
|
||||
int maxGamepads = 4;
|
||||
bool autoConnectGamepads = true;
|
||||
std::string gamepadMappingFile;
|
||||
|
||||
/**
|
||||
* @brief 验证死区值是否有效
|
||||
* @return 如果死区值在0-1范围内返回 true
|
||||
*/
|
||||
bool isDeadzoneValid() const { return deadzone >= 0.0f && deadzone <= 1.0f; }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -2,23 +2,27 @@
|
|||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <extra2d/input/input_config.h>
|
||||
#include <extra2d/platform/iinput.h>
|
||||
#include <extra2d/core/types.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file input_module.h
|
||||
* @brief 输入模块
|
||||
*
|
||||
* 输入模块管理键盘、鼠标、手柄和触摸输入。
|
||||
* 通过事件系统分发输入事件。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 输入模块配置
|
||||
* 实现 IModuleConfig 接口
|
||||
*/
|
||||
class InputModuleConfig : public IModuleConfig {
|
||||
public:
|
||||
bool enableKeyboard = true;
|
||||
bool enableMouse = true;
|
||||
bool enableGamepad = true;
|
||||
bool enableTouch = true;
|
||||
float deadzone = 0.15f;
|
||||
float mouseSensitivity = 1.0f;
|
||||
InputConfigData inputConfig;
|
||||
|
||||
/**
|
||||
* @brief 获取模块信息
|
||||
|
|
@ -26,6 +30,7 @@ public:
|
|||
*/
|
||||
ModuleInfo getModuleInfo() const override {
|
||||
ModuleInfo info;
|
||||
info.id = 0;
|
||||
info.name = "Input";
|
||||
info.version = "1.0.0";
|
||||
info.priority = ModulePriority::Input;
|
||||
|
|
@ -47,7 +52,6 @@ public:
|
|||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void applyPlatformConstraints(PlatformType platform) override;
|
||||
|
|
@ -55,7 +59,9 @@ public:
|
|||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void resetToDefaults() override;
|
||||
void resetToDefaults() override {
|
||||
inputConfig = InputConfigData{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
|
|
@ -75,57 +81,21 @@ public:
|
|||
/**
|
||||
* @brief 输入模块初始化器
|
||||
* 实现 IModuleInitializer 接口
|
||||
* 依赖窗口模块
|
||||
*/
|
||||
class InputModuleInitializer : public IModuleInitializer {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
InputModuleInitializer();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~InputModuleInitializer() override;
|
||||
|
||||
/**
|
||||
* @brief 获取模块标识符
|
||||
* @return 模块唯一标识符
|
||||
*/
|
||||
ModuleId getModuleId() const override { return moduleId_; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块优先级
|
||||
* @return 模块优先级
|
||||
*/
|
||||
ModulePriority getPriority() const override { return ModulePriority::Input; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* 返回此模块依赖的其他模块标识符
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> getDependencies() const override;
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool initialize(const IModuleConfig* config) override;
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
void shutdown() override;
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否已初始化
|
||||
* @return 已初始化返回 true
|
||||
*/
|
||||
bool isInitialized() const override { return initialized_; }
|
||||
|
||||
void setModuleId(ModuleId id) { moduleId_ = id; }
|
||||
|
||||
/**
|
||||
* @brief 获取输入接口
|
||||
* @return 输入接口指针
|
||||
|
|
@ -133,16 +103,27 @@ public:
|
|||
IInput* getInput() const { return input_; }
|
||||
|
||||
/**
|
||||
* @brief 设置窗口模块标识符
|
||||
* @param windowModuleId 窗口模块标识符
|
||||
* @brief 更新输入状态
|
||||
* 每帧调用,更新输入状态并分发事件
|
||||
*/
|
||||
void setWindowModuleId(ModuleId windowModuleId) { windowModuleId_ = windowModuleId; }
|
||||
void update();
|
||||
|
||||
private:
|
||||
ModuleId moduleId_ = INVALID_MODULE_ID;
|
||||
ModuleId windowModuleId_ = INVALID_MODULE_ID;
|
||||
IInput* input_ = nullptr;
|
||||
bool initialized_ = false;
|
||||
InputConfigData config_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
/**
|
||||
* @brief 获取输入模块标识符
|
||||
* @return 输入模块标识符
|
||||
*/
|
||||
ModuleId get_input_module_id();
|
||||
|
||||
/**
|
||||
* @brief 注册输入模块
|
||||
*/
|
||||
void register_input_module();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/platform/window_config.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file window_config.h
|
||||
* @brief 窗口模块配置
|
||||
*
|
||||
* 定义窗口相关的配置数据结构,由 WindowModule 管理。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 窗口模式枚举
|
||||
*/
|
||||
enum class WindowMode {
|
||||
Windowed,
|
||||
Fullscreen,
|
||||
Borderless
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 窗口配置数据结构
|
||||
*/
|
||||
struct WindowConfigData {
|
||||
std::string title = "Extra2D Application";
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
int minWidth = 320;
|
||||
int minHeight = 240;
|
||||
int maxWidth = 0;
|
||||
int maxHeight = 0;
|
||||
WindowMode mode = WindowMode::Windowed;
|
||||
bool resizable = true;
|
||||
bool borderless = false;
|
||||
bool alwaysOnTop = false;
|
||||
bool centered = true;
|
||||
int posX = -1;
|
||||
int posY = -1;
|
||||
bool hideOnClose = false;
|
||||
bool minimizeOnClose = true;
|
||||
float opacity = 1.0f;
|
||||
bool transparentFramebuffer = false;
|
||||
bool highDPI = true;
|
||||
float contentScale = 1.0f;
|
||||
bool vsync = true;
|
||||
int multisamples = 0;
|
||||
bool visible = true;
|
||||
bool decorated = true;
|
||||
|
||||
/**
|
||||
* @brief 检查窗口尺寸是否有效
|
||||
* @return 如果宽高都大于0返回 true
|
||||
*/
|
||||
bool isSizeValid() const { return width > 0 && height > 0; }
|
||||
|
||||
/**
|
||||
* @brief 检查是否设置了窗口位置
|
||||
* @return 如果设置了有效位置返回 true
|
||||
*/
|
||||
bool hasPosition() const { return posX >= 0 && posY >= 0; }
|
||||
|
||||
/**
|
||||
* @brief 获取窗口宽高比
|
||||
* @return 宽高比值
|
||||
*/
|
||||
float aspectRatio() const { return static_cast<float>(width) / static_cast<float>(height); }
|
||||
|
||||
/**
|
||||
* @brief 检查是否为全屏模式
|
||||
* @return 如果是全屏模式返回 true
|
||||
*/
|
||||
bool isFullscreen() const { return mode == WindowMode::Fullscreen; }
|
||||
|
||||
/**
|
||||
* @brief 检查是否为无边框模式
|
||||
* @return 如果是无边框模式返回 true
|
||||
*/
|
||||
bool isBorderless() const { return mode == WindowMode::Borderless || borderless; }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -2,17 +2,35 @@
|
|||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/platform/window_config.h>
|
||||
#include <extra2d/platform/iwindow.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file window_module.h
|
||||
* @brief 窗口模块
|
||||
*
|
||||
* 窗口模块使用 SDL2 作为唯一后端,支持以下平台:
|
||||
* - Windows
|
||||
* - Linux
|
||||
* - macOS
|
||||
* - Nintendo Switch
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 窗口模块配置
|
||||
* 实现 IModuleConfig 接口
|
||||
*/
|
||||
class WindowModuleConfig : public IModuleConfig {
|
||||
public:
|
||||
std::string backend = "sdl2";
|
||||
WindowConfigData windowConfig;
|
||||
|
||||
/**
|
||||
* @brief 获取模块信息
|
||||
* @return 模块信息结构体
|
||||
*/
|
||||
ModuleInfo getModuleInfo() const override {
|
||||
ModuleInfo info;
|
||||
info.id = 0;
|
||||
|
|
@ -23,55 +41,155 @@ public:
|
|||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置节名称
|
||||
* @return 配置节名称字符串
|
||||
*/
|
||||
std::string getConfigSectionName() const override {
|
||||
return "window";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 验证配置有效性
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool validate() const override {
|
||||
return windowConfig.width > 0 && windowConfig.height > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void applyPlatformConstraints(PlatformType platform) override;
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void resetToDefaults() override {
|
||||
backend = "sdl2";
|
||||
windowConfig = WindowConfigData{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool loadFromJson(const void* jsonData) override;
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool saveToJson(void* jsonData) const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 窗口模块初始化器
|
||||
* 实现 IModuleInitializer 接口
|
||||
*/
|
||||
class WindowModuleInitializer : public IModuleInitializer {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
WindowModuleInitializer();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~WindowModuleInitializer() override;
|
||||
|
||||
/**
|
||||
* @brief 获取模块标识符
|
||||
* @return 模块唯一标识符
|
||||
*/
|
||||
ModuleId getModuleId() const override { return moduleId_; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块优先级
|
||||
* @return 模块优先级
|
||||
*/
|
||||
ModulePriority getPriority() const override { return ModulePriority::Core; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* @return 依赖模块标识符列表(窗口模块无依赖)
|
||||
*/
|
||||
std::vector<ModuleId> getDependencies() const override { return {}; }
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool initialize(const IModuleConfig* config) override;
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
void shutdown() override;
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否已初始化
|
||||
* @return 已初始化返回 true
|
||||
*/
|
||||
bool isInitialized() const override { return initialized_; }
|
||||
|
||||
/**
|
||||
* @brief 设置模块标识符
|
||||
* @param id 模块标识符
|
||||
*/
|
||||
void setModuleId(ModuleId id) { moduleId_ = id; }
|
||||
|
||||
/**
|
||||
* @brief 设置窗口配置
|
||||
* @param config 窗口配置数据
|
||||
*/
|
||||
void setWindowConfig(const WindowConfigData& config) { windowConfig_ = config; }
|
||||
|
||||
/**
|
||||
* @brief 获取窗口接口
|
||||
* @return 窗口接口指针
|
||||
*/
|
||||
IWindow* getWindow() const { return window_.get(); }
|
||||
|
||||
private:
|
||||
bool initBackend();
|
||||
bool createWindow(const std::string& backend, const WindowConfigData& config);
|
||||
void shutdownBackend();
|
||||
/**
|
||||
* @brief 初始化 SDL2 后端
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool initSDL2();
|
||||
|
||||
/**
|
||||
* @brief 关闭 SDL2 后端
|
||||
*/
|
||||
void shutdownSDL2();
|
||||
|
||||
/**
|
||||
* @brief 创建窗口
|
||||
* @param config 窗口配置数据
|
||||
* @return 创建成功返回 true
|
||||
*/
|
||||
bool createWindow(const WindowConfigData& config);
|
||||
|
||||
ModuleId moduleId_ = INVALID_MODULE_ID;
|
||||
bool initialized_ = false;
|
||||
bool backendInitialized_ = false;
|
||||
std::string backend_;
|
||||
bool sdl2Initialized_ = false;
|
||||
WindowConfigData windowConfig_;
|
||||
UniquePtr<IWindow> window_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 获取窗口模块标识符
|
||||
* @return 窗口模块标识符
|
||||
*/
|
||||
ModuleId get_window_module_id();
|
||||
|
||||
/**
|
||||
* @brief 注册窗口模块
|
||||
*/
|
||||
void register_window_module();
|
||||
|
||||
} // namespace extra2d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @file resource_config.h
|
||||
* @brief 资源模块配置
|
||||
*
|
||||
* 定义资源相关的配置数据结构,由 ResourceModule 管理。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 资源配置数据结构
|
||||
*/
|
||||
struct ResourceConfigData {
|
||||
std::string assetRootPath = "assets";
|
||||
std::string cachePath = "cache";
|
||||
std::string savePath = "saves";
|
||||
std::string configPath = "config";
|
||||
std::string logPath = "logs";
|
||||
bool useAssetCache = true;
|
||||
int maxCacheSize = 512;
|
||||
bool hotReloadEnabled = false;
|
||||
float hotReloadInterval = 1.0f;
|
||||
bool compressTextures = false;
|
||||
bool preloadCommonAssets = true;
|
||||
std::vector<std::string> searchPaths;
|
||||
|
||||
/**
|
||||
* @brief 添加资源搜索路径
|
||||
* @param path 要添加的搜索路径
|
||||
*/
|
||||
void addSearchPath(const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief 移除资源搜索路径
|
||||
* @param path 要移除的搜索路径
|
||||
*/
|
||||
void removeSearchPath(const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief 检查是否存在指定的搜索路径
|
||||
* @param path 要检查的路径
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool hasSearchPath(const std::string& path) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
#include <extra2d/config/config_module.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/graphics/render_module.h>
|
||||
#include <extra2d/graphics/render_config.h>
|
||||
#include <extra2d/graphics/vram_manager.h>
|
||||
#include <extra2d/platform/iinput.h>
|
||||
#include <extra2d/platform/platform_init_module.h>
|
||||
|
|
@ -287,10 +288,14 @@ void Application::mainLoop() {
|
|||
render();
|
||||
|
||||
const auto& appConfig = ConfigManager::instance().appConfig();
|
||||
if (!appConfig.render.vsync && appConfig.render.isFPSCapped()) {
|
||||
|
||||
auto* renderConfig = ModuleRegistry::instance().getModuleConfig(get_render_module_id());
|
||||
auto* renderModuleConfig = dynamic_cast<const RenderModuleConfig*>(renderConfig);
|
||||
|
||||
if (renderModuleConfig && !renderModuleConfig->vsync && renderModuleConfig->targetFPS > 0) {
|
||||
double frameEndTime = getTimeSeconds();
|
||||
double frameTime = frameEndTime - currentTime;
|
||||
double target = 1.0 / static_cast<double>(appConfig.render.targetFPS);
|
||||
double target = 1.0 / static_cast<double>(renderModuleConfig->targetFPS);
|
||||
if (frameTime < target) {
|
||||
auto sleepSeconds = target - frameTime;
|
||||
std::this_thread::sleep_for(std::chrono::duration<double>(sleepSeconds));
|
||||
|
|
|
|||
|
|
@ -1,168 +1,11 @@
|
|||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 检查是否存在指定的调试标志
|
||||
* @param flag 要检查的标志名称
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool DebugConfigData::hasDebugFlag(const std::string& flag) const {
|
||||
return std::find(debugFlags.begin(), debugFlags.end(), flag) != debugFlags.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加调试标志
|
||||
* @param flag 要添加的标志名称
|
||||
*/
|
||||
void DebugConfigData::addDebugFlag(const std::string& flag) {
|
||||
if (!hasDebugFlag(flag)) {
|
||||
debugFlags.push_back(flag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除调试标志
|
||||
* @param flag 要移除的标志名称
|
||||
*/
|
||||
void DebugConfigData::removeDebugFlag(const std::string& flag) {
|
||||
auto it = std::find(debugFlags.begin(), debugFlags.end(), flag);
|
||||
if (it != debugFlags.end()) {
|
||||
debugFlags.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加资源搜索路径
|
||||
* @param path 要添加的搜索路径
|
||||
*/
|
||||
void ResourceConfigData::addSearchPath(const std::string& path) {
|
||||
if (!hasSearchPath(path)) {
|
||||
searchPaths.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除资源搜索路径
|
||||
* @param path 要移除的搜索路径
|
||||
*/
|
||||
void ResourceConfigData::removeSearchPath(const std::string& path) {
|
||||
auto it = std::find(searchPaths.begin(), searchPaths.end(), path);
|
||||
if (it != searchPaths.end()) {
|
||||
searchPaths.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否存在指定的搜索路径
|
||||
* @param path 要检查的路径
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool ResourceConfigData::hasSearchPath(const std::string& path) const {
|
||||
return std::find(searchPaths.begin(), searchPaths.end(), path) != searchPaths.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建默认配置
|
||||
* 返回一个包含所有默认值的应用配置实例
|
||||
* @return 默认的应用配置实例
|
||||
*/
|
||||
AppConfig AppConfig::createDefault() {
|
||||
AppConfig config;
|
||||
|
||||
config.window.title = "Extra2D Application";
|
||||
config.window.width = 1280;
|
||||
config.window.height = 720;
|
||||
config.window.minWidth = 320;
|
||||
config.window.minHeight = 240;
|
||||
config.window.maxWidth = 0;
|
||||
config.window.maxHeight = 0;
|
||||
config.window.mode = WindowMode::Windowed;
|
||||
config.window.resizable = true;
|
||||
config.window.borderless = false;
|
||||
config.window.alwaysOnTop = false;
|
||||
config.window.centered = true;
|
||||
config.window.posX = -1;
|
||||
config.window.posY = -1;
|
||||
config.window.hideOnClose = false;
|
||||
config.window.minimizeOnClose = true;
|
||||
config.window.opacity = 1.0f;
|
||||
config.window.transparentFramebuffer = false;
|
||||
config.window.highDPI = true;
|
||||
config.window.contentScale = 1.0f;
|
||||
|
||||
config.render.backend = BackendType::OpenGL;
|
||||
config.render.targetFPS = 60;
|
||||
config.render.vsync = true;
|
||||
config.render.tripleBuffering = false;
|
||||
config.render.multisamples = 0;
|
||||
config.render.sRGBFramebuffer = false;
|
||||
config.render.clearColor = Color{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
config.render.maxTextureSize = 0;
|
||||
config.render.textureAnisotropy = 1;
|
||||
config.render.wireframeMode = false;
|
||||
config.render.depthTest = false;
|
||||
config.render.blending = true;
|
||||
config.render.dithering = false;
|
||||
config.render.spriteBatchSize = 1000;
|
||||
config.render.maxRenderTargets = 1;
|
||||
config.render.allowShaderHotReload = false;
|
||||
|
||||
config.audio.enabled = true;
|
||||
config.audio.masterVolume = 100;
|
||||
config.audio.musicVolume = 100;
|
||||
config.audio.sfxVolume = 100;
|
||||
config.audio.voiceVolume = 100;
|
||||
config.audio.ambientVolume = 100;
|
||||
config.audio.frequency = 44100;
|
||||
config.audio.channels = 2;
|
||||
config.audio.chunkSize = 2048;
|
||||
config.audio.maxChannels = 16;
|
||||
config.audio.spatialAudio = false;
|
||||
config.audio.listenerPosition[0] = 0.0f;
|
||||
config.audio.listenerPosition[1] = 0.0f;
|
||||
config.audio.listenerPosition[2] = 0.0f;
|
||||
|
||||
config.debug.enabled = false;
|
||||
config.debug.showFPS = false;
|
||||
config.debug.showMemoryUsage = false;
|
||||
config.debug.showRenderStats = false;
|
||||
config.debug.showColliders = false;
|
||||
config.debug.showGrid = false;
|
||||
config.debug.logToFile = false;
|
||||
config.debug.logToConsole = true;
|
||||
config.debug.logLevel = 2;
|
||||
config.debug.breakOnAssert = true;
|
||||
config.debug.enableProfiling = false;
|
||||
config.debug.debugFlags.clear();
|
||||
|
||||
config.input.enabled = true;
|
||||
config.input.rawMouseInput = false;
|
||||
config.input.mouseSensitivity = 1.0f;
|
||||
config.input.invertMouseY = false;
|
||||
config.input.invertMouseX = false;
|
||||
config.input.deadzone = 0.15f;
|
||||
config.input.triggerThreshold = 0.5f;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 4;
|
||||
config.input.autoConnectGamepads = true;
|
||||
|
||||
config.resource.assetRootPath = "assets";
|
||||
config.resource.cachePath = "cache";
|
||||
config.resource.savePath = "saves";
|
||||
config.resource.configPath = "config";
|
||||
config.resource.logPath = "logs";
|
||||
config.resource.useAssetCache = true;
|
||||
config.resource.maxCacheSize = 512;
|
||||
config.resource.hotReloadEnabled = false;
|
||||
config.resource.hotReloadInterval = 1.0f;
|
||||
config.resource.compressTextures = false;
|
||||
config.resource.preloadCommonAssets = true;
|
||||
config.resource.searchPaths.clear();
|
||||
|
||||
config.appName = "Extra2D App";
|
||||
config.appVersion = "1.0.0";
|
||||
config.organization = "";
|
||||
|
|
@ -172,150 +15,7 @@ AppConfig AppConfig::createDefault() {
|
|||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 验证配置的有效性
|
||||
* 检查所有配置项是否在有效范围内
|
||||
* @return 如果配置有效返回 true,否则返回 false
|
||||
*/
|
||||
bool AppConfig::validate() const {
|
||||
if (window.width <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: window width must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.height <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: window height must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.minWidth <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: minimum window width must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.minHeight <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: minimum window height must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.maxWidth > 0 && window.maxWidth < window.minWidth) {
|
||||
E2D_LOG_ERROR("Config validation failed: maximum width less than minimum width");
|
||||
return false;
|
||||
}
|
||||
if (window.maxHeight > 0 && window.maxHeight < window.minHeight) {
|
||||
E2D_LOG_ERROR("Config validation failed: maximum height less than minimum height");
|
||||
return false;
|
||||
}
|
||||
if (window.width < window.minWidth) {
|
||||
E2D_LOG_ERROR("Config validation failed: window width less than minimum");
|
||||
return false;
|
||||
}
|
||||
if (window.height < window.minHeight) {
|
||||
E2D_LOG_ERROR("Config validation failed: window height less than minimum");
|
||||
return false;
|
||||
}
|
||||
if (window.maxWidth > 0 && window.width > window.maxWidth) {
|
||||
E2D_LOG_ERROR("Config validation failed: window width exceeds maximum");
|
||||
return false;
|
||||
}
|
||||
if (window.maxHeight > 0 && window.height > window.maxHeight) {
|
||||
E2D_LOG_ERROR("Config validation failed: window height exceeds maximum");
|
||||
return false;
|
||||
}
|
||||
if (window.opacity < 0.0f || window.opacity > 1.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: window opacity must be between 0 and 1");
|
||||
return false;
|
||||
}
|
||||
if (window.contentScale <= 0.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: content scale must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (render.targetFPS < 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: target FPS cannot be negative");
|
||||
return false;
|
||||
}
|
||||
if (render.multisamples < 0 || render.multisamples > 16) {
|
||||
E2D_LOG_ERROR("Config validation failed: multisamples must be between 0 and 16");
|
||||
return false;
|
||||
}
|
||||
if (render.textureAnisotropy < 1 || render.textureAnisotropy > 16) {
|
||||
E2D_LOG_ERROR("Config validation failed: texture anisotropy must be between 1 and 16");
|
||||
return false;
|
||||
}
|
||||
if (render.spriteBatchSize <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: sprite batch size must be positive");
|
||||
return false;
|
||||
}
|
||||
if (render.maxRenderTargets <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max render targets must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!audio.isValidVolume(audio.masterVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: master volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.musicVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: music volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.sfxVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: SFX volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.voiceVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: voice volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.ambientVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: ambient volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (audio.frequency <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: audio frequency must be positive");
|
||||
return false;
|
||||
}
|
||||
if (audio.channels <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: audio channels must be positive");
|
||||
return false;
|
||||
}
|
||||
if (audio.chunkSize <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: audio chunk size must be positive");
|
||||
return false;
|
||||
}
|
||||
if (audio.maxChannels <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max audio channels must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debug.logLevel < 0 || debug.logLevel > 5) {
|
||||
E2D_LOG_ERROR("Config validation failed: log level must be between 0 and 5");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!input.isDeadzoneValid()) {
|
||||
E2D_LOG_ERROR("Config validation failed: deadzone must be between 0 and 1");
|
||||
return false;
|
||||
}
|
||||
if (input.mouseSensitivity <= 0.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: mouse sensitivity must be positive");
|
||||
return false;
|
||||
}
|
||||
if (input.triggerThreshold < 0.0f || input.triggerThreshold > 1.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: trigger threshold must be between 0 and 1");
|
||||
return false;
|
||||
}
|
||||
if (input.maxGamepads <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max gamepads must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resource.maxCacheSize <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max cache size must be positive");
|
||||
return false;
|
||||
}
|
||||
if (resource.hotReloadInterval <= 0.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: hot reload interval must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (appName.empty()) {
|
||||
E2D_LOG_ERROR("Config validation failed: app name cannot be empty");
|
||||
return false;
|
||||
|
|
@ -332,333 +32,12 @@ bool AppConfig::validate() const {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置参数
|
||||
* @param platform 平台配置接口
|
||||
*/
|
||||
void AppConfig::applyPlatformConstraints(const PlatformConfig& platform) {
|
||||
const PlatformCapabilities& caps = platform.capabilities();
|
||||
|
||||
if (!caps.supportsWindowed && window.mode == WindowMode::Windowed) {
|
||||
E2D_LOG_WARN("Platform does not support windowed mode, switching to fullscreen");
|
||||
window.mode = WindowMode::Fullscreen;
|
||||
}
|
||||
if (!caps.supportsBorderless && window.mode == WindowMode::Borderless) {
|
||||
E2D_LOG_WARN("Platform does not support borderless mode, switching to fullscreen");
|
||||
window.mode = WindowMode::Fullscreen;
|
||||
}
|
||||
if (!caps.supportsFullscreen && window.mode == WindowMode::Fullscreen) {
|
||||
E2D_LOG_WARN("Platform does not support fullscreen mode, switching to windowed");
|
||||
window.mode = WindowMode::Windowed;
|
||||
}
|
||||
|
||||
if (!caps.supportsResize) {
|
||||
window.resizable = false;
|
||||
}
|
||||
if (!caps.supportsHighDPI) {
|
||||
window.highDPI = false;
|
||||
}
|
||||
if (!caps.supportsCursor) {
|
||||
window.borderless = false;
|
||||
}
|
||||
|
||||
if (!caps.supportsVSync) {
|
||||
render.vsync = false;
|
||||
}
|
||||
|
||||
if (caps.maxTextureSize > 0) {
|
||||
if (render.maxTextureSize == 0 || render.maxTextureSize > caps.maxTextureSize) {
|
||||
render.maxTextureSize = caps.maxTextureSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (!caps.supportsGamepad) {
|
||||
input.maxGamepads = 0;
|
||||
input.enableVibration = false;
|
||||
}
|
||||
if (!caps.supportsKeyboard) {
|
||||
input.enabled = false;
|
||||
}
|
||||
if (!caps.supportsMouse) {
|
||||
input.rawMouseInput = false;
|
||||
}
|
||||
|
||||
platform.applyConstraints(*this);
|
||||
|
||||
E2D_LOG_INFO("Applied platform constraints for: {}", platform.platformName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置为默认值
|
||||
* 将所有配置项恢复为默认值
|
||||
*/
|
||||
void AppConfig::reset() {
|
||||
*this = createDefault();
|
||||
E2D_LOG_INFO("App config reset to defaults");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 合并另一个配置
|
||||
* 将 other 中的非默认值覆盖到当前配置
|
||||
* @param other 要合并的配置
|
||||
*/
|
||||
void AppConfig::merge(const AppConfig& other) {
|
||||
if (other.window.title != "Extra2D Application") {
|
||||
window.title = other.window.title;
|
||||
}
|
||||
if (other.window.width != 1280) {
|
||||
window.width = other.window.width;
|
||||
}
|
||||
if (other.window.height != 720) {
|
||||
window.height = other.window.height;
|
||||
}
|
||||
if (other.window.minWidth != 320) {
|
||||
window.minWidth = other.window.minWidth;
|
||||
}
|
||||
if (other.window.minHeight != 240) {
|
||||
window.minHeight = other.window.minHeight;
|
||||
}
|
||||
if (other.window.maxWidth != 0) {
|
||||
window.maxWidth = other.window.maxWidth;
|
||||
}
|
||||
if (other.window.maxHeight != 0) {
|
||||
window.maxHeight = other.window.maxHeight;
|
||||
}
|
||||
if (other.window.mode != WindowMode::Windowed) {
|
||||
window.mode = other.window.mode;
|
||||
}
|
||||
if (other.window.resizable != true) {
|
||||
window.resizable = other.window.resizable;
|
||||
}
|
||||
if (other.window.borderless != false) {
|
||||
window.borderless = other.window.borderless;
|
||||
}
|
||||
if (other.window.alwaysOnTop != false) {
|
||||
window.alwaysOnTop = other.window.alwaysOnTop;
|
||||
}
|
||||
if (other.window.centered != true) {
|
||||
window.centered = other.window.centered;
|
||||
}
|
||||
if (other.window.posX >= 0) {
|
||||
window.posX = other.window.posX;
|
||||
}
|
||||
if (other.window.posY >= 0) {
|
||||
window.posY = other.window.posY;
|
||||
}
|
||||
if (other.window.hideOnClose != false) {
|
||||
window.hideOnClose = other.window.hideOnClose;
|
||||
}
|
||||
if (other.window.minimizeOnClose != true) {
|
||||
window.minimizeOnClose = other.window.minimizeOnClose;
|
||||
}
|
||||
if (other.window.opacity != 1.0f) {
|
||||
window.opacity = other.window.opacity;
|
||||
}
|
||||
if (other.window.transparentFramebuffer != false) {
|
||||
window.transparentFramebuffer = other.window.transparentFramebuffer;
|
||||
}
|
||||
if (other.window.highDPI != true) {
|
||||
window.highDPI = other.window.highDPI;
|
||||
}
|
||||
if (other.window.contentScale != 1.0f) {
|
||||
window.contentScale = other.window.contentScale;
|
||||
}
|
||||
|
||||
if (other.render.backend != BackendType::OpenGL) {
|
||||
render.backend = other.render.backend;
|
||||
}
|
||||
if (other.render.targetFPS != 60) {
|
||||
render.targetFPS = other.render.targetFPS;
|
||||
}
|
||||
if (other.render.vsync != true) {
|
||||
render.vsync = other.render.vsync;
|
||||
}
|
||||
if (other.render.tripleBuffering != false) {
|
||||
render.tripleBuffering = other.render.tripleBuffering;
|
||||
}
|
||||
if (other.render.multisamples != 0) {
|
||||
render.multisamples = other.render.multisamples;
|
||||
}
|
||||
if (other.render.sRGBFramebuffer != false) {
|
||||
render.sRGBFramebuffer = other.render.sRGBFramebuffer;
|
||||
}
|
||||
if (other.render.maxTextureSize != 0) {
|
||||
render.maxTextureSize = other.render.maxTextureSize;
|
||||
}
|
||||
if (other.render.textureAnisotropy != 1) {
|
||||
render.textureAnisotropy = other.render.textureAnisotropy;
|
||||
}
|
||||
if (other.render.wireframeMode != false) {
|
||||
render.wireframeMode = other.render.wireframeMode;
|
||||
}
|
||||
if (other.render.depthTest != false) {
|
||||
render.depthTest = other.render.depthTest;
|
||||
}
|
||||
if (other.render.blending != true) {
|
||||
render.blending = other.render.blending;
|
||||
}
|
||||
if (other.render.dithering != false) {
|
||||
render.dithering = other.render.dithering;
|
||||
}
|
||||
if (other.render.spriteBatchSize != 1000) {
|
||||
render.spriteBatchSize = other.render.spriteBatchSize;
|
||||
}
|
||||
if (other.render.maxRenderTargets != 1) {
|
||||
render.maxRenderTargets = other.render.maxRenderTargets;
|
||||
}
|
||||
if (other.render.allowShaderHotReload != false) {
|
||||
render.allowShaderHotReload = other.render.allowShaderHotReload;
|
||||
}
|
||||
if (!other.render.shaderCachePath.empty()) {
|
||||
render.shaderCachePath = other.render.shaderCachePath;
|
||||
}
|
||||
|
||||
if (other.audio.enabled != true) {
|
||||
audio.enabled = other.audio.enabled;
|
||||
}
|
||||
if (other.audio.masterVolume != 100) {
|
||||
audio.masterVolume = other.audio.masterVolume;
|
||||
}
|
||||
if (other.audio.musicVolume != 100) {
|
||||
audio.musicVolume = other.audio.musicVolume;
|
||||
}
|
||||
if (other.audio.sfxVolume != 100) {
|
||||
audio.sfxVolume = other.audio.sfxVolume;
|
||||
}
|
||||
if (other.audio.voiceVolume != 100) {
|
||||
audio.voiceVolume = other.audio.voiceVolume;
|
||||
}
|
||||
if (other.audio.ambientVolume != 100) {
|
||||
audio.ambientVolume = other.audio.ambientVolume;
|
||||
}
|
||||
if (other.audio.frequency != 44100) {
|
||||
audio.frequency = other.audio.frequency;
|
||||
}
|
||||
if (other.audio.channels != 2) {
|
||||
audio.channels = other.audio.channels;
|
||||
}
|
||||
if (other.audio.chunkSize != 2048) {
|
||||
audio.chunkSize = other.audio.chunkSize;
|
||||
}
|
||||
if (other.audio.maxChannels != 16) {
|
||||
audio.maxChannels = other.audio.maxChannels;
|
||||
}
|
||||
if (other.audio.spatialAudio != false) {
|
||||
audio.spatialAudio = other.audio.spatialAudio;
|
||||
}
|
||||
|
||||
if (other.debug.enabled != false) {
|
||||
debug.enabled = other.debug.enabled;
|
||||
}
|
||||
if (other.debug.showFPS != false) {
|
||||
debug.showFPS = other.debug.showFPS;
|
||||
}
|
||||
if (other.debug.showMemoryUsage != false) {
|
||||
debug.showMemoryUsage = other.debug.showMemoryUsage;
|
||||
}
|
||||
if (other.debug.showRenderStats != false) {
|
||||
debug.showRenderStats = other.debug.showRenderStats;
|
||||
}
|
||||
if (other.debug.showColliders != false) {
|
||||
debug.showColliders = other.debug.showColliders;
|
||||
}
|
||||
if (other.debug.showGrid != false) {
|
||||
debug.showGrid = other.debug.showGrid;
|
||||
}
|
||||
if (other.debug.logToFile != false) {
|
||||
debug.logToFile = other.debug.logToFile;
|
||||
}
|
||||
if (other.debug.logToConsole != true) {
|
||||
debug.logToConsole = other.debug.logToConsole;
|
||||
}
|
||||
if (other.debug.logLevel != 2) {
|
||||
debug.logLevel = other.debug.logLevel;
|
||||
}
|
||||
if (other.debug.breakOnAssert != true) {
|
||||
debug.breakOnAssert = other.debug.breakOnAssert;
|
||||
}
|
||||
if (other.debug.enableProfiling != false) {
|
||||
debug.enableProfiling = other.debug.enableProfiling;
|
||||
}
|
||||
if (!other.debug.logFilePath.empty()) {
|
||||
debug.logFilePath = other.debug.logFilePath;
|
||||
}
|
||||
for (const auto& flag : other.debug.debugFlags) {
|
||||
debug.addDebugFlag(flag);
|
||||
}
|
||||
|
||||
if (other.input.enabled != true) {
|
||||
input.enabled = other.input.enabled;
|
||||
}
|
||||
if (other.input.rawMouseInput != false) {
|
||||
input.rawMouseInput = other.input.rawMouseInput;
|
||||
}
|
||||
if (other.input.mouseSensitivity != 1.0f) {
|
||||
input.mouseSensitivity = other.input.mouseSensitivity;
|
||||
}
|
||||
if (other.input.invertMouseY != false) {
|
||||
input.invertMouseY = other.input.invertMouseY;
|
||||
}
|
||||
if (other.input.invertMouseX != false) {
|
||||
input.invertMouseX = other.input.invertMouseX;
|
||||
}
|
||||
if (other.input.deadzone != 0.15f) {
|
||||
input.deadzone = other.input.deadzone;
|
||||
}
|
||||
if (other.input.triggerThreshold != 0.5f) {
|
||||
input.triggerThreshold = other.input.triggerThreshold;
|
||||
}
|
||||
if (other.input.enableVibration != true) {
|
||||
input.enableVibration = other.input.enableVibration;
|
||||
}
|
||||
if (other.input.maxGamepads != 4) {
|
||||
input.maxGamepads = other.input.maxGamepads;
|
||||
}
|
||||
if (other.input.autoConnectGamepads != true) {
|
||||
input.autoConnectGamepads = other.input.autoConnectGamepads;
|
||||
}
|
||||
if (!other.input.gamepadMappingFile.empty()) {
|
||||
input.gamepadMappingFile = other.input.gamepadMappingFile;
|
||||
}
|
||||
|
||||
if (other.resource.assetRootPath != "assets") {
|
||||
resource.assetRootPath = other.resource.assetRootPath;
|
||||
}
|
||||
if (other.resource.cachePath != "cache") {
|
||||
resource.cachePath = other.resource.cachePath;
|
||||
}
|
||||
if (other.resource.savePath != "saves") {
|
||||
resource.savePath = other.resource.savePath;
|
||||
}
|
||||
if (other.resource.configPath != "config") {
|
||||
resource.configPath = other.resource.configPath;
|
||||
}
|
||||
if (other.resource.logPath != "logs") {
|
||||
resource.logPath = other.resource.logPath;
|
||||
}
|
||||
if (other.resource.useAssetCache != true) {
|
||||
resource.useAssetCache = other.resource.useAssetCache;
|
||||
}
|
||||
if (other.resource.maxCacheSize != 512) {
|
||||
resource.maxCacheSize = other.resource.maxCacheSize;
|
||||
}
|
||||
if (other.resource.hotReloadEnabled != false) {
|
||||
resource.hotReloadEnabled = other.resource.hotReloadEnabled;
|
||||
}
|
||||
if (other.resource.hotReloadInterval != 1.0f) {
|
||||
resource.hotReloadInterval = other.resource.hotReloadInterval;
|
||||
}
|
||||
if (other.resource.compressTextures != false) {
|
||||
resource.compressTextures = other.resource.compressTextures;
|
||||
}
|
||||
if (other.resource.preloadCommonAssets != true) {
|
||||
resource.preloadCommonAssets = other.resource.preloadCommonAssets;
|
||||
}
|
||||
for (const auto& path : other.resource.searchPaths) {
|
||||
resource.addSearchPath(path);
|
||||
}
|
||||
|
||||
if (other.appName != "Extra2D App") {
|
||||
appName = other.appName;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#include <extra2d/config/config_loader.h>
|
||||
#include <extra2d/config/config_manager.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <fstream>
|
||||
|
|
@ -39,54 +41,6 @@ static std::string toLower(const std::string& str) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为窗口模式枚举
|
||||
* @param modeStr 窗口模式字符串
|
||||
* @return 窗口模式枚举值
|
||||
*/
|
||||
static WindowMode stringToWindowMode(const std::string& modeStr) {
|
||||
std::string lower = toLower(modeStr);
|
||||
if (lower == "fullscreen") return WindowMode::Fullscreen;
|
||||
if (lower == "borderless") return WindowMode::Borderless;
|
||||
return WindowMode::Windowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将窗口模式枚举转换为字符串
|
||||
* @param mode 窗口模式枚举值
|
||||
* @return 窗口模式字符串
|
||||
*/
|
||||
static std::string windowModeToString(WindowMode mode) {
|
||||
switch (mode) {
|
||||
case WindowMode::Fullscreen: return "fullscreen";
|
||||
case WindowMode::Borderless: return "borderless";
|
||||
default: return "windowed";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为渲染后端类型枚举
|
||||
* @param backendStr 后端类型字符串
|
||||
* @return 渲染后端类型枚举值
|
||||
*/
|
||||
static BackendType stringToBackendType(const std::string& backendStr) {
|
||||
std::string lower = toLower(backendStr);
|
||||
if (lower == "opengl") return BackendType::OpenGL;
|
||||
return BackendType::OpenGL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将渲染后端类型枚举转换为字符串
|
||||
* @param backend 渲染后端类型枚举值
|
||||
* @return 后端类型字符串
|
||||
*/
|
||||
static std::string backendTypeToString(BackendType backend) {
|
||||
switch (backend) {
|
||||
case BackendType::OpenGL: return "opengl";
|
||||
default: return "opengl";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief INI 数据存储结构
|
||||
*/
|
||||
|
|
@ -177,18 +131,8 @@ static bool hasIniValue(const IniData& data, const std::string& section, const s
|
|||
return sectionIt->second.find(key) != sectionIt->second.end();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IniConfigLoader 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 从 INI 文件加载配置
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::load(const std::string& filepath, AppConfig& config) {
|
||||
E2D_LOG_INFO("正在从 INI 文件加载配置: {}", filepath);
|
||||
E2D_LOG_INFO("正在从 INI 文件加载应用配置: {}", filepath);
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
|
|
@ -204,14 +148,8 @@ ConfigLoadResult IniConfigLoader::load(const std::string& filepath, AppConfig& c
|
|||
return loadFromString(content, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 INI 文件
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 要保存的配置对象
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult IniConfigLoader::save(const std::string& filepath, const AppConfig& config) {
|
||||
E2D_LOG_INFO("正在保存配置到 INI 文件: {}", filepath);
|
||||
E2D_LOG_INFO("正在保存应用配置到 INI 文件: {}", filepath);
|
||||
|
||||
std::string content = saveToString(config);
|
||||
|
||||
|
|
@ -228,12 +166,6 @@ ConfigSaveResult IniConfigLoader::save(const std::string& filepath, const AppCon
|
|||
return ConfigSaveResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 INI 字符串加载配置
|
||||
* @param content INI 内容字符串
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::loadFromString(const std::string& content, AppConfig& config) {
|
||||
IniData data;
|
||||
auto result = parseIniContent(content, data);
|
||||
|
|
@ -250,499 +182,21 @@ ConfigLoadResult IniConfigLoader::loadFromString(const std::string& content, App
|
|||
if (hasIniValue(data, "app", "organization")) {
|
||||
config.organization = getIniValue(data, "app", "organization");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "window", "title")) {
|
||||
config.window.title = getIniValue(data, "window", "title");
|
||||
if (hasIniValue(data, "app", "configFile")) {
|
||||
config.configFile = getIniValue(data, "app", "configFile");
|
||||
}
|
||||
if (hasIniValue(data, "window", "width")) {
|
||||
if (hasIniValue(data, "app", "targetPlatform")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "width"), value, "window.width");
|
||||
if (res.hasError()) return res;
|
||||
config.window.width = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "height")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "height"), value, "window.height");
|
||||
if (res.hasError()) return res;
|
||||
config.window.height = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "minWidth")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "minWidth"), value, "window.minWidth");
|
||||
if (res.hasError()) return res;
|
||||
config.window.minWidth = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "minHeight")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "minHeight"), value, "window.minHeight");
|
||||
if (res.hasError()) return res;
|
||||
config.window.minHeight = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "maxWidth")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "maxWidth"), value, "window.maxWidth");
|
||||
if (res.hasError()) return res;
|
||||
config.window.maxWidth = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "maxHeight")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "maxHeight"), value, "window.maxHeight");
|
||||
if (res.hasError()) return res;
|
||||
config.window.maxHeight = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "mode")) {
|
||||
config.window.mode = stringToWindowMode(getIniValue(data, "window", "mode"));
|
||||
}
|
||||
if (hasIniValue(data, "window", "resizable")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "resizable"), value, "window.resizable");
|
||||
if (res.hasError()) return res;
|
||||
config.window.resizable = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "borderless")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "borderless"), value, "window.borderless");
|
||||
if (res.hasError()) return res;
|
||||
config.window.borderless = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "alwaysOnTop")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "alwaysOnTop"), value, "window.alwaysOnTop");
|
||||
if (res.hasError()) return res;
|
||||
config.window.alwaysOnTop = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "centered")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "centered"), value, "window.centered");
|
||||
if (res.hasError()) return res;
|
||||
config.window.centered = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "posX")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "posX"), value, "window.posX");
|
||||
if (res.hasError()) return res;
|
||||
config.window.posX = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "posY")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "posY"), value, "window.posY");
|
||||
if (res.hasError()) return res;
|
||||
config.window.posY = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "hideOnClose")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "hideOnClose"), value, "window.hideOnClose");
|
||||
if (res.hasError()) return res;
|
||||
config.window.hideOnClose = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "minimizeOnClose")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "minimizeOnClose"), value, "window.minimizeOnClose");
|
||||
if (res.hasError()) return res;
|
||||
config.window.minimizeOnClose = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "opacity")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "window", "opacity"), value, "window.opacity");
|
||||
if (res.hasError()) return res;
|
||||
config.window.opacity = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "transparentFramebuffer")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "transparentFramebuffer"), value, "window.transparentFramebuffer");
|
||||
if (res.hasError()) return res;
|
||||
config.window.transparentFramebuffer = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "highDPI")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "highDPI"), value, "window.highDPI");
|
||||
if (res.hasError()) return res;
|
||||
config.window.highDPI = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "contentScale")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "window", "contentScale"), value, "window.contentScale");
|
||||
if (res.hasError()) return res;
|
||||
config.window.contentScale = value;
|
||||
auto res = parseInt(getIniValue(data, "app", "targetPlatform"), value, "app.targetPlatform");
|
||||
if (res.isOk()) {
|
||||
config.targetPlatform = static_cast<PlatformType>(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "render", "backend")) {
|
||||
config.render.backend = stringToBackendType(getIniValue(data, "render", "backend"));
|
||||
}
|
||||
if (hasIniValue(data, "render", "targetFPS")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "targetFPS"), value, "render.targetFPS");
|
||||
if (res.hasError()) return res;
|
||||
config.render.targetFPS = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "vsync")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "vsync"), value, "render.vsync");
|
||||
if (res.hasError()) return res;
|
||||
config.render.vsync = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "tripleBuffering")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "tripleBuffering"), value, "render.tripleBuffering");
|
||||
if (res.hasError()) return res;
|
||||
config.render.tripleBuffering = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "multisamples")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "multisamples"), value, "render.multisamples");
|
||||
if (res.hasError()) return res;
|
||||
config.render.multisamples = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "sRGBFramebuffer")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "sRGBFramebuffer"), value, "render.sRGBFramebuffer");
|
||||
if (res.hasError()) return res;
|
||||
config.render.sRGBFramebuffer = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorR")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorR"), value, "render.clearColorR");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.r = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorG")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorG"), value, "render.clearColorG");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.g = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorB")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorB"), value, "render.clearColorB");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.b = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorA")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorA"), value, "render.clearColorA");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.a = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "maxTextureSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "maxTextureSize"), value, "render.maxTextureSize");
|
||||
if (res.hasError()) return res;
|
||||
config.render.maxTextureSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "textureAnisotropy")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "textureAnisotropy"), value, "render.textureAnisotropy");
|
||||
if (res.hasError()) return res;
|
||||
config.render.textureAnisotropy = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "wireframeMode")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "wireframeMode"), value, "render.wireframeMode");
|
||||
if (res.hasError()) return res;
|
||||
config.render.wireframeMode = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "depthTest")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "depthTest"), value, "render.depthTest");
|
||||
if (res.hasError()) return res;
|
||||
config.render.depthTest = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "blending")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "blending"), value, "render.blending");
|
||||
if (res.hasError()) return res;
|
||||
config.render.blending = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "dithering")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "dithering"), value, "render.dithering");
|
||||
if (res.hasError()) return res;
|
||||
config.render.dithering = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "spriteBatchSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "spriteBatchSize"), value, "render.spriteBatchSize");
|
||||
if (res.hasError()) return res;
|
||||
config.render.spriteBatchSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "maxRenderTargets")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "maxRenderTargets"), value, "render.maxRenderTargets");
|
||||
if (res.hasError()) return res;
|
||||
config.render.maxRenderTargets = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "allowShaderHotReload")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "allowShaderHotReload"), value, "render.allowShaderHotReload");
|
||||
if (res.hasError()) return res;
|
||||
config.render.allowShaderHotReload = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "shaderCachePath")) {
|
||||
config.render.shaderCachePath = getIniValue(data, "render", "shaderCachePath");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "audio", "enabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "audio", "enabled"), value, "audio.enabled");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.enabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "masterVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "masterVolume"), value, "audio.masterVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.masterVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "musicVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "musicVolume"), value, "audio.musicVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.musicVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "sfxVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "sfxVolume"), value, "audio.sfxVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.sfxVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "voiceVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "voiceVolume"), value, "audio.voiceVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.voiceVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "ambientVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "ambientVolume"), value, "audio.ambientVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.ambientVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "frequency")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "frequency"), value, "audio.frequency");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.frequency = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "channels")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "channels"), value, "audio.channels");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.channels = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "chunkSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "chunkSize"), value, "audio.chunkSize");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.chunkSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "maxChannels")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "maxChannels"), value, "audio.maxChannels");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.maxChannels = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "spatialAudio")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "audio", "spatialAudio"), value, "audio.spatialAudio");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.spatialAudio = value;
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "debug", "enabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "enabled"), value, "debug.enabled");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.enabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showFPS")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showFPS"), value, "debug.showFPS");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showFPS = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showMemoryUsage")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showMemoryUsage"), value, "debug.showMemoryUsage");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showMemoryUsage = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showRenderStats")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showRenderStats"), value, "debug.showRenderStats");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showRenderStats = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showColliders")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showColliders"), value, "debug.showColliders");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showColliders = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showGrid")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showGrid"), value, "debug.showGrid");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showGrid = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logToFile")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "logToFile"), value, "debug.logToFile");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.logToFile = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logToConsole")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "logToConsole"), value, "debug.logToConsole");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.logToConsole = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logLevel")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "debug", "logLevel"), value, "debug.logLevel");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.logLevel = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "breakOnAssert")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "breakOnAssert"), value, "debug.breakOnAssert");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.breakOnAssert = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "enableProfiling")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "enableProfiling"), value, "debug.enableProfiling");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.enableProfiling = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logFilePath")) {
|
||||
config.debug.logFilePath = getIniValue(data, "debug", "logFilePath");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "input", "enabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "enabled"), value, "input.enabled");
|
||||
if (res.hasError()) return res;
|
||||
config.input.enabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "rawMouseInput")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "rawMouseInput"), value, "input.rawMouseInput");
|
||||
if (res.hasError()) return res;
|
||||
config.input.rawMouseInput = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "mouseSensitivity")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "input", "mouseSensitivity"), value, "input.mouseSensitivity");
|
||||
if (res.hasError()) return res;
|
||||
config.input.mouseSensitivity = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "invertMouseY")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "invertMouseY"), value, "input.invertMouseY");
|
||||
if (res.hasError()) return res;
|
||||
config.input.invertMouseY = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "invertMouseX")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "invertMouseX"), value, "input.invertMouseX");
|
||||
if (res.hasError()) return res;
|
||||
config.input.invertMouseX = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "deadzone")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "input", "deadzone"), value, "input.deadzone");
|
||||
if (res.hasError()) return res;
|
||||
config.input.deadzone = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "triggerThreshold")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "input", "triggerThreshold"), value, "input.triggerThreshold");
|
||||
if (res.hasError()) return res;
|
||||
config.input.triggerThreshold = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "enableVibration")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "enableVibration"), value, "input.enableVibration");
|
||||
if (res.hasError()) return res;
|
||||
config.input.enableVibration = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "maxGamepads")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "input", "maxGamepads"), value, "input.maxGamepads");
|
||||
if (res.hasError()) return res;
|
||||
config.input.maxGamepads = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "autoConnectGamepads")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "autoConnectGamepads"), value, "input.autoConnectGamepads");
|
||||
if (res.hasError()) return res;
|
||||
config.input.autoConnectGamepads = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "gamepadMappingFile")) {
|
||||
config.input.gamepadMappingFile = getIniValue(data, "input", "gamepadMappingFile");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "resource", "assetRootPath")) {
|
||||
config.resource.assetRootPath = getIniValue(data, "resource", "assetRootPath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "cachePath")) {
|
||||
config.resource.cachePath = getIniValue(data, "resource", "cachePath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "savePath")) {
|
||||
config.resource.savePath = getIniValue(data, "resource", "savePath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "configPath")) {
|
||||
config.resource.configPath = getIniValue(data, "resource", "configPath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "logPath")) {
|
||||
config.resource.logPath = getIniValue(data, "resource", "logPath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "useAssetCache")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "useAssetCache"), value, "resource.useAssetCache");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.useAssetCache = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "maxCacheSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "resource", "maxCacheSize"), value, "resource.maxCacheSize");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.maxCacheSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "hotReloadEnabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "hotReloadEnabled"), value, "resource.hotReloadEnabled");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.hotReloadEnabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "hotReloadInterval")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "resource", "hotReloadInterval"), value, "resource.hotReloadInterval");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.hotReloadInterval = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "compressTextures")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "compressTextures"), value, "resource.compressTextures");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.compressTextures = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "preloadCommonAssets")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "preloadCommonAssets"), value, "resource.preloadCommonAssets");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.preloadCommonAssets = value;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("INI 配置加载成功");
|
||||
E2D_LOG_INFO("INI 应用配置加载成功");
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将配置序列化为 INI 字符串
|
||||
* @param config 配置对象
|
||||
* @return 序列化后的 INI 字符串
|
||||
*/
|
||||
std::string IniConfigLoader::saveToString(const AppConfig& config) {
|
||||
std::ostringstream oss;
|
||||
|
||||
|
|
@ -750,118 +204,84 @@ std::string IniConfigLoader::saveToString(const AppConfig& config) {
|
|||
oss << "name=" << config.appName << "\n";
|
||||
oss << "version=" << config.appVersion << "\n";
|
||||
oss << "organization=" << config.organization << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[window]\n";
|
||||
oss << "title=" << config.window.title << "\n";
|
||||
oss << "width=" << config.window.width << "\n";
|
||||
oss << "height=" << config.window.height << "\n";
|
||||
oss << "minWidth=" << config.window.minWidth << "\n";
|
||||
oss << "minHeight=" << config.window.minHeight << "\n";
|
||||
oss << "maxWidth=" << config.window.maxWidth << "\n";
|
||||
oss << "maxHeight=" << config.window.maxHeight << "\n";
|
||||
oss << "mode=" << windowModeToString(config.window.mode) << "\n";
|
||||
oss << "resizable=" << (config.window.resizable ? "true" : "false") << "\n";
|
||||
oss << "borderless=" << (config.window.borderless ? "true" : "false") << "\n";
|
||||
oss << "alwaysOnTop=" << (config.window.alwaysOnTop ? "true" : "false") << "\n";
|
||||
oss << "centered=" << (config.window.centered ? "true" : "false") << "\n";
|
||||
oss << "posX=" << config.window.posX << "\n";
|
||||
oss << "posY=" << config.window.posY << "\n";
|
||||
oss << "hideOnClose=" << (config.window.hideOnClose ? "true" : "false") << "\n";
|
||||
oss << "minimizeOnClose=" << (config.window.minimizeOnClose ? "true" : "false") << "\n";
|
||||
oss << "opacity=" << config.window.opacity << "\n";
|
||||
oss << "transparentFramebuffer=" << (config.window.transparentFramebuffer ? "true" : "false") << "\n";
|
||||
oss << "highDPI=" << (config.window.highDPI ? "true" : "false") << "\n";
|
||||
oss << "contentScale=" << config.window.contentScale << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[render]\n";
|
||||
oss << "backend=" << backendTypeToString(config.render.backend) << "\n";
|
||||
oss << "targetFPS=" << config.render.targetFPS << "\n";
|
||||
oss << "vsync=" << (config.render.vsync ? "true" : "false") << "\n";
|
||||
oss << "tripleBuffering=" << (config.render.tripleBuffering ? "true" : "false") << "\n";
|
||||
oss << "multisamples=" << config.render.multisamples << "\n";
|
||||
oss << "sRGBFramebuffer=" << (config.render.sRGBFramebuffer ? "true" : "false") << "\n";
|
||||
oss << "clearColorR=" << config.render.clearColor.r << "\n";
|
||||
oss << "clearColorG=" << config.render.clearColor.g << "\n";
|
||||
oss << "clearColorB=" << config.render.clearColor.b << "\n";
|
||||
oss << "clearColorA=" << config.render.clearColor.a << "\n";
|
||||
oss << "maxTextureSize=" << config.render.maxTextureSize << "\n";
|
||||
oss << "textureAnisotropy=" << config.render.textureAnisotropy << "\n";
|
||||
oss << "wireframeMode=" << (config.render.wireframeMode ? "true" : "false") << "\n";
|
||||
oss << "depthTest=" << (config.render.depthTest ? "true" : "false") << "\n";
|
||||
oss << "blending=" << (config.render.blending ? "true" : "false") << "\n";
|
||||
oss << "dithering=" << (config.render.dithering ? "true" : "false") << "\n";
|
||||
oss << "spriteBatchSize=" << config.render.spriteBatchSize << "\n";
|
||||
oss << "maxRenderTargets=" << config.render.maxRenderTargets << "\n";
|
||||
oss << "allowShaderHotReload=" << (config.render.allowShaderHotReload ? "true" : "false") << "\n";
|
||||
oss << "shaderCachePath=" << config.render.shaderCachePath << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[audio]\n";
|
||||
oss << "enabled=" << (config.audio.enabled ? "true" : "false") << "\n";
|
||||
oss << "masterVolume=" << config.audio.masterVolume << "\n";
|
||||
oss << "musicVolume=" << config.audio.musicVolume << "\n";
|
||||
oss << "sfxVolume=" << config.audio.sfxVolume << "\n";
|
||||
oss << "voiceVolume=" << config.audio.voiceVolume << "\n";
|
||||
oss << "ambientVolume=" << config.audio.ambientVolume << "\n";
|
||||
oss << "frequency=" << config.audio.frequency << "\n";
|
||||
oss << "channels=" << config.audio.channels << "\n";
|
||||
oss << "chunkSize=" << config.audio.chunkSize << "\n";
|
||||
oss << "maxChannels=" << config.audio.maxChannels << "\n";
|
||||
oss << "spatialAudio=" << (config.audio.spatialAudio ? "true" : "false") << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[debug]\n";
|
||||
oss << "enabled=" << (config.debug.enabled ? "true" : "false") << "\n";
|
||||
oss << "showFPS=" << (config.debug.showFPS ? "true" : "false") << "\n";
|
||||
oss << "showMemoryUsage=" << (config.debug.showMemoryUsage ? "true" : "false") << "\n";
|
||||
oss << "showRenderStats=" << (config.debug.showRenderStats ? "true" : "false") << "\n";
|
||||
oss << "showColliders=" << (config.debug.showColliders ? "true" : "false") << "\n";
|
||||
oss << "showGrid=" << (config.debug.showGrid ? "true" : "false") << "\n";
|
||||
oss << "logToFile=" << (config.debug.logToFile ? "true" : "false") << "\n";
|
||||
oss << "logToConsole=" << (config.debug.logToConsole ? "true" : "false") << "\n";
|
||||
oss << "logLevel=" << config.debug.logLevel << "\n";
|
||||
oss << "breakOnAssert=" << (config.debug.breakOnAssert ? "true" : "false") << "\n";
|
||||
oss << "enableProfiling=" << (config.debug.enableProfiling ? "true" : "false") << "\n";
|
||||
oss << "logFilePath=" << config.debug.logFilePath << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[input]\n";
|
||||
oss << "enabled=" << (config.input.enabled ? "true" : "false") << "\n";
|
||||
oss << "rawMouseInput=" << (config.input.rawMouseInput ? "true" : "false") << "\n";
|
||||
oss << "mouseSensitivity=" << config.input.mouseSensitivity << "\n";
|
||||
oss << "invertMouseY=" << (config.input.invertMouseY ? "true" : "false") << "\n";
|
||||
oss << "invertMouseX=" << (config.input.invertMouseX ? "true" : "false") << "\n";
|
||||
oss << "deadzone=" << config.input.deadzone << "\n";
|
||||
oss << "triggerThreshold=" << config.input.triggerThreshold << "\n";
|
||||
oss << "enableVibration=" << (config.input.enableVibration ? "true" : "false") << "\n";
|
||||
oss << "maxGamepads=" << config.input.maxGamepads << "\n";
|
||||
oss << "autoConnectGamepads=" << (config.input.autoConnectGamepads ? "true" : "false") << "\n";
|
||||
oss << "gamepadMappingFile=" << config.input.gamepadMappingFile << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[resource]\n";
|
||||
oss << "assetRootPath=" << config.resource.assetRootPath << "\n";
|
||||
oss << "cachePath=" << config.resource.cachePath << "\n";
|
||||
oss << "savePath=" << config.resource.savePath << "\n";
|
||||
oss << "configPath=" << config.resource.configPath << "\n";
|
||||
oss << "logPath=" << config.resource.logPath << "\n";
|
||||
oss << "useAssetCache=" << (config.resource.useAssetCache ? "true" : "false") << "\n";
|
||||
oss << "maxCacheSize=" << config.resource.maxCacheSize << "\n";
|
||||
oss << "hotReloadEnabled=" << (config.resource.hotReloadEnabled ? "true" : "false") << "\n";
|
||||
oss << "hotReloadInterval=" << config.resource.hotReloadInterval << "\n";
|
||||
oss << "compressTextures=" << (config.resource.compressTextures ? "true" : "false") << "\n";
|
||||
oss << "preloadCommonAssets=" << (config.resource.preloadCommonAssets ? "true" : "false") << "\n";
|
||||
oss << "configFile=" << config.configFile << "\n";
|
||||
oss << "targetPlatform=" << static_cast<int>(config.targetPlatform) << "\n";
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持指定文件
|
||||
* @param filepath 文件路径
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::loadWithModules(const std::string& filepath) {
|
||||
E2D_LOG_INFO("正在从 INI 文件加载完整配置(含模块): {}", filepath);
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法打开配置文件: {}", filepath);
|
||||
return ConfigLoadResult::error("无法打开配置文件: " + filepath);
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string content = buffer.str();
|
||||
file.close();
|
||||
|
||||
IniData data;
|
||||
auto result = parseIniContent(content, data);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto& registry = ModuleRegistry::instance();
|
||||
auto moduleIds = registry.getAllModules();
|
||||
|
||||
for (ModuleId moduleId : moduleIds) {
|
||||
IModuleConfig* moduleConfig = registry.getModuleConfig(moduleId);
|
||||
if (!moduleConfig) continue;
|
||||
|
||||
std::string sectionName = moduleConfig->getConfigSectionName();
|
||||
if (sectionName.empty()) continue;
|
||||
|
||||
if (data.find(sectionName) != data.end()) {
|
||||
E2D_LOG_DEBUG("加载模块 {} 的 INI 配置", moduleConfig->getModuleInfo().name);
|
||||
}
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("完整配置加载成功");
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
ConfigSaveResult IniConfigLoader::saveWithModules(const std::string& filepath) {
|
||||
E2D_LOG_INFO("正在保存完整配置(含模块)到 INI 文件: {}", filepath);
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << saveToString(ConfigManager::instance().appConfig());
|
||||
|
||||
auto& registry = ModuleRegistry::instance();
|
||||
auto moduleIds = registry.getAllModules();
|
||||
|
||||
for (ModuleId moduleId : moduleIds) {
|
||||
IModuleConfig* moduleConfig = registry.getModuleConfig(moduleId);
|
||||
if (!moduleConfig) continue;
|
||||
|
||||
std::string sectionName = moduleConfig->getConfigSectionName();
|
||||
if (sectionName.empty()) continue;
|
||||
|
||||
oss << "\n[" << sectionName << "]\n";
|
||||
}
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法创建配置文件: {}", filepath);
|
||||
return ConfigSaveResult::error("无法创建配置文件: " + filepath);
|
||||
}
|
||||
|
||||
file << oss.str();
|
||||
file.close();
|
||||
|
||||
E2D_LOG_INFO("完整配置已成功保存到: {}", filepath);
|
||||
return ConfigSaveResult::ok();
|
||||
}
|
||||
|
||||
bool IniConfigLoader::supportsFile(const std::string& filepath) const {
|
||||
if (filepath.length() >= 4) {
|
||||
std::string ext = filepath.substr(filepath.length() - 4);
|
||||
|
|
@ -871,31 +291,14 @@ bool IniConfigLoader::supportsFile(const std::string& filepath) const {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 克隆加载器实例
|
||||
* @return 新的加载器实例
|
||||
*/
|
||||
UniquePtr<ConfigLoader> IniConfigLoader::clone() const {
|
||||
return makeUnique<IniConfigLoader>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 生成节键组合字符串
|
||||
* @param section 节名
|
||||
* @param key 键名
|
||||
* @return 组合字符串
|
||||
*/
|
||||
std::string IniConfigLoader::sectionKey(const std::string& section, const std::string& key) const {
|
||||
return section + "." + key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析整数
|
||||
* @param value 字符串值
|
||||
* @param result 输出整数结果
|
||||
* @param fieldName 字段名(用于错误报告)
|
||||
* @return 解析结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::parseInt(const std::string& value, int& result, const std::string& fieldName) {
|
||||
try {
|
||||
size_t pos;
|
||||
|
|
@ -909,13 +312,6 @@ ConfigLoadResult IniConfigLoader::parseInt(const std::string& value, int& result
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析浮点数
|
||||
* @param value 字符串值
|
||||
* @param result 输出浮点数结果
|
||||
* @param fieldName 字段名(用于错误报告)
|
||||
* @return 解析结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::parseFloat(const std::string& value, float& result, const std::string& fieldName) {
|
||||
try {
|
||||
size_t pos;
|
||||
|
|
@ -929,13 +325,6 @@ ConfigLoadResult IniConfigLoader::parseFloat(const std::string& value, float& re
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析布尔值
|
||||
* @param value 字符串值
|
||||
* @param result 输出布尔结果
|
||||
* @param fieldName 字段名(用于错误报告)
|
||||
* @return 解析结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::parseBool(const std::string& value, bool& result, const std::string& fieldName) {
|
||||
std::string lower = toLower(value);
|
||||
if (lower == "true" || lower == "1" || lower == "yes" || lower == "on") {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include <extra2d/config/config_loader.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
|
@ -9,64 +10,8 @@ using json = nlohmann::json;
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为窗口模式枚举
|
||||
* @param modeStr 窗口模式字符串
|
||||
* @return 窗口模式枚举值
|
||||
*/
|
||||
static WindowMode stringToWindowMode(const std::string& modeStr) {
|
||||
if (modeStr == "fullscreen") return WindowMode::Fullscreen;
|
||||
if (modeStr == "borderless") return WindowMode::Borderless;
|
||||
return WindowMode::Windowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将窗口模式枚举转换为字符串
|
||||
* @param mode 窗口模式枚举值
|
||||
* @return 窗口模式字符串
|
||||
*/
|
||||
static std::string windowModeToString(WindowMode mode) {
|
||||
switch (mode) {
|
||||
case WindowMode::Fullscreen: return "fullscreen";
|
||||
case WindowMode::Borderless: return "borderless";
|
||||
default: return "windowed";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为渲染后端类型枚举
|
||||
* @param backendStr 后端类型字符串
|
||||
* @return 渲染后端类型枚举值
|
||||
*/
|
||||
static BackendType stringToBackendType(const std::string& backendStr) {
|
||||
if (backendStr == "opengl") return BackendType::OpenGL;
|
||||
return BackendType::OpenGL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将渲染后端类型枚举转换为字符串
|
||||
* @param backend 渲染后端类型枚举值
|
||||
* @return 后端类型字符串
|
||||
*/
|
||||
static std::string backendTypeToString(BackendType backend) {
|
||||
switch (backend) {
|
||||
case BackendType::OpenGL: return "opengl";
|
||||
default: return "opengl";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JsonConfigLoader 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 文件加载配置
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::load(const std::string& filepath, AppConfig& config) {
|
||||
E2D_LOG_INFO("正在从 JSON 文件加载配置: {}", filepath);
|
||||
E2D_LOG_INFO("正在从 JSON 文件加载应用配置: {}", filepath);
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
|
|
@ -82,14 +27,8 @@ ConfigLoadResult JsonConfigLoader::load(const std::string& filepath, AppConfig&
|
|||
return loadFromString(content, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 文件
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 要保存的配置对象
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult JsonConfigLoader::save(const std::string& filepath, const AppConfig& config) {
|
||||
E2D_LOG_INFO("正在保存配置到 JSON 文件: {}", filepath);
|
||||
E2D_LOG_INFO("正在保存应用配置到 JSON 文件: {}", filepath);
|
||||
|
||||
std::string content = saveToString(config);
|
||||
|
||||
|
|
@ -106,12 +45,6 @@ ConfigSaveResult JsonConfigLoader::save(const std::string& filepath, const AppCo
|
|||
return ConfigSaveResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 字符串加载配置
|
||||
* @param content JSON 内容字符串
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::loadFromString(const std::string& content, AppConfig& config) {
|
||||
json root;
|
||||
|
||||
|
|
@ -135,79 +68,110 @@ ConfigLoadResult JsonConfigLoader::loadFromString(const std::string& content, Ap
|
|||
config.organization = root["organization"].get<std::string>();
|
||||
}
|
||||
|
||||
if (root.contains("window")) {
|
||||
auto result = parseWindowConfig(&root["window"], config.window);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
if (root.contains("configFile") && root["configFile"].is_string()) {
|
||||
config.configFile = root["configFile"].get<std::string>();
|
||||
}
|
||||
|
||||
if (root.contains("render")) {
|
||||
auto result = parseRenderConfig(&root["render"], config.render);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
if (root.contains("targetPlatform") && root["targetPlatform"].is_number_integer()) {
|
||||
config.targetPlatform = static_cast<PlatformType>(root["targetPlatform"].get<int>());
|
||||
}
|
||||
|
||||
if (root.contains("audio")) {
|
||||
auto result = parseAudioConfig(&root["audio"], config.audio);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("debug")) {
|
||||
auto result = parseDebugConfig(&root["debug"], config.debug);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("input")) {
|
||||
auto result = parseInputConfig(&root["input"], config.input);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("resource")) {
|
||||
auto result = parseResourceConfig(&root["resource"], config.resource);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("JSON 配置加载成功");
|
||||
E2D_LOG_INFO("JSON 应用配置加载成功");
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将配置序列化为 JSON 字符串
|
||||
* @param config 配置对象
|
||||
* @return 序列化后的 JSON 字符串
|
||||
*/
|
||||
std::string JsonConfigLoader::saveToString(const AppConfig& config) {
|
||||
json root;
|
||||
|
||||
root["appName"] = config.appName;
|
||||
root["appVersion"] = config.appVersion;
|
||||
root["organization"] = config.organization;
|
||||
|
||||
serializeWindowConfig(&root, config.window);
|
||||
serializeRenderConfig(&root, config.render);
|
||||
serializeAudioConfig(&root, config.audio);
|
||||
serializeDebugConfig(&root, config.debug);
|
||||
serializeInputConfig(&root, config.input);
|
||||
serializeResourceConfig(&root, config.resource);
|
||||
root["configFile"] = config.configFile;
|
||||
root["targetPlatform"] = static_cast<int>(config.targetPlatform);
|
||||
|
||||
return root.dump(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持指定文件
|
||||
* @param filepath 文件路径
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::loadWithModules(const std::string& filepath) {
|
||||
E2D_LOG_INFO("正在从 JSON 文件加载完整配置(含模块): {}", filepath);
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法打开配置文件: {}", filepath);
|
||||
return ConfigLoadResult::error("无法打开配置文件: " + filepath);
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string content = buffer.str();
|
||||
file.close();
|
||||
|
||||
json root;
|
||||
try {
|
||||
root = json::parse(content);
|
||||
} catch (const json::parse_error& e) {
|
||||
E2D_LOG_ERROR("JSON 解析错误: {}", e.what());
|
||||
return ConfigLoadResult::error(std::string("JSON 解析错误: ") + e.what(),
|
||||
static_cast<int>(e.byte));
|
||||
}
|
||||
|
||||
auto& registry = ModuleRegistry::instance();
|
||||
auto moduleIds = registry.getAllModules();
|
||||
|
||||
for (ModuleId moduleId : moduleIds) {
|
||||
IModuleConfig* moduleConfig = registry.getModuleConfig(moduleId);
|
||||
if (!moduleConfig) continue;
|
||||
|
||||
std::string sectionName = moduleConfig->getConfigSectionName();
|
||||
if (sectionName.empty()) continue;
|
||||
|
||||
if (root.contains(sectionName)) {
|
||||
if (!moduleConfig->loadFromJson(&root[sectionName])) {
|
||||
E2D_LOG_WARN("模块 {} 配置加载失败", moduleConfig->getModuleInfo().name);
|
||||
} else {
|
||||
E2D_LOG_DEBUG("模块 {} 配置加载成功", moduleConfig->getModuleInfo().name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("完整配置加载成功");
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
ConfigSaveResult JsonConfigLoader::saveWithModules(const std::string& filepath) {
|
||||
E2D_LOG_INFO("正在保存完整配置(含模块)到 JSON 文件: {}", filepath);
|
||||
|
||||
json root;
|
||||
|
||||
auto& registry = ModuleRegistry::instance();
|
||||
auto moduleIds = registry.getAllModules();
|
||||
|
||||
for (ModuleId moduleId : moduleIds) {
|
||||
IModuleConfig* moduleConfig = registry.getModuleConfig(moduleId);
|
||||
if (!moduleConfig) continue;
|
||||
|
||||
std::string sectionName = moduleConfig->getConfigSectionName();
|
||||
if (sectionName.empty()) continue;
|
||||
|
||||
json sectionJson;
|
||||
if (moduleConfig->saveToJson(§ionJson)) {
|
||||
root[sectionName] = sectionJson;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法创建配置文件: {}", filepath);
|
||||
return ConfigSaveResult::error("无法创建配置文件: " + filepath);
|
||||
}
|
||||
|
||||
file << root.dump(4);
|
||||
file.close();
|
||||
|
||||
E2D_LOG_INFO("完整配置已成功保存到: {}", filepath);
|
||||
return ConfigSaveResult::ok();
|
||||
}
|
||||
|
||||
bool JsonConfigLoader::supportsFile(const std::string& filepath) const {
|
||||
if (filepath.length() >= 5) {
|
||||
std::string ext = filepath.substr(filepath.length() - 5);
|
||||
|
|
@ -217,547 +181,8 @@ bool JsonConfigLoader::supportsFile(const std::string& filepath) const {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 克隆加载器实例
|
||||
* @return 新的加载器实例
|
||||
*/
|
||||
UniquePtr<ConfigLoader> JsonConfigLoader::clone() const {
|
||||
return makeUnique<JsonConfigLoader>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析窗口配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param window 输出的窗口配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseWindowConfig(const void* jsonValue, WindowConfigData& window) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("window 配置必须是一个对象", -1, "window");
|
||||
}
|
||||
|
||||
if (obj.contains("title") && obj["title"].is_string()) {
|
||||
window.title = obj["title"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("width") && obj["width"].is_number_integer()) {
|
||||
window.width = obj["width"].get<int>();
|
||||
}
|
||||
if (obj.contains("height") && obj["height"].is_number_integer()) {
|
||||
window.height = obj["height"].get<int>();
|
||||
}
|
||||
if (obj.contains("minWidth") && obj["minWidth"].is_number_integer()) {
|
||||
window.minWidth = obj["minWidth"].get<int>();
|
||||
}
|
||||
if (obj.contains("minHeight") && obj["minHeight"].is_number_integer()) {
|
||||
window.minHeight = obj["minHeight"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxWidth") && obj["maxWidth"].is_number_integer()) {
|
||||
window.maxWidth = obj["maxWidth"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxHeight") && obj["maxHeight"].is_number_integer()) {
|
||||
window.maxHeight = obj["maxHeight"].get<int>();
|
||||
}
|
||||
if (obj.contains("mode") && obj["mode"].is_string()) {
|
||||
window.mode = stringToWindowMode(obj["mode"].get<std::string>());
|
||||
}
|
||||
if (obj.contains("resizable") && obj["resizable"].is_boolean()) {
|
||||
window.resizable = obj["resizable"].get<bool>();
|
||||
}
|
||||
if (obj.contains("borderless") && obj["borderless"].is_boolean()) {
|
||||
window.borderless = obj["borderless"].get<bool>();
|
||||
}
|
||||
if (obj.contains("alwaysOnTop") && obj["alwaysOnTop"].is_boolean()) {
|
||||
window.alwaysOnTop = obj["alwaysOnTop"].get<bool>();
|
||||
}
|
||||
if (obj.contains("centered") && obj["centered"].is_boolean()) {
|
||||
window.centered = obj["centered"].get<bool>();
|
||||
}
|
||||
if (obj.contains("posX") && obj["posX"].is_number_integer()) {
|
||||
window.posX = obj["posX"].get<int>();
|
||||
}
|
||||
if (obj.contains("posY") && obj["posY"].is_number_integer()) {
|
||||
window.posY = obj["posY"].get<int>();
|
||||
}
|
||||
if (obj.contains("hideOnClose") && obj["hideOnClose"].is_boolean()) {
|
||||
window.hideOnClose = obj["hideOnClose"].get<bool>();
|
||||
}
|
||||
if (obj.contains("minimizeOnClose") && obj["minimizeOnClose"].is_boolean()) {
|
||||
window.minimizeOnClose = obj["minimizeOnClose"].get<bool>();
|
||||
}
|
||||
if (obj.contains("opacity") && obj["opacity"].is_number()) {
|
||||
window.opacity = obj["opacity"].get<float>();
|
||||
}
|
||||
if (obj.contains("transparentFramebuffer") && obj["transparentFramebuffer"].is_boolean()) {
|
||||
window.transparentFramebuffer = obj["transparentFramebuffer"].get<bool>();
|
||||
}
|
||||
if (obj.contains("highDPI") && obj["highDPI"].is_boolean()) {
|
||||
window.highDPI = obj["highDPI"].get<bool>();
|
||||
}
|
||||
if (obj.contains("contentScale") && obj["contentScale"].is_number()) {
|
||||
window.contentScale = obj["contentScale"].get<float>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析渲染配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param render 输出的渲染配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseRenderConfig(const void* jsonValue, RenderConfigData& render) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("render 配置必须是一个对象", -1, "render");
|
||||
}
|
||||
|
||||
if (obj.contains("backend") && obj["backend"].is_string()) {
|
||||
render.backend = stringToBackendType(obj["backend"].get<std::string>());
|
||||
}
|
||||
if (obj.contains("targetFPS") && obj["targetFPS"].is_number_integer()) {
|
||||
render.targetFPS = obj["targetFPS"].get<int>();
|
||||
}
|
||||
if (obj.contains("vsync") && obj["vsync"].is_boolean()) {
|
||||
render.vsync = obj["vsync"].get<bool>();
|
||||
}
|
||||
if (obj.contains("tripleBuffering") && obj["tripleBuffering"].is_boolean()) {
|
||||
render.tripleBuffering = obj["tripleBuffering"].get<bool>();
|
||||
}
|
||||
if (obj.contains("multisamples") && obj["multisamples"].is_number_integer()) {
|
||||
render.multisamples = obj["multisamples"].get<int>();
|
||||
}
|
||||
if (obj.contains("sRGBFramebuffer") && obj["sRGBFramebuffer"].is_boolean()) {
|
||||
render.sRGBFramebuffer = obj["sRGBFramebuffer"].get<bool>();
|
||||
}
|
||||
if (obj.contains("clearColor") && obj["clearColor"].is_array() && obj["clearColor"].size() >= 4) {
|
||||
render.clearColor.r = obj["clearColor"][0].get<float>();
|
||||
render.clearColor.g = obj["clearColor"][1].get<float>();
|
||||
render.clearColor.b = obj["clearColor"][2].get<float>();
|
||||
render.clearColor.a = obj["clearColor"][3].get<float>();
|
||||
}
|
||||
if (obj.contains("maxTextureSize") && obj["maxTextureSize"].is_number_integer()) {
|
||||
render.maxTextureSize = obj["maxTextureSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("textureAnisotropy") && obj["textureAnisotropy"].is_number_integer()) {
|
||||
render.textureAnisotropy = obj["textureAnisotropy"].get<int>();
|
||||
}
|
||||
if (obj.contains("wireframeMode") && obj["wireframeMode"].is_boolean()) {
|
||||
render.wireframeMode = obj["wireframeMode"].get<bool>();
|
||||
}
|
||||
if (obj.contains("depthTest") && obj["depthTest"].is_boolean()) {
|
||||
render.depthTest = obj["depthTest"].get<bool>();
|
||||
}
|
||||
if (obj.contains("blending") && obj["blending"].is_boolean()) {
|
||||
render.blending = obj["blending"].get<bool>();
|
||||
}
|
||||
if (obj.contains("dithering") && obj["dithering"].is_boolean()) {
|
||||
render.dithering = obj["dithering"].get<bool>();
|
||||
}
|
||||
if (obj.contains("spriteBatchSize") && obj["spriteBatchSize"].is_number_integer()) {
|
||||
render.spriteBatchSize = obj["spriteBatchSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxRenderTargets") && obj["maxRenderTargets"].is_number_integer()) {
|
||||
render.maxRenderTargets = obj["maxRenderTargets"].get<int>();
|
||||
}
|
||||
if (obj.contains("allowShaderHotReload") && obj["allowShaderHotReload"].is_boolean()) {
|
||||
render.allowShaderHotReload = obj["allowShaderHotReload"].get<bool>();
|
||||
}
|
||||
if (obj.contains("shaderCachePath") && obj["shaderCachePath"].is_string()) {
|
||||
render.shaderCachePath = obj["shaderCachePath"].get<std::string>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析音频配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param audio 输出的音频配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseAudioConfig(const void* jsonValue, AudioConfigData& audio) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("audio 配置必须是一个对象", -1, "audio");
|
||||
}
|
||||
|
||||
if (obj.contains("enabled") && obj["enabled"].is_boolean()) {
|
||||
audio.enabled = obj["enabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("masterVolume") && obj["masterVolume"].is_number_integer()) {
|
||||
audio.masterVolume = obj["masterVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("musicVolume") && obj["musicVolume"].is_number_integer()) {
|
||||
audio.musicVolume = obj["musicVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("sfxVolume") && obj["sfxVolume"].is_number_integer()) {
|
||||
audio.sfxVolume = obj["sfxVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("voiceVolume") && obj["voiceVolume"].is_number_integer()) {
|
||||
audio.voiceVolume = obj["voiceVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("ambientVolume") && obj["ambientVolume"].is_number_integer()) {
|
||||
audio.ambientVolume = obj["ambientVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("frequency") && obj["frequency"].is_number_integer()) {
|
||||
audio.frequency = obj["frequency"].get<int>();
|
||||
}
|
||||
if (obj.contains("channels") && obj["channels"].is_number_integer()) {
|
||||
audio.channels = obj["channels"].get<int>();
|
||||
}
|
||||
if (obj.contains("chunkSize") && obj["chunkSize"].is_number_integer()) {
|
||||
audio.chunkSize = obj["chunkSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxChannels") && obj["maxChannels"].is_number_integer()) {
|
||||
audio.maxChannels = obj["maxChannels"].get<int>();
|
||||
}
|
||||
if (obj.contains("spatialAudio") && obj["spatialAudio"].is_boolean()) {
|
||||
audio.spatialAudio = obj["spatialAudio"].get<bool>();
|
||||
}
|
||||
if (obj.contains("listenerPosition") && obj["listenerPosition"].is_array() && obj["listenerPosition"].size() >= 3) {
|
||||
audio.listenerPosition[0] = obj["listenerPosition"][0].get<float>();
|
||||
audio.listenerPosition[1] = obj["listenerPosition"][1].get<float>();
|
||||
audio.listenerPosition[2] = obj["listenerPosition"][2].get<float>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析调试配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param debug 输出的调试配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseDebugConfig(const void* jsonValue, DebugConfigData& debug) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("debug 配置必须是一个对象", -1, "debug");
|
||||
}
|
||||
|
||||
if (obj.contains("enabled") && obj["enabled"].is_boolean()) {
|
||||
debug.enabled = obj["enabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showFPS") && obj["showFPS"].is_boolean()) {
|
||||
debug.showFPS = obj["showFPS"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showMemoryUsage") && obj["showMemoryUsage"].is_boolean()) {
|
||||
debug.showMemoryUsage = obj["showMemoryUsage"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showRenderStats") && obj["showRenderStats"].is_boolean()) {
|
||||
debug.showRenderStats = obj["showRenderStats"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showColliders") && obj["showColliders"].is_boolean()) {
|
||||
debug.showColliders = obj["showColliders"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showGrid") && obj["showGrid"].is_boolean()) {
|
||||
debug.showGrid = obj["showGrid"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logToFile") && obj["logToFile"].is_boolean()) {
|
||||
debug.logToFile = obj["logToFile"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logToConsole") && obj["logToConsole"].is_boolean()) {
|
||||
debug.logToConsole = obj["logToConsole"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logLevel") && obj["logLevel"].is_number_integer()) {
|
||||
debug.logLevel = obj["logLevel"].get<int>();
|
||||
}
|
||||
if (obj.contains("breakOnAssert") && obj["breakOnAssert"].is_boolean()) {
|
||||
debug.breakOnAssert = obj["breakOnAssert"].get<bool>();
|
||||
}
|
||||
if (obj.contains("enableProfiling") && obj["enableProfiling"].is_boolean()) {
|
||||
debug.enableProfiling = obj["enableProfiling"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logFilePath") && obj["logFilePath"].is_string()) {
|
||||
debug.logFilePath = obj["logFilePath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("debugFlags") && obj["debugFlags"].is_array()) {
|
||||
debug.debugFlags.clear();
|
||||
for (const auto& flag : obj["debugFlags"]) {
|
||||
if (flag.is_string()) {
|
||||
debug.debugFlags.push_back(flag.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析输入配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param input 输出的输入配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseInputConfig(const void* jsonValue, InputConfigData& input) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("input 配置必须是一个对象", -1, "input");
|
||||
}
|
||||
|
||||
if (obj.contains("enabled") && obj["enabled"].is_boolean()) {
|
||||
input.enabled = obj["enabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("rawMouseInput") && obj["rawMouseInput"].is_boolean()) {
|
||||
input.rawMouseInput = obj["rawMouseInput"].get<bool>();
|
||||
}
|
||||
if (obj.contains("mouseSensitivity") && obj["mouseSensitivity"].is_number()) {
|
||||
input.mouseSensitivity = obj["mouseSensitivity"].get<float>();
|
||||
}
|
||||
if (obj.contains("invertMouseY") && obj["invertMouseY"].is_boolean()) {
|
||||
input.invertMouseY = obj["invertMouseY"].get<bool>();
|
||||
}
|
||||
if (obj.contains("invertMouseX") && obj["invertMouseX"].is_boolean()) {
|
||||
input.invertMouseX = obj["invertMouseX"].get<bool>();
|
||||
}
|
||||
if (obj.contains("deadzone") && obj["deadzone"].is_number()) {
|
||||
input.deadzone = obj["deadzone"].get<float>();
|
||||
}
|
||||
if (obj.contains("triggerThreshold") && obj["triggerThreshold"].is_number()) {
|
||||
input.triggerThreshold = obj["triggerThreshold"].get<float>();
|
||||
}
|
||||
if (obj.contains("enableVibration") && obj["enableVibration"].is_boolean()) {
|
||||
input.enableVibration = obj["enableVibration"].get<bool>();
|
||||
}
|
||||
if (obj.contains("maxGamepads") && obj["maxGamepads"].is_number_integer()) {
|
||||
input.maxGamepads = obj["maxGamepads"].get<int>();
|
||||
}
|
||||
if (obj.contains("autoConnectGamepads") && obj["autoConnectGamepads"].is_boolean()) {
|
||||
input.autoConnectGamepads = obj["autoConnectGamepads"].get<bool>();
|
||||
}
|
||||
if (obj.contains("gamepadMappingFile") && obj["gamepadMappingFile"].is_string()) {
|
||||
input.gamepadMappingFile = obj["gamepadMappingFile"].get<std::string>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析资源配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param resource 输出的资源配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseResourceConfig(const void* jsonValue, ResourceConfigData& resource) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("resource 配置必须是一个对象", -1, "resource");
|
||||
}
|
||||
|
||||
if (obj.contains("assetRootPath") && obj["assetRootPath"].is_string()) {
|
||||
resource.assetRootPath = obj["assetRootPath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("cachePath") && obj["cachePath"].is_string()) {
|
||||
resource.cachePath = obj["cachePath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("savePath") && obj["savePath"].is_string()) {
|
||||
resource.savePath = obj["savePath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("configPath") && obj["configPath"].is_string()) {
|
||||
resource.configPath = obj["configPath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("logPath") && obj["logPath"].is_string()) {
|
||||
resource.logPath = obj["logPath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("useAssetCache") && obj["useAssetCache"].is_boolean()) {
|
||||
resource.useAssetCache = obj["useAssetCache"].get<bool>();
|
||||
}
|
||||
if (obj.contains("maxCacheSize") && obj["maxCacheSize"].is_number_integer()) {
|
||||
resource.maxCacheSize = obj["maxCacheSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("hotReloadEnabled") && obj["hotReloadEnabled"].is_boolean()) {
|
||||
resource.hotReloadEnabled = obj["hotReloadEnabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("hotReloadInterval") && obj["hotReloadInterval"].is_number()) {
|
||||
resource.hotReloadInterval = obj["hotReloadInterval"].get<float>();
|
||||
}
|
||||
if (obj.contains("compressTextures") && obj["compressTextures"].is_boolean()) {
|
||||
resource.compressTextures = obj["compressTextures"].get<bool>();
|
||||
}
|
||||
if (obj.contains("preloadCommonAssets") && obj["preloadCommonAssets"].is_boolean()) {
|
||||
resource.preloadCommonAssets = obj["preloadCommonAssets"].get<bool>();
|
||||
}
|
||||
if (obj.contains("searchPaths") && obj["searchPaths"].is_array()) {
|
||||
resource.searchPaths.clear();
|
||||
for (const auto& path : obj["searchPaths"]) {
|
||||
if (path.is_string()) {
|
||||
resource.searchPaths.push_back(path.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化窗口配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param window 窗口配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeWindowConfig(void* jsonValue, const WindowConfigData& window) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["title"] = window.title;
|
||||
obj["width"] = window.width;
|
||||
obj["height"] = window.height;
|
||||
obj["minWidth"] = window.minWidth;
|
||||
obj["minHeight"] = window.minHeight;
|
||||
obj["maxWidth"] = window.maxWidth;
|
||||
obj["maxHeight"] = window.maxHeight;
|
||||
obj["mode"] = windowModeToString(window.mode);
|
||||
obj["resizable"] = window.resizable;
|
||||
obj["borderless"] = window.borderless;
|
||||
obj["alwaysOnTop"] = window.alwaysOnTop;
|
||||
obj["centered"] = window.centered;
|
||||
obj["posX"] = window.posX;
|
||||
obj["posY"] = window.posY;
|
||||
obj["hideOnClose"] = window.hideOnClose;
|
||||
obj["minimizeOnClose"] = window.minimizeOnClose;
|
||||
obj["opacity"] = window.opacity;
|
||||
obj["transparentFramebuffer"] = window.transparentFramebuffer;
|
||||
obj["highDPI"] = window.highDPI;
|
||||
obj["contentScale"] = window.contentScale;
|
||||
|
||||
root["window"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化渲染配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param render 渲染配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeRenderConfig(void* jsonValue, const RenderConfigData& render) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["backend"] = backendTypeToString(render.backend);
|
||||
obj["targetFPS"] = render.targetFPS;
|
||||
obj["vsync"] = render.vsync;
|
||||
obj["tripleBuffering"] = render.tripleBuffering;
|
||||
obj["multisamples"] = render.multisamples;
|
||||
obj["sRGBFramebuffer"] = render.sRGBFramebuffer;
|
||||
obj["clearColor"] = {render.clearColor.r, render.clearColor.g, render.clearColor.b, render.clearColor.a};
|
||||
obj["maxTextureSize"] = render.maxTextureSize;
|
||||
obj["textureAnisotropy"] = render.textureAnisotropy;
|
||||
obj["wireframeMode"] = render.wireframeMode;
|
||||
obj["depthTest"] = render.depthTest;
|
||||
obj["blending"] = render.blending;
|
||||
obj["dithering"] = render.dithering;
|
||||
obj["spriteBatchSize"] = render.spriteBatchSize;
|
||||
obj["maxRenderTargets"] = render.maxRenderTargets;
|
||||
obj["allowShaderHotReload"] = render.allowShaderHotReload;
|
||||
obj["shaderCachePath"] = render.shaderCachePath;
|
||||
|
||||
root["render"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化音频配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param audio 音频配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeAudioConfig(void* jsonValue, const AudioConfigData& audio) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["enabled"] = audio.enabled;
|
||||
obj["masterVolume"] = audio.masterVolume;
|
||||
obj["musicVolume"] = audio.musicVolume;
|
||||
obj["sfxVolume"] = audio.sfxVolume;
|
||||
obj["voiceVolume"] = audio.voiceVolume;
|
||||
obj["ambientVolume"] = audio.ambientVolume;
|
||||
obj["frequency"] = audio.frequency;
|
||||
obj["channels"] = audio.channels;
|
||||
obj["chunkSize"] = audio.chunkSize;
|
||||
obj["maxChannels"] = audio.maxChannels;
|
||||
obj["spatialAudio"] = audio.spatialAudio;
|
||||
obj["listenerPosition"] = {audio.listenerPosition[0], audio.listenerPosition[1], audio.listenerPosition[2]};
|
||||
|
||||
root["audio"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化调试配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param debug 调试配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeDebugConfig(void* jsonValue, const DebugConfigData& debug) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["enabled"] = debug.enabled;
|
||||
obj["showFPS"] = debug.showFPS;
|
||||
obj["showMemoryUsage"] = debug.showMemoryUsage;
|
||||
obj["showRenderStats"] = debug.showRenderStats;
|
||||
obj["showColliders"] = debug.showColliders;
|
||||
obj["showGrid"] = debug.showGrid;
|
||||
obj["logToFile"] = debug.logToFile;
|
||||
obj["logToConsole"] = debug.logToConsole;
|
||||
obj["logLevel"] = debug.logLevel;
|
||||
obj["breakOnAssert"] = debug.breakOnAssert;
|
||||
obj["enableProfiling"] = debug.enableProfiling;
|
||||
obj["logFilePath"] = debug.logFilePath;
|
||||
obj["debugFlags"] = debug.debugFlags;
|
||||
|
||||
root["debug"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化输入配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param input 输入配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeInputConfig(void* jsonValue, const InputConfigData& input) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["enabled"] = input.enabled;
|
||||
obj["rawMouseInput"] = input.rawMouseInput;
|
||||
obj["mouseSensitivity"] = input.mouseSensitivity;
|
||||
obj["invertMouseY"] = input.invertMouseY;
|
||||
obj["invertMouseX"] = input.invertMouseX;
|
||||
obj["deadzone"] = input.deadzone;
|
||||
obj["triggerThreshold"] = input.triggerThreshold;
|
||||
obj["enableVibration"] = input.enableVibration;
|
||||
obj["maxGamepads"] = input.maxGamepads;
|
||||
obj["autoConnectGamepads"] = input.autoConnectGamepads;
|
||||
obj["gamepadMappingFile"] = input.gamepadMappingFile;
|
||||
|
||||
root["input"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化资源配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param resource 资源配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeResourceConfig(void* jsonValue, const ResourceConfigData& resource) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["assetRootPath"] = resource.assetRootPath;
|
||||
obj["cachePath"] = resource.cachePath;
|
||||
obj["savePath"] = resource.savePath;
|
||||
obj["configPath"] = resource.configPath;
|
||||
obj["logPath"] = resource.logPath;
|
||||
obj["useAssetCache"] = resource.useAssetCache;
|
||||
obj["maxCacheSize"] = resource.maxCacheSize;
|
||||
obj["hotReloadEnabled"] = resource.hotReloadEnabled;
|
||||
obj["hotReloadInterval"] = resource.hotReloadInterval;
|
||||
obj["compressTextures"] = resource.compressTextures;
|
||||
obj["preloadCommonAssets"] = resource.preloadCommonAssets;
|
||||
obj["searchPaths"] = resource.searchPaths;
|
||||
|
||||
root["resource"] = obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 构造函数
|
||||
* 初始化配置管理器的成员变量
|
||||
*/
|
||||
ConfigManager::ConfigManager()
|
||||
: m_nextCallbackId(1)
|
||||
, m_autoSaveEnabled(false)
|
||||
|
|
@ -19,32 +15,17 @@ ConfigManager::ConfigManager()
|
|||
m_appConfig = AppConfig::createDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
* 确保在销毁时关闭配置管理器
|
||||
*/
|
||||
ConfigManager::~ConfigManager() {
|
||||
if (m_initialized) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* 使用静态局部变量实现线程安全的单例模式
|
||||
* @return 配置管理器实例引用
|
||||
*/
|
||||
ConfigManager& ConfigManager::instance() {
|
||||
static ConfigManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化配置管理器
|
||||
* 创建平台配置和配置加载器
|
||||
* @param configPath 配置文件路径
|
||||
* @return 如果初始化成功返回 true
|
||||
*/
|
||||
bool ConfigManager::initialize(const std::string& configPath) {
|
||||
if (m_initialized) {
|
||||
E2D_LOG_WARN("ConfigManager already initialized");
|
||||
|
|
@ -70,8 +51,6 @@ bool ConfigManager::initialize(const std::string& configPath) {
|
|||
m_appConfig = AppConfig::createDefault();
|
||||
m_appConfig.targetPlatform = PlatformDetector::detect();
|
||||
|
||||
m_platformConfig->applyDefaults(m_appConfig);
|
||||
|
||||
m_initialized = true;
|
||||
m_modified = false;
|
||||
|
||||
|
|
@ -80,10 +59,6 @@ bool ConfigManager::initialize(const std::string& configPath) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关闭配置管理器
|
||||
* 清理所有资源并重置状态
|
||||
*/
|
||||
void ConfigManager::shutdown() {
|
||||
if (!m_initialized) {
|
||||
return;
|
||||
|
|
@ -96,7 +71,7 @@ void ConfigManager::shutdown() {
|
|||
}
|
||||
|
||||
m_changeCallbacks.clear();
|
||||
m_moduleConfigs.clear();
|
||||
m_rawValues.clear();
|
||||
m_loader.reset();
|
||||
m_platformConfig.reset();
|
||||
|
||||
|
|
@ -106,19 +81,10 @@ void ConfigManager::shutdown() {
|
|||
E2D_LOG_INFO("ConfigManager shutdown complete");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否已初始化
|
||||
* @return 如果已初始化返回 true
|
||||
*/
|
||||
bool ConfigManager::isInitialized() const {
|
||||
return m_initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载配置文件
|
||||
* @param filepath 配置文件路径(可选,默认使用初始化时的路径)
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult ConfigManager::loadConfig(const std::string& filepath) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
|
|
@ -137,10 +103,6 @@ ConfigLoadResult ConfigManager::loadConfig(const std::string& filepath) {
|
|||
if (result.success) {
|
||||
m_appConfig.merge(loadedConfig);
|
||||
|
||||
if (m_platformConfig) {
|
||||
m_appConfig.applyPlatformConstraints(*m_platformConfig);
|
||||
}
|
||||
|
||||
if (!m_appConfig.validate()) {
|
||||
E2D_LOG_WARN("Loaded config validation failed, using defaults");
|
||||
m_appConfig = AppConfig::createDefault();
|
||||
|
|
@ -157,11 +119,6 @@ ConfigLoadResult ConfigManager::loadConfig(const std::string& filepath) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到文件
|
||||
* @param filepath 配置文件路径(可选,默认使用初始化时的路径)
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult ConfigManager::saveConfig(const std::string& filepath) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
|
|
@ -188,35 +145,69 @@ ConfigSaveResult ConfigManager::saveConfig(const std::string& filepath) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重新加载配置
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult ConfigManager::loadConfigWithModules(const std::string& filepath) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string path = filepath.empty() ? m_configPath : filepath;
|
||||
if (path.empty()) {
|
||||
return ConfigLoadResult::error("No config file path specified");
|
||||
}
|
||||
|
||||
if (!m_loader) {
|
||||
return ConfigLoadResult::error("Config loader not initialized");
|
||||
}
|
||||
|
||||
ConfigLoadResult result = m_loader->loadWithModules(path);
|
||||
|
||||
if (result.success) {
|
||||
m_configPath = path;
|
||||
m_modified = false;
|
||||
E2D_LOG_INFO("Full config (with modules) loaded from: {}", path);
|
||||
} else {
|
||||
E2D_LOG_ERROR("Failed to load full config from {}: {}", path, result.errorMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ConfigSaveResult ConfigManager::saveConfigWithModules(const std::string& filepath) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string path = filepath.empty() ? m_configPath : filepath;
|
||||
if (path.empty()) {
|
||||
return ConfigSaveResult::error("No config file path specified");
|
||||
}
|
||||
|
||||
if (!m_loader) {
|
||||
return ConfigSaveResult::error("Config loader not initialized");
|
||||
}
|
||||
|
||||
ConfigSaveResult result = m_loader->saveWithModules(path);
|
||||
|
||||
if (result.success) {
|
||||
m_configPath = path;
|
||||
m_modified = false;
|
||||
E2D_LOG_INFO("Full config (with modules) saved to: {}", path);
|
||||
} else {
|
||||
E2D_LOG_ERROR("Failed to save full config to {}: {}", path, result.errorMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ConfigLoadResult ConfigManager::reload() {
|
||||
return loadConfig(m_configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取应用配置
|
||||
* @return 应用配置的常量引用
|
||||
*/
|
||||
const AppConfig& ConfigManager::appConfig() const {
|
||||
return m_appConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取可修改的应用配置
|
||||
* @return 应用配置的引用
|
||||
*/
|
||||
AppConfig& ConfigManager::appConfig() {
|
||||
m_modified = true;
|
||||
return m_appConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置应用配置
|
||||
* @param config 新的配置
|
||||
*/
|
||||
void ConfigManager::setAppConfig(const AppConfig& config) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
|
|
@ -226,27 +217,14 @@ void ConfigManager::setAppConfig(const AppConfig& config) {
|
|||
E2D_LOG_INFO("App config updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台配置
|
||||
* @return 平台配置接口指针
|
||||
*/
|
||||
PlatformConfig* ConfigManager::platformConfig() {
|
||||
return m_platformConfig.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台配置(常量版本)
|
||||
* @return 平台配置接口常量指针
|
||||
*/
|
||||
const PlatformConfig* ConfigManager::platformConfig() const {
|
||||
return m_platformConfig.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册配置变更回调
|
||||
* @param callback 回调函数
|
||||
* @return 回调ID,用于取消注册
|
||||
*/
|
||||
int ConfigManager::registerChangeCallback(ConfigChangeCallback callback) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
|
|
@ -257,10 +235,6 @@ int ConfigManager::registerChangeCallback(ConfigChangeCallback callback) {
|
|||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 取消注册配置变更回调
|
||||
* @param callbackId 回调ID
|
||||
*/
|
||||
void ConfigManager::unregisterChangeCallback(int callbackId) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
|
|
@ -271,192 +245,111 @@ void ConfigManager::unregisterChangeCallback(int callbackId) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除所有变更回调
|
||||
*/
|
||||
void ConfigManager::clearChangeCallbacks() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_changeCallbacks.clear();
|
||||
E2D_LOG_DEBUG("Cleared all config change callbacks");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册模块配置
|
||||
* @param moduleName 模块名称
|
||||
* @param config 模块配置指针
|
||||
*/
|
||||
void ConfigManager::registerModuleConfig(const std::string& moduleName, Ptr<void> config) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
m_moduleConfigs[moduleName] = config;
|
||||
E2D_LOG_DEBUG("Registered module config: {}", moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除模块配置
|
||||
* @param moduleName 模块名称
|
||||
*/
|
||||
void ConfigManager::removeModuleConfig(const std::string& moduleName) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto it = m_moduleConfigs.find(moduleName);
|
||||
if (it != m_moduleConfigs.end()) {
|
||||
m_moduleConfigs.erase(it);
|
||||
E2D_LOG_DEBUG("Removed module config: {}", moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查模块配置是否存在
|
||||
* @param moduleName 模块名称
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool ConfigManager::hasModuleConfig(const std::string& moduleName) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_moduleConfigs.find(moduleName) != m_moduleConfigs.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(字符串)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, const std::string& value) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string fullKey = section + "." + key;
|
||||
|
||||
ConfigChangeEvent event;
|
||||
event.section = section;
|
||||
event.field = key;
|
||||
event.oldValue = m_rawValues[fullKey];
|
||||
event.newValue = value;
|
||||
|
||||
m_rawValues[fullKey] = value;
|
||||
m_modified = true;
|
||||
notifyChangeCallbacks(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(整数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, int value) {
|
||||
std::ostringstream oss;
|
||||
oss << value;
|
||||
setValue(section, key, oss.str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(浮点数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, float value) {
|
||||
std::ostringstream oss;
|
||||
oss << value;
|
||||
setValue(section, key, oss.str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(布尔值)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, bool value) {
|
||||
setValue(section, key, std::string(value ? "true" : "false"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(字符串)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
std::string ConfigManager::getValue(const std::string& section, const std::string& key,
|
||||
const std::string& defaultValue) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string fullKey = section + "." + key;
|
||||
auto it = m_rawValues.find(fullKey);
|
||||
if (it != m_rawValues.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(整数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
int ConfigManager::getIntValue(const std::string& section, const std::string& key, int defaultValue) const {
|
||||
std::string value = getValue(section, key);
|
||||
if (!value.empty()) {
|
||||
try {
|
||||
return std::stoi(value);
|
||||
} catch (...) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(浮点数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
float ConfigManager::getFloatValue(const std::string& section, const std::string& key, float defaultValue) const {
|
||||
std::string value = getValue(section, key);
|
||||
if (!value.empty()) {
|
||||
try {
|
||||
return std::stof(value);
|
||||
} catch (...) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(布尔值)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
bool ConfigManager::getBoolValue(const std::string& section, const std::string& key, bool defaultValue) const {
|
||||
std::string value = getValue(section, key);
|
||||
if (!value.empty()) {
|
||||
if (value == "true" || value == "1") return true;
|
||||
if (value == "false" || value == "0") return false;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置配置到默认值
|
||||
*/
|
||||
void ConfigManager::resetToDefaults() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
m_appConfig = AppConfig::createDefault();
|
||||
|
||||
if (m_platformConfig) {
|
||||
m_platformConfig->applyDefaults(m_appConfig);
|
||||
}
|
||||
|
||||
m_rawValues.clear();
|
||||
m_modified = true;
|
||||
|
||||
E2D_LOG_INFO("Config reset to defaults");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查配置是否有未保存的更改
|
||||
* @return 如果有未保存的更改返回 true
|
||||
*/
|
||||
bool ConfigManager::hasUnsavedChanges() const {
|
||||
return m_modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 标记配置为已修改
|
||||
*/
|
||||
void ConfigManager::markModified() {
|
||||
m_modified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除修改标记
|
||||
*/
|
||||
void ConfigManager::clearModified() {
|
||||
m_modified = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置自动保存
|
||||
* @param enabled 是否启用自动保存
|
||||
* @param interval 自动保存间隔(秒)
|
||||
*/
|
||||
void ConfigManager::setAutoSave(bool enabled, float interval) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
|
|
@ -468,10 +361,6 @@ void ConfigManager::setAutoSave(bool enabled, float interval) {
|
|||
enabled ? "enabled" : "disabled", m_autoSaveInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新配置管理器(用于自动保存)
|
||||
* @param deltaTime 帧时间(秒)
|
||||
*/
|
||||
void ConfigManager::update(float deltaTime) {
|
||||
if (!m_autoSaveEnabled || !m_modified) {
|
||||
return;
|
||||
|
|
@ -485,10 +374,6 @@ void ConfigManager::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 通知所有变更回调
|
||||
* @param event 配置变更事件
|
||||
*/
|
||||
void ConfigManager::notifyChangeCallbacks(const ConfigChangeEvent& event) {
|
||||
for (const auto& pair : m_changeCallbacks) {
|
||||
if (pair.second) {
|
||||
|
|
@ -497,20 +382,4 @@ void ConfigManager::notifyChangeCallbacks(const ConfigChangeEvent& event) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将配置应用到内部存储
|
||||
* @param config 配置对象
|
||||
*/
|
||||
void ConfigManager::applyConfigToInternal(const AppConfig& config) {
|
||||
m_appConfig = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从内部存储提取配置
|
||||
* @param config 输出配置对象
|
||||
*/
|
||||
void ConfigManager::extractConfigFromInternal(AppConfig& config) const {
|
||||
config = m_appConfig;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,32 +42,6 @@ public:
|
|||
const char *platformName() const override { return "Windows"; }
|
||||
const PlatformCapabilities &capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig &config) const override {
|
||||
if (config.window.width < 320)
|
||||
config.window.width = 320;
|
||||
if (config.window.height < 240)
|
||||
config.window.height = 240;
|
||||
if (config.window.width > caps_.maxTextureSize)
|
||||
config.window.width = caps_.maxTextureSize;
|
||||
if (config.window.height > caps_.maxTextureSize)
|
||||
config.window.height = caps_.maxTextureSize;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig &config) const override {
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig &config) const override {
|
||||
if (config.window.width <= 0 || config.window.height <= 0) {
|
||||
E2D_LOG_ERROR("Windows: Invalid window dimensions");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
|
|
@ -107,26 +81,6 @@ public:
|
|||
const char *platformName() const override { return "Linux"; }
|
||||
const PlatformCapabilities &capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig &config) const override {
|
||||
if (config.window.width < 320)
|
||||
config.window.width = 320;
|
||||
if (config.window.height < 240)
|
||||
config.window.height = 240;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig &config) const override {
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig &config) const override {
|
||||
if (config.window.width <= 0 || config.window.height <= 0) {
|
||||
E2D_LOG_ERROR("Linux: Invalid window dimensions");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
|
|
@ -165,27 +119,6 @@ public:
|
|||
const char *platformName() const override { return "macOS"; }
|
||||
const PlatformCapabilities &capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig &config) const override {
|
||||
if (config.window.width < 320)
|
||||
config.window.width = 320;
|
||||
if (config.window.height < 240)
|
||||
config.window.height = 240;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig &config) const override {
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig &config) const override {
|
||||
if (config.window.width <= 0 || config.window.height <= 0) {
|
||||
E2D_LOG_ERROR("macOS: Invalid window dimensions");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
|
|
@ -196,7 +129,6 @@ private:
|
|||
PlatformCapabilities caps_;
|
||||
};
|
||||
|
||||
#ifdef __SWITCH__
|
||||
class SwitchPlatformConfig : public PlatformConfig {
|
||||
public:
|
||||
SwitchPlatformConfig() {
|
||||
|
|
@ -225,37 +157,6 @@ public:
|
|||
const char *platformName() const override { return "Nintendo Switch"; }
|
||||
const PlatformCapabilities &capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig &config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.window.borderless = false;
|
||||
config.window.highDPI = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 2;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig &config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig &config) const override {
|
||||
if (config.window.mode != WindowMode::Fullscreen) {
|
||||
E2D_LOG_WARN("Switch: Only fullscreen mode is supported");
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
|
|
@ -266,85 +167,9 @@ public:
|
|||
private:
|
||||
PlatformCapabilities caps_;
|
||||
};
|
||||
#else
|
||||
class SwitchPlatformConfig : public PlatformConfig {
|
||||
public:
|
||||
SwitchPlatformConfig() {
|
||||
caps_.supportsWindowed = false;
|
||||
caps_.supportsFullscreen = true;
|
||||
caps_.supportsBorderless = false;
|
||||
caps_.supportsCursor = false;
|
||||
caps_.supportsCursorHide = false;
|
||||
caps_.supportsDPIAwareness = false;
|
||||
caps_.supportsVSync = true;
|
||||
caps_.supportsMultiMonitor = false;
|
||||
caps_.supportsClipboard = false;
|
||||
caps_.supportsGamepad = true;
|
||||
caps_.supportsTouch = true;
|
||||
caps_.supportsKeyboard = false;
|
||||
caps_.supportsMouse = false;
|
||||
caps_.supportsResize = false;
|
||||
caps_.supportsHighDPI = false;
|
||||
caps_.maxTextureSize = 8192;
|
||||
caps_.preferredScreenWidth = 1920;
|
||||
caps_.preferredScreenHeight = 1080;
|
||||
caps_.defaultDPI = 96.0f;
|
||||
}
|
||||
|
||||
PlatformType platformType() const override { return PlatformType::Switch; }
|
||||
const char *platformName() const override { return "Nintendo Switch"; }
|
||||
const PlatformCapabilities &capabilities() const override { return caps_; }
|
||||
}
|
||||
|
||||
void applyConstraints(AppConfig &config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.window.borderless = false;
|
||||
config.window.highDPI = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 2;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig &config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig &config) const override {
|
||||
if (config.window.mode != WindowMode::Fullscreen) {
|
||||
E2D_LOG_WARN("Switch: Only fullscreen mode is supported");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
return (width == 1920 && height == 1080) ||
|
||||
(width == 1280 && height == 720);
|
||||
}
|
||||
|
||||
private:
|
||||
PlatformCapabilities caps_;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 创建平台配置实例
|
||||
* 根据 PlatformType 创建对应的平台配置对象
|
||||
* @param type 平台类型,默认为 Auto(自动检测)
|
||||
* @return 平台配置的智能指针
|
||||
*/
|
||||
UniquePtr<PlatformConfig> createPlatformConfig(PlatformType type) {
|
||||
if (type == PlatformType::Auto) {
|
||||
#ifdef _WIN32
|
||||
|
|
@ -379,12 +204,6 @@ UniquePtr<PlatformConfig> createPlatformConfig(PlatformType type) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台类型名称
|
||||
* 将 PlatformType 枚举转换为可读的字符串
|
||||
* @param type 平台类型枚举值
|
||||
* @return 平台名称字符串
|
||||
*/
|
||||
const char *getPlatformTypeName(PlatformType type) {
|
||||
switch (type) {
|
||||
case PlatformType::Auto:
|
||||
|
|
@ -402,4 +221,4 @@ const char *getPlatformTypeName(PlatformType type) {
|
|||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -657,39 +657,21 @@ PlatformCapabilities PlatformDetector::getSwitchCapabilities() {
|
|||
|
||||
AppConfig PlatformDetector::getWindowsDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
return config;
|
||||
}
|
||||
|
||||
AppConfig PlatformDetector::getLinuxDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
AppConfig PlatformDetector::getMacOSDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
AppConfig PlatformDetector::getSwitchDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.window.highDPI = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 2;
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
#include <extra2d/debug/debug_config.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
bool DebugConfigData::hasDebugFlag(const std::string& flag) const {
|
||||
return std::find(debugFlags.begin(), debugFlags.end(), flag) != debugFlags.end();
|
||||
}
|
||||
|
||||
void DebugConfigData::addDebugFlag(const std::string& flag) {
|
||||
if (!hasDebugFlag(flag)) {
|
||||
debugFlags.push_back(flag);
|
||||
}
|
||||
}
|
||||
|
||||
void DebugConfigData::removeDebugFlag(const std::string& flag) {
|
||||
auto it = std::find(debugFlags.begin(), debugFlags.end(), flag);
|
||||
if (it != debugFlags.end()) {
|
||||
debugFlags.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,55 +18,227 @@ SDL2Input::~SDL2Input() {
|
|||
}
|
||||
|
||||
void SDL2Input::init() {
|
||||
SDL_GameControllerEventState(SDL_ENABLE);
|
||||
E2D_LOG_INFO("SDL2Input initialized");
|
||||
|
||||
if (SDL_Init(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
E2D_LOG_WARN("Failed to init gamecontroller subsystem: {}", SDL_GetError());
|
||||
}
|
||||
|
||||
openGamepad();
|
||||
E2D_LOG_DEBUG("SDL2 input initialized");
|
||||
}
|
||||
|
||||
void SDL2Input::shutdown() {
|
||||
closeGamepad();
|
||||
E2D_LOG_INFO("SDL2Input shutdown");
|
||||
}
|
||||
|
||||
void SDL2Input::update() {
|
||||
keyPrevious_ = keyCurrent_;
|
||||
mousePrevious_ = mouseCurrent_;
|
||||
gamepadPrevious_ = gamepadCurrent_;
|
||||
scrollDelta_ = 0.0f;
|
||||
mouseDelta_ = Vec2::Zero();
|
||||
|
||||
updateKeyboard();
|
||||
updateMouse();
|
||||
scrollDelta_ = 0.0f;
|
||||
mouseDelta_ = Vec2{0.0f, 0.0f};
|
||||
|
||||
updateGamepad();
|
||||
}
|
||||
|
||||
void SDL2Input::setEventCallback(EventCallback callback) {
|
||||
eventCallback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void SDL2Input::handleSDLEvent(const SDL_Event& event) {
|
||||
switch (event.type) {
|
||||
case SDL_KEYDOWN: {
|
||||
int key = event.key.keysym.scancode;
|
||||
if (key >= 0 && key < static_cast<int>(Key::Count)) {
|
||||
if (!keyCurrent_[key]) {
|
||||
keyCurrent_[key] = true;
|
||||
|
||||
Event e = Event::createKeyPress(
|
||||
event.key.keysym.sym,
|
||||
event.key.keysym.scancode,
|
||||
event.key.keysym.mod
|
||||
);
|
||||
dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_KEYUP: {
|
||||
int key = event.key.keysym.scancode;
|
||||
if (key >= 0 && key < static_cast<int>(Key::Count)) {
|
||||
keyCurrent_[key] = false;
|
||||
|
||||
Event e = Event::createKeyRelease(
|
||||
event.key.keysym.sym,
|
||||
event.key.keysym.scancode,
|
||||
event.key.keysym.mod
|
||||
);
|
||||
dispatchEvent(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN: {
|
||||
int btn = event.button.button - 1;
|
||||
if (btn >= 0 && btn < static_cast<int>(Mouse::Count)) {
|
||||
mouseCurrent_[btn] = true;
|
||||
|
||||
Vec2 pos{static_cast<float>(event.button.x),
|
||||
static_cast<float>(event.button.y)};
|
||||
Event e = Event::createMouseButtonPress(btn, 0, pos);
|
||||
dispatchEvent(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_MOUSEBUTTONUP: {
|
||||
int btn = event.button.button - 1;
|
||||
if (btn >= 0 && btn < static_cast<int>(Mouse::Count)) {
|
||||
mouseCurrent_[btn] = false;
|
||||
|
||||
Vec2 pos{static_cast<float>(event.button.x),
|
||||
static_cast<float>(event.button.y)};
|
||||
Event e = Event::createMouseButtonRelease(btn, 0, pos);
|
||||
dispatchEvent(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_MOUSEMOTION: {
|
||||
Vec2 newPos{static_cast<float>(event.motion.x),
|
||||
static_cast<float>(event.motion.y)};
|
||||
Vec2 delta{static_cast<float>(event.motion.xrel),
|
||||
static_cast<float>(event.motion.yrel)};
|
||||
|
||||
mouseDelta_ = mouseDelta_ + delta;
|
||||
mousePos_ = newPos;
|
||||
|
||||
Event e = Event::createMouseMove(newPos, delta);
|
||||
dispatchEvent(e);
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_MOUSEWHEEL: {
|
||||
Vec2 offset{static_cast<float>(event.wheel.x),
|
||||
static_cast<float>(event.wheel.y)};
|
||||
Vec2 pos = mousePos_;
|
||||
|
||||
scroll_ += event.wheel.y;
|
||||
scrollDelta_ += event.wheel.y;
|
||||
|
||||
Event e = Event::createMouseScroll(offset, pos);
|
||||
dispatchEvent(e);
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
E2D_LOG_INFO("Gamepad connected: index {}", event.cdevice.which);
|
||||
openGamepad();
|
||||
break;
|
||||
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
if (gamepad_ && event.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(gamepad_))) {
|
||||
E2D_LOG_INFO("Gamepad disconnected");
|
||||
closeGamepad();
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
if (gamepad_) {
|
||||
int btn = event.cbutton.button;
|
||||
if (btn >= 0 && btn < static_cast<int>(Gamepad::Count)) {
|
||||
gamepadCurrent_[btn] = true;
|
||||
|
||||
GamepadButtonEvent btnEvent;
|
||||
btnEvent.gamepadId = gamepadIndex_;
|
||||
btnEvent.button = btn;
|
||||
|
||||
Event e;
|
||||
e.type = EventType::GamepadButtonPressed;
|
||||
e.data = btnEvent;
|
||||
dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_CONTROLLERBUTTONUP:
|
||||
if (gamepad_) {
|
||||
int btn = event.cbutton.button;
|
||||
if (btn >= 0 && btn < static_cast<int>(Gamepad::Count)) {
|
||||
gamepadCurrent_[btn] = false;
|
||||
|
||||
GamepadButtonEvent btnEvent;
|
||||
btnEvent.gamepadId = gamepadIndex_;
|
||||
btnEvent.button = btn;
|
||||
|
||||
Event e;
|
||||
e.type = EventType::GamepadButtonReleased;
|
||||
e.data = btnEvent;
|
||||
dispatchEvent(e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SDL2Input::dispatchEvent(const Event& event) {
|
||||
if (eventCallback_) {
|
||||
eventCallback_(event);
|
||||
}
|
||||
}
|
||||
|
||||
bool SDL2Input::down(Key key) const {
|
||||
size_t idx = static_cast<size_t>(key);
|
||||
return idx < keyCurrent_.size() ? keyCurrent_[idx] : false;
|
||||
if (idx < keyCurrent_.size()) {
|
||||
return keyCurrent_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDL2Input::pressed(Key key) const {
|
||||
size_t idx = static_cast<size_t>(key);
|
||||
return idx < keyCurrent_.size() ? (keyCurrent_[idx] && !keyPrevious_[idx]) : false;
|
||||
if (idx < keyCurrent_.size()) {
|
||||
return keyCurrent_[idx] && !keyPrevious_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDL2Input::released(Key key) const {
|
||||
size_t idx = static_cast<size_t>(key);
|
||||
return idx < keyCurrent_.size() ? (!keyCurrent_[idx] && keyPrevious_[idx]) : false;
|
||||
if (idx < keyCurrent_.size()) {
|
||||
return !keyCurrent_[idx] && keyPrevious_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDL2Input::down(Mouse btn) const {
|
||||
size_t idx = static_cast<size_t>(btn);
|
||||
return idx < mouseCurrent_.size() ? mouseCurrent_[idx] : false;
|
||||
if (idx < mouseCurrent_.size()) {
|
||||
return mouseCurrent_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDL2Input::pressed(Mouse btn) const {
|
||||
size_t idx = static_cast<size_t>(btn);
|
||||
return idx < mouseCurrent_.size() ? (mouseCurrent_[idx] && !mousePrevious_[idx]) : false;
|
||||
if (idx < mouseCurrent_.size()) {
|
||||
return mouseCurrent_[idx] && !mousePrevious_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDL2Input::released(Mouse btn) const {
|
||||
size_t idx = static_cast<size_t>(btn);
|
||||
return idx < mouseCurrent_.size() ? (!mouseCurrent_[idx] && mousePrevious_[idx]) : false;
|
||||
if (idx < mouseCurrent_.size()) {
|
||||
return !mouseCurrent_[idx] && mousePrevious_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Vec2 SDL2Input::mouse() const {
|
||||
|
|
@ -95,17 +267,26 @@ bool SDL2Input::gamepad() const {
|
|||
|
||||
bool SDL2Input::down(Gamepad btn) const {
|
||||
size_t idx = static_cast<size_t>(btn);
|
||||
return idx < gamepadCurrent_.size() ? gamepadCurrent_[idx] : false;
|
||||
if (idx < gamepadCurrent_.size()) {
|
||||
return gamepadCurrent_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDL2Input::pressed(Gamepad btn) const {
|
||||
size_t idx = static_cast<size_t>(btn);
|
||||
return idx < gamepadCurrent_.size() ? (gamepadCurrent_[idx] && !gamepadPrevious_[idx]) : false;
|
||||
if (idx < gamepadCurrent_.size()) {
|
||||
return gamepadCurrent_[idx] && !gamepadPrevious_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDL2Input::released(Gamepad btn) const {
|
||||
size_t idx = static_cast<size_t>(btn);
|
||||
return idx < gamepadCurrent_.size() ? (!gamepadCurrent_[idx] && gamepadPrevious_[idx]) : false;
|
||||
if (idx < gamepadCurrent_.size()) {
|
||||
return !gamepadCurrent_[idx] && gamepadPrevious_[idx];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Vec2 SDL2Input::leftStick() const {
|
||||
|
|
@ -126,9 +307,9 @@ float SDL2Input::rightTrigger() const {
|
|||
|
||||
void SDL2Input::vibrate(float left, float right) {
|
||||
if (gamepad_) {
|
||||
Uint16 lowFreq = static_cast<Uint16>(std::clamp(left, 0.0f, 1.0f) * 65535);
|
||||
Uint16 highFreq = static_cast<Uint16>(std::clamp(right, 0.0f, 1.0f) * 65535);
|
||||
SDL_GameControllerRumble(gamepad_, lowFreq, highFreq, 0);
|
||||
Uint16 lowFreq = static_cast<Uint16>(left * 65535.0f);
|
||||
Uint16 highFreq = static_cast<Uint16>(right * 65535.0f);
|
||||
SDL_GameControllerRumble(gamepad_, lowFreq, highFreq, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +323,7 @@ int SDL2Input::touchCount() const {
|
|||
|
||||
Vec2 SDL2Input::touch(int index) const {
|
||||
(void)index;
|
||||
return Vec2::Zero();
|
||||
return Vec2{0.0f, 0.0f};
|
||||
}
|
||||
|
||||
TouchPoint SDL2Input::touchPoint(int index) const {
|
||||
|
|
@ -151,160 +332,53 @@ TouchPoint SDL2Input::touchPoint(int index) const {
|
|||
}
|
||||
|
||||
void SDL2Input::updateKeyboard() {
|
||||
int numKeys = 0;
|
||||
const Uint8* state = SDL_GetKeyboardState(&numKeys);
|
||||
|
||||
auto updateKey = [&](Key key, int sdlScancode) {
|
||||
size_t idx = static_cast<size_t>(key);
|
||||
if (idx < keyCurrent_.size() && sdlScancode < numKeys) {
|
||||
keyCurrent_[idx] = state[sdlScancode] != 0;
|
||||
}
|
||||
};
|
||||
|
||||
updateKey(Key::A, SDL_SCANCODE_A);
|
||||
updateKey(Key::B, SDL_SCANCODE_B);
|
||||
updateKey(Key::C, SDL_SCANCODE_C);
|
||||
updateKey(Key::D, SDL_SCANCODE_D);
|
||||
updateKey(Key::E, SDL_SCANCODE_E);
|
||||
updateKey(Key::F, SDL_SCANCODE_F);
|
||||
updateKey(Key::G, SDL_SCANCODE_G);
|
||||
updateKey(Key::H, SDL_SCANCODE_H);
|
||||
updateKey(Key::I, SDL_SCANCODE_I);
|
||||
updateKey(Key::J, SDL_SCANCODE_J);
|
||||
updateKey(Key::K, SDL_SCANCODE_K);
|
||||
updateKey(Key::L, SDL_SCANCODE_L);
|
||||
updateKey(Key::M, SDL_SCANCODE_M);
|
||||
updateKey(Key::N, SDL_SCANCODE_N);
|
||||
updateKey(Key::O, SDL_SCANCODE_O);
|
||||
updateKey(Key::P, SDL_SCANCODE_P);
|
||||
updateKey(Key::Q, SDL_SCANCODE_Q);
|
||||
updateKey(Key::R, SDL_SCANCODE_R);
|
||||
updateKey(Key::S, SDL_SCANCODE_S);
|
||||
updateKey(Key::T, SDL_SCANCODE_T);
|
||||
updateKey(Key::U, SDL_SCANCODE_U);
|
||||
updateKey(Key::V, SDL_SCANCODE_V);
|
||||
updateKey(Key::W, SDL_SCANCODE_W);
|
||||
updateKey(Key::X, SDL_SCANCODE_X);
|
||||
updateKey(Key::Y, SDL_SCANCODE_Y);
|
||||
updateKey(Key::Z, SDL_SCANCODE_Z);
|
||||
updateKey(Key::Num0, SDL_SCANCODE_0);
|
||||
updateKey(Key::Num1, SDL_SCANCODE_1);
|
||||
updateKey(Key::Num2, SDL_SCANCODE_2);
|
||||
updateKey(Key::Num3, SDL_SCANCODE_3);
|
||||
updateKey(Key::Num4, SDL_SCANCODE_4);
|
||||
updateKey(Key::Num5, SDL_SCANCODE_5);
|
||||
updateKey(Key::Num6, SDL_SCANCODE_6);
|
||||
updateKey(Key::Num7, SDL_SCANCODE_7);
|
||||
updateKey(Key::Num8, SDL_SCANCODE_8);
|
||||
updateKey(Key::Num9, SDL_SCANCODE_9);
|
||||
updateKey(Key::F1, SDL_SCANCODE_F1);
|
||||
updateKey(Key::F2, SDL_SCANCODE_F2);
|
||||
updateKey(Key::F3, SDL_SCANCODE_F3);
|
||||
updateKey(Key::F4, SDL_SCANCODE_F4);
|
||||
updateKey(Key::F5, SDL_SCANCODE_F5);
|
||||
updateKey(Key::F6, SDL_SCANCODE_F6);
|
||||
updateKey(Key::F7, SDL_SCANCODE_F7);
|
||||
updateKey(Key::F8, SDL_SCANCODE_F8);
|
||||
updateKey(Key::F9, SDL_SCANCODE_F9);
|
||||
updateKey(Key::F10, SDL_SCANCODE_F10);
|
||||
updateKey(Key::F11, SDL_SCANCODE_F11);
|
||||
updateKey(Key::F12, SDL_SCANCODE_F12);
|
||||
updateKey(Key::Space, SDL_SCANCODE_SPACE);
|
||||
updateKey(Key::Enter, SDL_SCANCODE_RETURN);
|
||||
updateKey(Key::Escape, SDL_SCANCODE_ESCAPE);
|
||||
updateKey(Key::Tab, SDL_SCANCODE_TAB);
|
||||
updateKey(Key::Backspace, SDL_SCANCODE_BACKSPACE);
|
||||
updateKey(Key::Insert, SDL_SCANCODE_INSERT);
|
||||
updateKey(Key::Delete, SDL_SCANCODE_DELETE);
|
||||
updateKey(Key::Home, SDL_SCANCODE_HOME);
|
||||
updateKey(Key::End, SDL_SCANCODE_END);
|
||||
updateKey(Key::PageUp, SDL_SCANCODE_PAGEUP);
|
||||
updateKey(Key::PageDown, SDL_SCANCODE_PAGEDOWN);
|
||||
updateKey(Key::Up, SDL_SCANCODE_UP);
|
||||
updateKey(Key::Down, SDL_SCANCODE_DOWN);
|
||||
updateKey(Key::Left, SDL_SCANCODE_LEFT);
|
||||
updateKey(Key::Right, SDL_SCANCODE_RIGHT);
|
||||
updateKey(Key::LShift, SDL_SCANCODE_LSHIFT);
|
||||
updateKey(Key::RShift, SDL_SCANCODE_RSHIFT);
|
||||
updateKey(Key::LCtrl, SDL_SCANCODE_LCTRL);
|
||||
updateKey(Key::RCtrl, SDL_SCANCODE_RCTRL);
|
||||
updateKey(Key::LAlt, SDL_SCANCODE_LALT);
|
||||
updateKey(Key::RAlt, SDL_SCANCODE_RALT);
|
||||
updateKey(Key::CapsLock, SDL_SCANCODE_CAPSLOCK);
|
||||
updateKey(Key::NumLock, SDL_SCANCODE_NUMLOCKCLEAR);
|
||||
updateKey(Key::ScrollLock, SDL_SCANCODE_SCROLLLOCK);
|
||||
}
|
||||
|
||||
void SDL2Input::updateMouse() {
|
||||
int x, y;
|
||||
Uint32 state = SDL_GetMouseState(&x, &y);
|
||||
|
||||
Vec2 newPos(static_cast<float>(x), static_cast<float>(y));
|
||||
mouseDelta_ = newPos - mousePos_;
|
||||
mousePos_ = newPos;
|
||||
|
||||
mouseCurrent_[static_cast<size_t>(Mouse::Left)] = (state & SDL_BUTTON_LMASK) != 0;
|
||||
mouseCurrent_[static_cast<size_t>(Mouse::Right)] = (state & SDL_BUTTON_RMASK) != 0;
|
||||
mouseCurrent_[static_cast<size_t>(Mouse::Middle)] = (state & SDL_BUTTON_MMASK) != 0;
|
||||
mouseCurrent_[static_cast<size_t>(Mouse::X1)] = (state & SDL_BUTTON_X1MASK) != 0;
|
||||
mouseCurrent_[static_cast<size_t>(Mouse::X2)] = (state & SDL_BUTTON_X2MASK) != 0;
|
||||
int x = 0, y = 0;
|
||||
SDL_GetMouseState(&x, &y);
|
||||
mousePos_ = Vec2{static_cast<float>(x), static_cast<float>(y)};
|
||||
}
|
||||
|
||||
void SDL2Input::updateGamepad() {
|
||||
if (!gamepad_) {
|
||||
openGamepad();
|
||||
if (!gamepad_) return;
|
||||
return;
|
||||
}
|
||||
|
||||
auto applyDeadzone = [this](float value) -> float {
|
||||
if (std::abs(value) < deadzone_) return 0.0f;
|
||||
float sign = value >= 0 ? 1.0f : -1.0f;
|
||||
if (std::abs(value) < deadzone_) {
|
||||
return 0.0f;
|
||||
}
|
||||
float sign = value >= 0.0f ? 1.0f : -1.0f;
|
||||
return sign * (std::abs(value) - deadzone_) / (1.0f - deadzone_);
|
||||
};
|
||||
|
||||
auto getAxis = [this](SDL_GameControllerAxis axis) -> float {
|
||||
if (!gamepad_) return 0.0f;
|
||||
Sint16 value = SDL_GameControllerGetAxis(gamepad_, axis);
|
||||
return static_cast<float>(value) / 32767.0f;
|
||||
};
|
||||
int lx = SDL_GameControllerGetAxis(gamepad_, SDL_CONTROLLER_AXIS_LEFTX);
|
||||
int ly = SDL_GameControllerGetAxis(gamepad_, SDL_CONTROLLER_AXIS_LEFTY);
|
||||
int rx = SDL_GameControllerGetAxis(gamepad_, SDL_CONTROLLER_AXIS_RIGHTX);
|
||||
int ry = SDL_GameControllerGetAxis(gamepad_, SDL_CONTROLLER_AXIS_RIGHTY);
|
||||
|
||||
auto getButton = [this](SDL_GameControllerButton btn) -> bool {
|
||||
return gamepad_ ? SDL_GameControllerGetButton(gamepad_, btn) != 0 : false;
|
||||
};
|
||||
leftStick_.x = applyDeadzone(lx / 32767.0f);
|
||||
leftStick_.y = applyDeadzone(ly / 32767.0f);
|
||||
rightStick_.x = applyDeadzone(rx / 32767.0f);
|
||||
rightStick_.y = applyDeadzone(ry / 32767.0f);
|
||||
|
||||
leftStick_.x = applyDeadzone(getAxis(SDL_CONTROLLER_AXIS_LEFTX));
|
||||
leftStick_.y = applyDeadzone(getAxis(SDL_CONTROLLER_AXIS_LEFTY));
|
||||
rightStick_.x = applyDeadzone(getAxis(SDL_CONTROLLER_AXIS_RIGHTX));
|
||||
rightStick_.y = applyDeadzone(getAxis(SDL_CONTROLLER_AXIS_RIGHTY));
|
||||
int lt = SDL_GameControllerGetAxis(gamepad_, SDL_CONTROLLER_AXIS_TRIGGERLEFT);
|
||||
int rt = SDL_GameControllerGetAxis(gamepad_, SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
|
||||
|
||||
leftTrigger_ = getAxis(SDL_CONTROLLER_AXIS_TRIGGERLEFT);
|
||||
rightTrigger_ = getAxis(SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
|
||||
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::A)] = getButton(SDL_CONTROLLER_BUTTON_A);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::B)] = getButton(SDL_CONTROLLER_BUTTON_B);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::X)] = getButton(SDL_CONTROLLER_BUTTON_X);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::Y)] = getButton(SDL_CONTROLLER_BUTTON_Y);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::LB)] = getButton(SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::RB)] = getButton(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::Back)] = getButton(SDL_CONTROLLER_BUTTON_BACK);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::Start)] = getButton(SDL_CONTROLLER_BUTTON_START);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::Guide)] = getButton(SDL_CONTROLLER_BUTTON_GUIDE);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::LStick)] = getButton(SDL_CONTROLLER_BUTTON_LEFTSTICK);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::RStick)] = getButton(SDL_CONTROLLER_BUTTON_RIGHTSTICK);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::DUp)] = getButton(SDL_CONTROLLER_BUTTON_DPAD_UP);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::DDown)] = getButton(SDL_CONTROLLER_BUTTON_DPAD_DOWN);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::DLeft)] = getButton(SDL_CONTROLLER_BUTTON_DPAD_LEFT);
|
||||
gamepadCurrent_[static_cast<size_t>(Gamepad::DRight)] = getButton(SDL_CONTROLLER_BUTTON_DPAD_RIGHT);
|
||||
leftTrigger_ = lt / 32767.0f;
|
||||
rightTrigger_ = rt / 32767.0f;
|
||||
}
|
||||
|
||||
void SDL2Input::openGamepad() {
|
||||
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
|
||||
int numJoysticks = SDL_NumJoysticks();
|
||||
for (int i = 0; i < numJoysticks; ++i) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
gamepad_ = SDL_GameControllerOpen(i);
|
||||
if (gamepad_) {
|
||||
gamepadIndex_ = i;
|
||||
E2D_LOG_INFO("Gamepad connected: {}", SDL_GameControllerName(gamepad_));
|
||||
break;
|
||||
E2D_LOG_INFO("Gamepad opened: {}", SDL_GameControllerName(gamepad_));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -315,47 +389,20 @@ void SDL2Input::closeGamepad() {
|
|||
SDL_GameControllerClose(gamepad_);
|
||||
gamepad_ = nullptr;
|
||||
gamepadIndex_ = -1;
|
||||
E2D_LOG_INFO("Gamepad disconnected");
|
||||
gamepadCurrent_.fill(false);
|
||||
gamepadPrevious_.fill(false);
|
||||
}
|
||||
}
|
||||
|
||||
int SDL2Input::keyToSDL(Key key) {
|
||||
switch (key) {
|
||||
case Key::A: return SDL_SCANCODE_A;
|
||||
case Key::B: return SDL_SCANCODE_B;
|
||||
case Key::C: return SDL_SCANCODE_C;
|
||||
case Key::D: return SDL_SCANCODE_D;
|
||||
case Key::E: return SDL_SCANCODE_E;
|
||||
case Key::F: return SDL_SCANCODE_F;
|
||||
case Key::G: return SDL_SCANCODE_G;
|
||||
case Key::H: return SDL_SCANCODE_H;
|
||||
case Key::I: return SDL_SCANCODE_I;
|
||||
case Key::J: return SDL_SCANCODE_J;
|
||||
case Key::K: return SDL_SCANCODE_K;
|
||||
case Key::L: return SDL_SCANCODE_L;
|
||||
case Key::M: return SDL_SCANCODE_M;
|
||||
case Key::N: return SDL_SCANCODE_N;
|
||||
case Key::O: return SDL_SCANCODE_O;
|
||||
case Key::P: return SDL_SCANCODE_P;
|
||||
case Key::Q: return SDL_SCANCODE_Q;
|
||||
case Key::R: return SDL_SCANCODE_R;
|
||||
case Key::S: return SDL_SCANCODE_S;
|
||||
case Key::T: return SDL_SCANCODE_T;
|
||||
case Key::U: return SDL_SCANCODE_U;
|
||||
case Key::V: return SDL_SCANCODE_V;
|
||||
case Key::W: return SDL_SCANCODE_W;
|
||||
case Key::X: return SDL_SCANCODE_X;
|
||||
case Key::Y: return SDL_SCANCODE_Y;
|
||||
case Key::Z: return SDL_SCANCODE_Z;
|
||||
default: return SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
return static_cast<int>(key);
|
||||
}
|
||||
|
||||
int SDL2Input::mouseToSDL(Mouse btn) {
|
||||
switch (btn) {
|
||||
case Mouse::Left: return SDL_BUTTON_LEFT;
|
||||
case Mouse::Right: return SDL_BUTTON_RIGHT;
|
||||
case Mouse::Middle: return SDL_BUTTON_MIDDLE;
|
||||
case Mouse::Right: return SDL_BUTTON_RIGHT;
|
||||
case Mouse::X1: return SDL_BUTTON_X1;
|
||||
case Mouse::X2: return SDL_BUTTON_X2;
|
||||
default: return 0;
|
||||
|
|
@ -368,19 +415,37 @@ int SDL2Input::gamepadToSDL(Gamepad btn) {
|
|||
case Gamepad::B: return SDL_CONTROLLER_BUTTON_B;
|
||||
case Gamepad::X: return SDL_CONTROLLER_BUTTON_X;
|
||||
case Gamepad::Y: return SDL_CONTROLLER_BUTTON_Y;
|
||||
case Gamepad::LB: return SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
|
||||
case Gamepad::RB: return SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
|
||||
case Gamepad::Back: return SDL_CONTROLLER_BUTTON_BACK;
|
||||
case Gamepad::Start: return SDL_CONTROLLER_BUTTON_START;
|
||||
case Gamepad::Guide: return SDL_CONTROLLER_BUTTON_GUIDE;
|
||||
case Gamepad::LStick: return SDL_CONTROLLER_BUTTON_LEFTSTICK;
|
||||
case Gamepad::RStick: return SDL_CONTROLLER_BUTTON_RIGHTSTICK;
|
||||
case Gamepad::LB: return SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
|
||||
case Gamepad::RB: return SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
|
||||
case Gamepad::DUp: return SDL_CONTROLLER_BUTTON_DPAD_UP;
|
||||
case Gamepad::DDown: return SDL_CONTROLLER_BUTTON_DPAD_DOWN;
|
||||
case Gamepad::DLeft: return SDL_CONTROLLER_BUTTON_DPAD_LEFT;
|
||||
case Gamepad::DRight: return SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
|
||||
default: return SDL_CONTROLLER_BUTTON_INVALID;
|
||||
case Gamepad::Guide: return SDL_CONTROLLER_BUTTON_GUIDE;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
Key SDL2Input::sdlToKey(int sdlKey) {
|
||||
if (sdlKey >= 0 && sdlKey < static_cast<int>(Key::Count)) {
|
||||
return static_cast<Key>(sdlKey);
|
||||
}
|
||||
return Key::None;
|
||||
}
|
||||
|
||||
Mouse SDL2Input::sdlToMouse(int sdlButton) {
|
||||
switch (sdlButton) {
|
||||
case SDL_BUTTON_LEFT: return Mouse::Left;
|
||||
case SDL_BUTTON_MIDDLE: return Mouse::Middle;
|
||||
case SDL_BUTTON_RIGHT: return Mouse::Right;
|
||||
case SDL_BUTTON_X1: return Mouse::X1;
|
||||
case SDL_BUTTON_X2: return Mouse::X2;
|
||||
default: return Mouse::Count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/platform/iinput.h>
|
||||
#include <extra2d/event/event.h>
|
||||
#include <SDL.h>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
|
|
@ -11,6 +13,8 @@ namespace extra2d {
|
|||
*/
|
||||
class SDL2Input : public IInput {
|
||||
public:
|
||||
using EventCallback = std::function<void(const Event&)>;
|
||||
|
||||
SDL2Input();
|
||||
~SDL2Input() override;
|
||||
|
||||
|
|
@ -46,6 +50,18 @@ public:
|
|||
Vec2 touch(int index) const override;
|
||||
TouchPoint touchPoint(int index) const override;
|
||||
|
||||
/**
|
||||
* @brief 设置事件回调
|
||||
* @param callback 事件回调函数
|
||||
*/
|
||||
void setEventCallback(EventCallback callback);
|
||||
|
||||
/**
|
||||
* @brief 处理 SDL 事件
|
||||
* @param event SDL 事件
|
||||
*/
|
||||
void handleSDLEvent(const SDL_Event& event);
|
||||
|
||||
private:
|
||||
void updateKeyboard();
|
||||
void updateMouse();
|
||||
|
|
@ -56,6 +72,10 @@ private:
|
|||
static int keyToSDL(Key key);
|
||||
static int mouseToSDL(Mouse btn);
|
||||
static int gamepadToSDL(Gamepad btn);
|
||||
static Key sdlToKey(int sdlKey);
|
||||
static Mouse sdlToMouse(int sdlButton);
|
||||
|
||||
void dispatchEvent(const Event& event);
|
||||
|
||||
std::array<bool, static_cast<size_t>(Key::Count)> keyCurrent_{};
|
||||
std::array<bool, static_cast<size_t>(Key::Count)> keyPrevious_{};
|
||||
|
|
@ -77,6 +97,8 @@ private:
|
|||
float leftTrigger_ = 0.0f;
|
||||
float rightTrigger_ = 0.0f;
|
||||
float deadzone_ = 0.15f;
|
||||
|
||||
EventCallback eventCallback_;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ bool SDL2Window::create(const WindowConfigData& cfg) {
|
|||
}
|
||||
|
||||
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
|
||||
|
||||
#ifdef __SWITCH__
|
||||
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
#else
|
||||
if (cfg.isFullscreen()) {
|
||||
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
} else if (cfg.isBorderless()) {
|
||||
|
|
@ -32,10 +36,12 @@ bool SDL2Window::create(const WindowConfigData& cfg) {
|
|||
if (!cfg.decorated) {
|
||||
flags |= SDL_WINDOW_BORDERLESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
|
|
@ -75,7 +81,7 @@ bool SDL2Window::create(const WindowConfigData& cfg) {
|
|||
}
|
||||
|
||||
if (!gladLoadGLES2Loader((GLADloadproc)SDL_GL_GetProcAddress)) {
|
||||
E2D_LOG_ERROR("Failed to initialize GLAD");
|
||||
E2D_LOG_ERROR("Failed to initialize GLAD GLES2");
|
||||
SDL_GL_DeleteContext(glContext_);
|
||||
glContext_ = nullptr;
|
||||
SDL_DestroyWindow(sdlWindow_);
|
||||
|
|
@ -87,16 +93,19 @@ bool SDL2Window::create(const WindowConfigData& cfg) {
|
|||
SDL_GL_SetSwapInterval(cfg.vsync ? 1 : 0);
|
||||
|
||||
SDL_GetWindowSize(sdlWindow_, &width_, &height_);
|
||||
fullscreen_ = cfg.isFullscreen();
|
||||
fullscreen_ = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0;
|
||||
vsync_ = cfg.vsync;
|
||||
|
||||
#ifndef __SWITCH__
|
||||
initCursors();
|
||||
#endif
|
||||
updateContentScale();
|
||||
|
||||
input_ = makeUnique<SDL2Input>();
|
||||
input_->init();
|
||||
|
||||
E2D_LOG_INFO("SDL2 window created: {}x{}", width_, height_);
|
||||
E2D_LOG_INFO(" Platform: OpenGL ES 3.2");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +115,9 @@ void SDL2Window::destroy() {
|
|||
input_.reset();
|
||||
}
|
||||
|
||||
#ifndef __SWITCH__
|
||||
deinitCursors();
|
||||
#endif
|
||||
|
||||
if (glContext_) {
|
||||
SDL_GL_DeleteContext(glContext_);
|
||||
|
|
@ -163,16 +174,25 @@ void SDL2Window::setSize(int w, int h) {
|
|||
}
|
||||
|
||||
void SDL2Window::setPos(int x, int y) {
|
||||
#ifndef __SWITCH__
|
||||
if (sdlWindow_) {
|
||||
SDL_SetWindowPosition(sdlWindow_, x, y);
|
||||
}
|
||||
#else
|
||||
(void)x;
|
||||
(void)y;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SDL2Window::setFullscreen(bool fs) {
|
||||
#ifndef __SWITCH__
|
||||
if (sdlWindow_) {
|
||||
SDL_SetWindowFullscreen(sdlWindow_, fs ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
|
||||
fullscreen_ = fs;
|
||||
}
|
||||
#else
|
||||
(void)fs;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SDL2Window::setVSync(bool vsync) {
|
||||
|
|
@ -183,6 +203,7 @@ void SDL2Window::setVSync(bool vsync) {
|
|||
}
|
||||
|
||||
void SDL2Window::setVisible(bool visible) {
|
||||
#ifndef __SWITCH__
|
||||
if (sdlWindow_) {
|
||||
if (visible) {
|
||||
SDL_ShowWindow(sdlWindow_);
|
||||
|
|
@ -190,6 +211,9 @@ void SDL2Window::setVisible(bool visible) {
|
|||
SDL_HideWindow(sdlWindow_);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)visible;
|
||||
#endif
|
||||
}
|
||||
|
||||
int SDL2Window::width() const {
|
||||
|
|
@ -205,12 +229,12 @@ Size SDL2Window::size() const {
|
|||
}
|
||||
|
||||
Vec2 SDL2Window::pos() const {
|
||||
int x, y;
|
||||
int x = 0, y = 0;
|
||||
#ifndef __SWITCH__
|
||||
if (sdlWindow_) {
|
||||
SDL_GetWindowPosition(sdlWindow_, &x, &y);
|
||||
} else {
|
||||
x = y = 0;
|
||||
}
|
||||
#endif
|
||||
return Vec2(static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +263,7 @@ float SDL2Window::scaleY() const {
|
|||
}
|
||||
|
||||
void SDL2Window::setCursor(Cursor cursor) {
|
||||
#ifndef __SWITCH__
|
||||
if (cursor == Cursor::Hidden) {
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
return;
|
||||
|
|
@ -251,18 +276,29 @@ void SDL2Window::setCursor(Cursor cursor) {
|
|||
SDL_SetCursor(sdlCursors_[idx]);
|
||||
currentCursor_ = idx;
|
||||
}
|
||||
#else
|
||||
(void)cursor;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SDL2Window::showCursor(bool show) {
|
||||
#ifndef __SWITCH__
|
||||
SDL_ShowCursor(show ? SDL_ENABLE : SDL_DISABLE);
|
||||
cursorVisible_ = show;
|
||||
#else
|
||||
(void)show;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SDL2Window::lockCursor(bool lock) {
|
||||
#ifndef __SWITCH__
|
||||
if (sdlWindow_) {
|
||||
SDL_SetRelativeMouseMode(lock ? SDL_TRUE : SDL_FALSE);
|
||||
cursorLocked_ = lock;
|
||||
}
|
||||
#else
|
||||
(void)lock;
|
||||
#endif
|
||||
}
|
||||
|
||||
IInput* SDL2Window::input() const {
|
||||
|
|
@ -288,7 +324,8 @@ void* SDL2Window::native() const {
|
|||
bool SDL2Window::initSDL() {
|
||||
static int sdlInitCount = 0;
|
||||
if (sdlInitCount == 0) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
Uint32 initFlags = SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER;
|
||||
if (SDL_Init(initFlags) != 0) {
|
||||
E2D_LOG_ERROR("Failed to initialize SDL: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
|
@ -305,6 +342,7 @@ void SDL2Window::deinitSDL() {
|
|||
}
|
||||
}
|
||||
|
||||
#ifndef __SWITCH__
|
||||
void SDL2Window::initCursors() {
|
||||
sdlCursors_[0] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
|
||||
sdlCursors_[1] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
|
||||
|
|
@ -323,6 +361,7 @@ void SDL2Window::deinitCursors() {
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void SDL2Window::updateContentScale() {
|
||||
if (sdlWindow_) {
|
||||
|
|
@ -335,6 +374,10 @@ void SDL2Window::updateContentScale() {
|
|||
}
|
||||
|
||||
void SDL2Window::handleEvent(const SDL_Event& event) {
|
||||
if (input_) {
|
||||
input_->handleSDLEvent(event);
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
shouldClose_ = true;
|
||||
|
|
@ -378,4 +421,4 @@ void SDL2Window::handleEvent(const SDL_Event& event) {
|
|||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,270 +1,196 @@
|
|||
#include <extra2d/platform/input_module.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/platform/platform_module.h>
|
||||
#include <extra2d/platform/window_module.h>
|
||||
#include <extra2d/services/event_service.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// InputModuleConfig 实现
|
||||
// ============================================================================
|
||||
static ModuleId s_inputModuleId = INVALID_MODULE_ID;
|
||||
|
||||
ModuleId get_input_module_id() {
|
||||
return s_inputModuleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 验证配置有效性
|
||||
* 检查各项配置参数是否在有效范围内
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool InputModuleConfig::validate() const {
|
||||
if (deadzone < 0.0f || deadzone > 1.0f) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: deadzone 必须在 [0.0, 1.0] 范围内,当前值: {}", deadzone);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mouseSensitivity < 0.0f || mouseSensitivity > 10.0f) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: mouseSensitivity 必须在 [0.0, 10.0] 范围内,当前值: {}", mouseSensitivity);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!enableKeyboard && !enableMouse && !enableGamepad && !enableTouch) {
|
||||
E2D_LOG_WARN("InputModuleConfig: 所有输入设备都已禁用");
|
||||
}
|
||||
|
||||
return true;
|
||||
return inputConfig.isDeadzoneValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void InputModuleConfig::applyPlatformConstraints(PlatformType platform) {
|
||||
switch (platform) {
|
||||
case PlatformType::Switch:
|
||||
enableMouse = false;
|
||||
enableTouch = true;
|
||||
enableGamepad = true;
|
||||
E2D_LOG_INFO("InputModuleConfig: Switch 平台 - 禁用鼠标输入");
|
||||
break;
|
||||
|
||||
case PlatformType::Windows:
|
||||
case PlatformType::Linux:
|
||||
case PlatformType::macOS:
|
||||
enableMouse = true;
|
||||
enableKeyboard = true;
|
||||
enableTouch = false;
|
||||
E2D_LOG_INFO("InputModuleConfig: PC 平台 - 禁用触摸输入");
|
||||
break;
|
||||
|
||||
case PlatformType::Auto:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#ifdef __SWITCH__
|
||||
(void)platform;
|
||||
inputConfig.enableVibration = true;
|
||||
inputConfig.maxGamepads = 2;
|
||||
#else
|
||||
(void)platform;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void InputModuleConfig::resetToDefaults() {
|
||||
enableKeyboard = true;
|
||||
enableMouse = true;
|
||||
enableGamepad = true;
|
||||
enableTouch = true;
|
||||
deadzone = 0.15f;
|
||||
mouseSensitivity = 1.0f;
|
||||
|
||||
E2D_LOG_INFO("InputModuleConfig: 已重置为默认配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool InputModuleConfig::loadFromJson(const void* jsonData) {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 数据指针为空");
|
||||
return false;
|
||||
}
|
||||
if (!jsonData) return false;
|
||||
|
||||
try {
|
||||
const json& obj = *static_cast<const json*>(jsonData);
|
||||
const json& j = *static_cast<const json*>(jsonData);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 数据不是对象类型");
|
||||
return false;
|
||||
if (j.contains("enabled")) {
|
||||
inputConfig.enabled = j["enabled"].get<bool>();
|
||||
}
|
||||
if (j.contains("rawMouseInput")) {
|
||||
inputConfig.rawMouseInput = j["rawMouseInput"].get<bool>();
|
||||
}
|
||||
if (j.contains("mouseSensitivity")) {
|
||||
inputConfig.mouseSensitivity = j["mouseSensitivity"].get<float>();
|
||||
}
|
||||
if (j.contains("invertMouseY")) {
|
||||
inputConfig.invertMouseY = j["invertMouseY"].get<bool>();
|
||||
}
|
||||
if (j.contains("invertMouseX")) {
|
||||
inputConfig.invertMouseX = j["invertMouseX"].get<bool>();
|
||||
}
|
||||
if (j.contains("deadzone")) {
|
||||
inputConfig.deadzone = j["deadzone"].get<float>();
|
||||
}
|
||||
if (j.contains("triggerThreshold")) {
|
||||
inputConfig.triggerThreshold = j["triggerThreshold"].get<float>();
|
||||
}
|
||||
if (j.contains("enableVibration")) {
|
||||
inputConfig.enableVibration = j["enableVibration"].get<bool>();
|
||||
}
|
||||
if (j.contains("maxGamepads")) {
|
||||
inputConfig.maxGamepads = j["maxGamepads"].get<int>();
|
||||
}
|
||||
if (j.contains("autoConnectGamepads")) {
|
||||
inputConfig.autoConnectGamepads = j["autoConnectGamepads"].get<bool>();
|
||||
}
|
||||
if (j.contains("gamepadMappingFile")) {
|
||||
inputConfig.gamepadMappingFile = j["gamepadMappingFile"].get<std::string>();
|
||||
}
|
||||
|
||||
if (obj.contains("enableKeyboard") && obj["enableKeyboard"].is_boolean()) {
|
||||
enableKeyboard = obj["enableKeyboard"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("enableMouse") && obj["enableMouse"].is_boolean()) {
|
||||
enableMouse = obj["enableMouse"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("enableGamepad") && obj["enableGamepad"].is_boolean()) {
|
||||
enableGamepad = obj["enableGamepad"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("enableTouch") && obj["enableTouch"].is_boolean()) {
|
||||
enableTouch = obj["enableTouch"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("deadzone") && obj["deadzone"].is_number()) {
|
||||
deadzone = obj["deadzone"].get<float>();
|
||||
}
|
||||
|
||||
if (obj.contains("mouseSensitivity") && obj["mouseSensitivity"].is_number()) {
|
||||
mouseSensitivity = obj["mouseSensitivity"].get<float>();
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("InputModuleConfig: 从 JSON 加载配置成功");
|
||||
return true;
|
||||
|
||||
} catch (const json::exception& e) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 解析错误: {}", e.what());
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool InputModuleConfig::saveToJson(void* jsonData) const {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 数据指针为空");
|
||||
return false;
|
||||
}
|
||||
if (!jsonData) return false;
|
||||
|
||||
try {
|
||||
json& obj = *static_cast<json*>(jsonData);
|
||||
json& j = *static_cast<json*>(jsonData);
|
||||
|
||||
obj["enableKeyboard"] = enableKeyboard;
|
||||
obj["enableMouse"] = enableMouse;
|
||||
obj["enableGamepad"] = enableGamepad;
|
||||
obj["enableTouch"] = enableTouch;
|
||||
obj["deadzone"] = deadzone;
|
||||
obj["mouseSensitivity"] = mouseSensitivity;
|
||||
j["enabled"] = inputConfig.enabled;
|
||||
j["rawMouseInput"] = inputConfig.rawMouseInput;
|
||||
j["mouseSensitivity"] = inputConfig.mouseSensitivity;
|
||||
j["invertMouseY"] = inputConfig.invertMouseY;
|
||||
j["invertMouseX"] = inputConfig.invertMouseX;
|
||||
j["deadzone"] = inputConfig.deadzone;
|
||||
j["triggerThreshold"] = inputConfig.triggerThreshold;
|
||||
j["enableVibration"] = inputConfig.enableVibration;
|
||||
j["maxGamepads"] = inputConfig.maxGamepads;
|
||||
j["autoConnectGamepads"] = inputConfig.autoConnectGamepads;
|
||||
j["gamepadMappingFile"] = inputConfig.gamepadMappingFile;
|
||||
|
||||
E2D_LOG_INFO("InputModuleConfig: 保存配置到 JSON 成功");
|
||||
return true;
|
||||
|
||||
} catch (const json::exception& e) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 序列化错误: {}", e.what());
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// InputModuleInitializer 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
InputModuleInitializer::InputModuleInitializer()
|
||||
: moduleId_(INVALID_MODULE_ID)
|
||||
, windowModuleId_(INVALID_MODULE_ID)
|
||||
, input_(nullptr)
|
||||
, initialized_(false) {
|
||||
E2D_LOG_DEBUG("InputModuleInitializer: 构造函数调用");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
InputModuleInitializer::~InputModuleInitializer() {
|
||||
if (initialized_) {
|
||||
shutdown();
|
||||
}
|
||||
E2D_LOG_DEBUG("InputModuleInitializer: 析构函数调用");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* 返回此模块依赖的窗口模块标识符
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> InputModuleInitializer::getDependencies() const {
|
||||
std::vector<ModuleId> dependencies;
|
||||
|
||||
if (windowModuleId_ != INVALID_MODULE_ID) {
|
||||
dependencies.push_back(windowModuleId_);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
return { get_window_module_id() };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* 从窗口模块获取输入接口
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool InputModuleInitializer::initialize(const IModuleConfig* config) {
|
||||
if (initialized_) {
|
||||
E2D_LOG_WARN("InputModuleInitializer: 模块已经初始化");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
E2D_LOG_ERROR("InputModuleInitializer: 配置指针为空");
|
||||
return false;
|
||||
}
|
||||
if (initialized_) return true;
|
||||
|
||||
const InputModuleConfig* inputConfig = dynamic_cast<const InputModuleConfig*>(config);
|
||||
if (!inputConfig) {
|
||||
E2D_LOG_ERROR("InputModuleInitializer: 配置类型不正确");
|
||||
E2D_LOG_ERROR("Invalid input module config");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!inputConfig->validate()) {
|
||||
E2D_LOG_ERROR("InputModuleInitializer: 配置验证失败");
|
||||
config_ = inputConfig->inputConfig;
|
||||
|
||||
auto& registry = ModuleRegistry::instance();
|
||||
auto* windowInitializer = registry.getInitializer(get_window_module_id());
|
||||
if (!windowInitializer) {
|
||||
E2D_LOG_ERROR("Window module not found - Input module depends on it");
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleInfo info = config->getModuleInfo();
|
||||
moduleId_ = info.id;
|
||||
auto* windowModule = static_cast<WindowModuleInitializer*>(windowInitializer);
|
||||
IWindow* window = windowModule->getWindow();
|
||||
if (!window) {
|
||||
E2D_LOG_ERROR("Window not created - cannot get input interface");
|
||||
return false;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 正在初始化输入模块 '{}' (版本: {})",
|
||||
info.name, info.version);
|
||||
input_ = window->input();
|
||||
if (!input_) {
|
||||
E2D_LOG_ERROR("Input interface not available from window");
|
||||
return false;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 输入配置 - 键盘: {}, 鼠标: {}, 手柄: {}, 触摸: {}",
|
||||
inputConfig->enableKeyboard ? "启用" : "禁用",
|
||||
inputConfig->enableMouse ? "启用" : "禁用",
|
||||
inputConfig->enableGamepad ? "启用" : "禁用",
|
||||
inputConfig->enableTouch ? "启用" : "禁用");
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 死区: {}, 鼠标灵敏度: {}",
|
||||
inputConfig->deadzone, inputConfig->mouseSensitivity);
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 输入模块初始化成功");
|
||||
initialized_ = true;
|
||||
E2D_LOG_INFO("Input module initialized");
|
||||
E2D_LOG_INFO(" Deadzone: {}", config_.deadzone);
|
||||
E2D_LOG_INFO(" Mouse sensitivity: {}", config_.mouseSensitivity);
|
||||
E2D_LOG_INFO(" Vibration: {}", config_.enableVibration ? "enabled" : "disabled");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
void InputModuleInitializer::shutdown() {
|
||||
if (!initialized_) {
|
||||
E2D_LOG_WARN("InputModuleInitializer: 模块未初始化,无需关闭");
|
||||
return;
|
||||
}
|
||||
if (!initialized_) return;
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 正在关闭输入模块");
|
||||
E2D_LOG_INFO("Input module shutting down");
|
||||
|
||||
input_ = nullptr;
|
||||
moduleId_ = INVALID_MODULE_ID;
|
||||
initialized_ = false;
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 输入模块已关闭");
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
void InputModuleInitializer::update() {
|
||||
if (!initialized_ || !input_) return;
|
||||
|
||||
input_->update();
|
||||
}
|
||||
|
||||
void register_input_module() {
|
||||
if (s_inputModuleId != INVALID_MODULE_ID) return;
|
||||
|
||||
s_inputModuleId = ModuleRegistry::instance().registerModule(
|
||||
makeUnique<InputModuleConfig>(),
|
||||
[]() -> UniquePtr<IModuleInitializer> {
|
||||
auto initializer = makeUnique<InputModuleInitializer>();
|
||||
initializer->setModuleId(s_inputModuleId);
|
||||
return initializer;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct InputModuleAutoRegister {
|
||||
InputModuleAutoRegister() {
|
||||
register_input_module();
|
||||
}
|
||||
};
|
||||
|
||||
static InputModuleAutoRegister s_autoRegister;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,9 +93,6 @@ bool PlatformModuleInitializer::initialize(const IModuleConfig* config) {
|
|||
return false;
|
||||
}
|
||||
|
||||
auto& appConfig = ConfigManager::instance().appConfig();
|
||||
appConfig.applyPlatformConstraints(*platformConfig_);
|
||||
|
||||
if (resolvedPlatform_ == PlatformType::Switch) {
|
||||
if (!initSwitch()) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,9 @@
|
|||
#include <extra2d/platform/window_module.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/config/config_manager.h>
|
||||
#include <extra2d/platform/platform_module.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#ifdef E2D_BACKEND_SDL2
|
||||
#include <SDL.h>
|
||||
#endif
|
||||
|
||||
#ifdef E2D_BACKEND_GLFW
|
||||
#include <GLFW/glfw3.h>
|
||||
#endif
|
||||
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
|
|
@ -27,16 +19,25 @@ ModuleId get_window_module_id() {
|
|||
return s_windowModuleId;
|
||||
}
|
||||
|
||||
void WindowModuleConfig::applyPlatformConstraints(PlatformType platform) {
|
||||
#ifdef __SWITCH__
|
||||
(void)platform;
|
||||
windowConfig.mode = WindowMode::Fullscreen;
|
||||
windowConfig.resizable = false;
|
||||
windowConfig.highDPI = false;
|
||||
windowConfig.width = 1920;
|
||||
windowConfig.height = 1080;
|
||||
#else
|
||||
(void)platform;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WindowModuleConfig::loadFromJson(const void* jsonData) {
|
||||
if (!jsonData) return false;
|
||||
|
||||
try {
|
||||
const json& j = *static_cast<const json*>(jsonData);
|
||||
|
||||
if (j.contains("backend")) {
|
||||
backend = j["backend"].get<std::string>();
|
||||
}
|
||||
|
||||
if (j.contains("title")) {
|
||||
windowConfig.title = j["title"].get<std::string>();
|
||||
}
|
||||
|
|
@ -46,15 +47,37 @@ bool WindowModuleConfig::loadFromJson(const void* jsonData) {
|
|||
if (j.contains("height")) {
|
||||
windowConfig.height = j["height"].get<int>();
|
||||
}
|
||||
if (j.contains("minWidth")) {
|
||||
windowConfig.minWidth = j["minWidth"].get<int>();
|
||||
}
|
||||
if (j.contains("minHeight")) {
|
||||
windowConfig.minHeight = j["minHeight"].get<int>();
|
||||
}
|
||||
if (j.contains("fullscreen")) {
|
||||
windowConfig.mode = j["fullscreen"].get<bool>() ? WindowMode::Fullscreen : WindowMode::Windowed;
|
||||
}
|
||||
if (j.contains("mode")) {
|
||||
std::string modeStr = j["mode"].get<std::string>();
|
||||
if (modeStr == "fullscreen") {
|
||||
windowConfig.mode = WindowMode::Fullscreen;
|
||||
} else if (modeStr == "borderless") {
|
||||
windowConfig.mode = WindowMode::Borderless;
|
||||
} else {
|
||||
windowConfig.mode = WindowMode::Windowed;
|
||||
}
|
||||
}
|
||||
if (j.contains("vsync")) {
|
||||
windowConfig.vsync = j["vsync"].get<bool>();
|
||||
}
|
||||
if (j.contains("resizable")) {
|
||||
windowConfig.resizable = j["resizable"].get<bool>();
|
||||
}
|
||||
if (j.contains("highDPI")) {
|
||||
windowConfig.highDPI = j["highDPI"].get<bool>();
|
||||
}
|
||||
if (j.contains("multisamples")) {
|
||||
windowConfig.multisamples = j["multisamples"].get<int>();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (...) {
|
||||
|
|
@ -67,13 +90,28 @@ bool WindowModuleConfig::saveToJson(void* jsonData) const {
|
|||
|
||||
try {
|
||||
json& j = *static_cast<json*>(jsonData);
|
||||
j["backend"] = backend;
|
||||
j["title"] = windowConfig.title;
|
||||
j["width"] = windowConfig.width;
|
||||
j["height"] = windowConfig.height;
|
||||
j["fullscreen"] = (windowConfig.mode == WindowMode::Fullscreen);
|
||||
j["minWidth"] = windowConfig.minWidth;
|
||||
j["minHeight"] = windowConfig.minHeight;
|
||||
|
||||
switch (windowConfig.mode) {
|
||||
case WindowMode::Fullscreen:
|
||||
j["mode"] = "fullscreen";
|
||||
break;
|
||||
case WindowMode::Borderless:
|
||||
j["mode"] = "borderless";
|
||||
break;
|
||||
default:
|
||||
j["mode"] = "windowed";
|
||||
break;
|
||||
}
|
||||
|
||||
j["vsync"] = windowConfig.vsync;
|
||||
j["resizable"] = windowConfig.resizable;
|
||||
j["highDPI"] = windowConfig.highDPI;
|
||||
j["multisamples"] = windowConfig.multisamples;
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
|
|
@ -83,7 +121,7 @@ bool WindowModuleConfig::saveToJson(void* jsonData) const {
|
|||
WindowModuleInitializer::WindowModuleInitializer()
|
||||
: moduleId_(INVALID_MODULE_ID)
|
||||
, initialized_(false)
|
||||
, backendInitialized_(false) {
|
||||
, sdl2Initialized_(false) {
|
||||
}
|
||||
|
||||
WindowModuleInitializer::~WindowModuleInitializer() {
|
||||
|
|
@ -92,51 +130,29 @@ WindowModuleInitializer::~WindowModuleInitializer() {
|
|||
}
|
||||
}
|
||||
|
||||
bool WindowModuleInitializer::initBackend() {
|
||||
#ifdef E2D_BACKEND_SDL2
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) != 0) {
|
||||
bool WindowModuleInitializer::initSDL2() {
|
||||
Uint32 initFlags = SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER;
|
||||
|
||||
#ifdef __SWITCH__
|
||||
initFlags |= SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER;
|
||||
#endif
|
||||
|
||||
if (SDL_Init(initFlags) != 0) {
|
||||
E2D_LOG_ERROR("Failed to initialize SDL2: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
E2D_LOG_INFO("SDL2 backend initialized");
|
||||
backendInitialized_ = true;
|
||||
return true;
|
||||
#endif
|
||||
|
||||
#ifdef E2D_BACKEND_GLFW
|
||||
if (!glfwInit()) {
|
||||
E2D_LOG_ERROR("Failed to initialize GLFW");
|
||||
return false;
|
||||
}
|
||||
E2D_LOG_INFO("GLFW backend initialized");
|
||||
backendInitialized_ = true;
|
||||
sdl2Initialized_ = true;
|
||||
E2D_LOG_INFO("SDL2 initialized successfully");
|
||||
return true;
|
||||
#endif
|
||||
|
||||
#ifdef E2D_BACKEND_SWITCH
|
||||
E2D_LOG_INFO("Switch backend (no init required)");
|
||||
backendInitialized_ = true;
|
||||
return true;
|
||||
#endif
|
||||
|
||||
E2D_LOG_ERROR("No backend available");
|
||||
return false;
|
||||
}
|
||||
|
||||
void WindowModuleInitializer::shutdownBackend() {
|
||||
if (!backendInitialized_) return;
|
||||
void WindowModuleInitializer::shutdownSDL2() {
|
||||
if (!sdl2Initialized_) return;
|
||||
|
||||
#ifdef E2D_BACKEND_SDL2
|
||||
SDL_Quit();
|
||||
E2D_LOG_INFO("SDL2 backend shutdown");
|
||||
#endif
|
||||
|
||||
#ifdef E2D_BACKEND_GLFW
|
||||
glfwTerminate();
|
||||
E2D_LOG_INFO("GLFW backend shutdown");
|
||||
#endif
|
||||
|
||||
backendInitialized_ = false;
|
||||
sdl2Initialized_ = false;
|
||||
E2D_LOG_INFO("SDL2 shutdown");
|
||||
}
|
||||
|
||||
bool WindowModuleInitializer::initialize(const IModuleConfig* config) {
|
||||
|
|
@ -148,60 +164,53 @@ bool WindowModuleInitializer::initialize(const IModuleConfig* config) {
|
|||
return false;
|
||||
}
|
||||
|
||||
backend_ = windowConfig->backend;
|
||||
windowConfig_ = windowConfig->windowConfig;
|
||||
|
||||
#ifdef __SWITCH__
|
||||
backend_ = "switch";
|
||||
windowConfig_.mode = WindowMode::Fullscreen;
|
||||
windowConfig_.resizable = false;
|
||||
windowConfig_.highDPI = false;
|
||||
E2D_LOG_INFO("Switch platform: forcing fullscreen mode");
|
||||
#endif
|
||||
|
||||
if (!initBackend()) {
|
||||
if (!initSDL2()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef E2D_BACKEND_SDL2
|
||||
extern void initSDL2Backend();
|
||||
initSDL2Backend();
|
||||
#endif
|
||||
|
||||
if (!BackendFactory::has(backend_)) {
|
||||
E2D_LOG_ERROR("Backend '{}' not available", backend_);
|
||||
auto backends = BackendFactory::backends();
|
||||
if (backends.empty()) {
|
||||
E2D_LOG_ERROR("No backends registered!");
|
||||
shutdownBackend();
|
||||
return false;
|
||||
}
|
||||
backend_ = backends[0];
|
||||
E2D_LOG_WARN("Using fallback backend: {}", backend_);
|
||||
if (!BackendFactory::has("sdl2")) {
|
||||
E2D_LOG_ERROR("SDL2 backend not registered!");
|
||||
shutdownSDL2();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!createWindow(backend_, windowConfig_)) {
|
||||
if (!createWindow(windowConfig_)) {
|
||||
E2D_LOG_ERROR("Failed to create window");
|
||||
shutdownBackend();
|
||||
shutdownSDL2();
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
E2D_LOG_INFO("Window module initialized");
|
||||
E2D_LOG_INFO(" Window: {}x{}", window_->width(), window_->height());
|
||||
E2D_LOG_INFO(" Backend: {}", backend_);
|
||||
E2D_LOG_INFO(" Backend: SDL2");
|
||||
E2D_LOG_INFO(" VSync: {}", windowConfig_.vsync);
|
||||
E2D_LOG_INFO(" Fullscreen: {}", windowConfig_.isFullscreen());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowModuleInitializer::createWindow(const std::string& backend, const WindowConfigData& config) {
|
||||
window_ = BackendFactory::createWindow(backend);
|
||||
bool WindowModuleInitializer::createWindow(const WindowConfigData& config) {
|
||||
window_ = BackendFactory::createWindow("sdl2");
|
||||
if (!window_) {
|
||||
E2D_LOG_ERROR("Failed to create window for backend: {}", backend);
|
||||
E2D_LOG_ERROR("Failed to create SDL2 window");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!window_->create(config)) {
|
||||
E2D_LOG_ERROR("Failed to create window");
|
||||
E2D_LOG_ERROR("Failed to create window with specified config");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +227,7 @@ void WindowModuleInitializer::shutdown() {
|
|||
window_.reset();
|
||||
}
|
||||
|
||||
shutdownBackend();
|
||||
shutdownSDL2();
|
||||
|
||||
initialized_ = false;
|
||||
}
|
||||
|
|
@ -246,4 +255,4 @@ namespace {
|
|||
static WindowModuleAutoRegister s_autoRegister;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
#include <extra2d/resource/resource_config.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
void ResourceConfigData::addSearchPath(const std::string& path) {
|
||||
if (!hasSearchPath(path)) {
|
||||
searchPaths.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceConfigData::removeSearchPath(const std::string& path) {
|
||||
auto it = std::find(searchPaths.begin(), searchPaths.end(), path);
|
||||
if (it != searchPaths.end()) {
|
||||
searchPaths.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceConfigData::hasSearchPath(const std::string& path) const {
|
||||
return std::find(searchPaths.begin(), searchPaths.end(), path) != searchPaths.end();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,48 +2,55 @@
|
|||
|
||||
## 概述
|
||||
|
||||
Extra2D 采用模块化架构设计,所有核心功能都通过模块系统管理。模块系统提供:
|
||||
Extra2D 采用模块化架构设计,所有核心功能通过模块系统和服务系统管理。系统提供:
|
||||
|
||||
- **统一的生命周期管理**:初始化、关闭、依赖处理
|
||||
- **优先级排序**:确保模块按正确顺序初始化
|
||||
- **优先级排序**:确保模块/服务按正确顺序初始化
|
||||
- **配置驱动**:每个模块可独立配置
|
||||
- **解耦设计**:Application 只协调模块,不直接管理任何依赖
|
||||
- **依赖注入**:通过服务定位器解耦模块间依赖
|
||||
- **可测试性**:支持 Mock 服务进行单元测试
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application │
|
||||
│ (只负责协调模块,不直接管理任何依赖) │
|
||||
│ (协调模块和服务,通过服务定位器获取依赖) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ModuleRegistry │
|
||||
│ (模块注册表,管理所有模块的生命周期) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ Logger Module │ │ Config Module │ │Platform Module│
|
||||
│ (日志系统) │ │ (配置管理) │ │ (平台初始化) │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
│ │ │
|
||||
└─────────────────────┼─────────────────────┘
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ Window Module │
|
||||
│ (窗口管理) │
|
||||
└───────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ Render Module │
|
||||
│ (渲染系统) │
|
||||
└───────────────┘
|
||||
┌───────────────┴───────────────┐
|
||||
▼ ▼
|
||||
┌─────────────────────────────┐ ┌─────────────────────────────┐
|
||||
│ ModuleRegistry │ │ ServiceLocator │
|
||||
│ (模块注册表,管理平台级模块) │ │ (服务定位器,管理运行时服务) │
|
||||
└─────────────────────────────┘ └─────────────────────────────┘
|
||||
│ │
|
||||
┌─────┴─────┐ ┌───────┴───────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ Config │ │ Window │ │ Scene │ │ Timer │
|
||||
│ Module │ │ Module │ │ Service │ │ Service │
|
||||
└───────────┘ └───────────┘ └───────────┘ └───────────┘
|
||||
│ │
|
||||
┌─────┴─────┐ ┌─────┴─────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Event │ │ Camera │ │ ... │
|
||||
│ Service │ │ Service │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## 模块 vs 服务
|
||||
|
||||
| 特性 | 模块 (Module) | 服务 (Service) |
|
||||
|-----|--------------|---------------|
|
||||
| 用途 | 平台级初始化 | 运行时功能 |
|
||||
| 生命周期 | Application 管理 | ServiceLocator 管理 |
|
||||
| 依赖方式 | 通过 ModuleRegistry | 通过 ServiceLocator |
|
||||
| 可替换性 | 编译时确定 | 运行时可替换 |
|
||||
| 测试支持 | 需要重构 | 原生支持 Mock |
|
||||
| 示例 | Window, Render, Config | Scene, Timer, Event, Camera |
|
||||
|
||||
## 模块优先级
|
||||
|
||||
模块按优先级从小到大初始化,关闭时逆序执行:
|
||||
|
|
@ -51,14 +58,27 @@ Extra2D 采用模块化架构设计,所有核心功能都通过模块系统管
|
|||
| 优先级值 | 枚举名称 | 用途 | 模块示例 |
|
||||
|---------|---------|------|---------|
|
||||
| 0 | `Core` | 核心模块,最先初始化 | Logger, Config, Platform, Window |
|
||||
| 50 | `Input` | 输入处理 | Input |
|
||||
| 100 | `Graphics` | 图形渲染 | Render |
|
||||
| 200 | `Audio` | 音频系统 | Audio |
|
||||
| 300 | `Physics` | 物理系统 | Physics |
|
||||
| 400 | `Gameplay` | 游戏逻辑 | Gameplay |
|
||||
| 500 | `UI` | 用户界面 | UI |
|
||||
| 500 | `Resource` | 资源管理 | Resource |
|
||||
|
||||
## 核心接口
|
||||
## 服务优先级
|
||||
|
||||
服务按优先级从小到大初始化:
|
||||
|
||||
| 优先级值 | 枚举名称 | 用途 | 服务示例 |
|
||||
|---------|---------|------|---------|
|
||||
| 0 | `Core` | 核心服务 | - |
|
||||
| 100 | `Event` | 事件系统 | EventService |
|
||||
| 200 | `Timer` | 计时器 | TimerService |
|
||||
| 300 | `Scene` | 场景管理 | SceneService |
|
||||
| 400 | `Camera` | 相机系统 | CameraService |
|
||||
| 500 | `Resource` | 资源管理 | - |
|
||||
| 600 | `Audio` | 音频系统 | - |
|
||||
|
||||
---
|
||||
|
||||
## 模块系统
|
||||
|
||||
### IModuleConfig
|
||||
|
||||
|
|
@ -69,17 +89,12 @@ class IModuleConfig {
|
|||
public:
|
||||
virtual ~IModuleConfig() = default;
|
||||
|
||||
// 模块信息
|
||||
virtual ModuleInfo getModuleInfo() const = 0;
|
||||
|
||||
// 配置管理
|
||||
virtual std::string getConfigSectionName() const = 0;
|
||||
virtual bool validate() const = 0;
|
||||
virtual void resetToDefaults() = 0;
|
||||
virtual bool loadFromJson(const void* jsonData) = 0;
|
||||
virtual bool saveToJson(void* jsonData) const = 0;
|
||||
|
||||
// 平台约束
|
||||
virtual void applyPlatformConstraints(PlatformType platform) {}
|
||||
};
|
||||
```
|
||||
|
|
@ -93,12 +108,10 @@ class IModuleInitializer {
|
|||
public:
|
||||
virtual ~IModuleInitializer() = default;
|
||||
|
||||
// 模块标识
|
||||
virtual ModuleId getModuleId() const = 0;
|
||||
virtual ModulePriority getPriority() const = 0;
|
||||
virtual std::vector<ModuleId> getDependencies() const = 0;
|
||||
|
||||
// 生命周期
|
||||
virtual bool initialize(const IModuleConfig* config) = 0;
|
||||
virtual void shutdown() = 0;
|
||||
virtual bool isInitialized() const = 0;
|
||||
|
|
@ -114,58 +127,292 @@ class ModuleRegistry {
|
|||
public:
|
||||
static ModuleRegistry& instance();
|
||||
|
||||
// 注册模块
|
||||
ModuleId registerModule(
|
||||
UniquePtr<IModuleConfig> config,
|
||||
ModuleInitializerFactory factory
|
||||
);
|
||||
|
||||
// 获取模块
|
||||
IModuleConfig* getModuleConfig(ModuleId id);
|
||||
IModuleInitializer* getInitializer(ModuleId id);
|
||||
|
||||
// 初始化顺序
|
||||
std::vector<ModuleId> getInitializationOrder() const;
|
||||
|
||||
// 查询
|
||||
bool hasModule(const std::string& name) const;
|
||||
std::vector<std::string> getModuleNames() const;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 服务系统
|
||||
|
||||
### IService
|
||||
|
||||
服务接口基类,所有服务必须实现:
|
||||
|
||||
```cpp
|
||||
class IService {
|
||||
public:
|
||||
virtual ~IService() = default;
|
||||
|
||||
virtual ServiceInfo getServiceInfo() const = 0;
|
||||
virtual bool initialize() = 0;
|
||||
virtual void shutdown() = 0;
|
||||
virtual void pause();
|
||||
virtual void resume();
|
||||
virtual void update(float deltaTime);
|
||||
virtual bool isInitialized() const;
|
||||
|
||||
ServiceState getState() const;
|
||||
const std::string& getName() const;
|
||||
};
|
||||
```
|
||||
|
||||
### ServiceLocator
|
||||
|
||||
服务定位器,实现依赖注入和服务发现:
|
||||
|
||||
```cpp
|
||||
class ServiceLocator {
|
||||
public:
|
||||
static ServiceLocator& instance();
|
||||
|
||||
// 注册服务实例
|
||||
template<typename T>
|
||||
void registerService(SharedPtr<T> service);
|
||||
|
||||
// 注册服务工厂(延迟创建)
|
||||
template<typename T>
|
||||
void registerFactory(ServiceFactory<T> factory);
|
||||
|
||||
// 获取服务
|
||||
template<typename T>
|
||||
SharedPtr<T> getService() const;
|
||||
|
||||
// 检查服务是否存在
|
||||
template<typename T>
|
||||
bool hasService() const;
|
||||
|
||||
// 批量操作
|
||||
bool initializeAll();
|
||||
void shutdownAll();
|
||||
void updateAll(float deltaTime);
|
||||
void pauseAll();
|
||||
void resumeAll();
|
||||
void clear();
|
||||
};
|
||||
```
|
||||
|
||||
### 内置服务
|
||||
|
||||
#### SceneService
|
||||
|
||||
场景管理服务,包装 SceneManager:
|
||||
|
||||
```cpp
|
||||
class ISceneService : public IService {
|
||||
public:
|
||||
virtual void runWithScene(Ptr<Scene> scene) = 0;
|
||||
virtual void replaceScene(Ptr<Scene> scene) = 0;
|
||||
virtual void pushScene(Ptr<Scene> scene) = 0;
|
||||
virtual void popScene() = 0;
|
||||
|
||||
virtual Ptr<Scene> getCurrentScene() const = 0;
|
||||
virtual size_t getSceneCount() const = 0;
|
||||
|
||||
virtual void render(RenderBackend& renderer) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
#### TimerService
|
||||
|
||||
计时器服务,包装 TimerManager:
|
||||
|
||||
```cpp
|
||||
class ITimerService : public IService {
|
||||
public:
|
||||
virtual uint32 addTimer(float delay, Timer::Callback callback) = 0;
|
||||
virtual uint32 addRepeatingTimer(float interval, Timer::Callback callback) = 0;
|
||||
virtual void cancelTimer(uint32 timerId) = 0;
|
||||
virtual void pauseTimer(uint32 timerId) = 0;
|
||||
virtual void resumeTimer(uint32 timerId) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
#### EventService
|
||||
|
||||
事件服务,整合 EventQueue 和 EventDispatcher:
|
||||
|
||||
```cpp
|
||||
class IEventService : public IService {
|
||||
public:
|
||||
virtual void pushEvent(const Event& event) = 0;
|
||||
virtual bool pollEvent(Event& event) = 0;
|
||||
|
||||
virtual ListenerId addListener(EventType type, EventCallback callback) = 0;
|
||||
virtual void removeListener(ListenerId id) = 0;
|
||||
|
||||
virtual void dispatch(Event& event) = 0;
|
||||
virtual void processQueue() = 0;
|
||||
};
|
||||
```
|
||||
|
||||
#### CameraService
|
||||
|
||||
相机服务,整合 Camera 和 ViewportAdapter:
|
||||
|
||||
```cpp
|
||||
class ICameraService : public IService {
|
||||
public:
|
||||
virtual void setPosition(const Vec2& position) = 0;
|
||||
virtual void setZoom(float zoom) = 0;
|
||||
virtual void setRotation(float degrees) = 0;
|
||||
|
||||
virtual glm::mat4 getViewProjectionMatrix() const = 0;
|
||||
virtual Vec2 screenToWorld(const Vec2& screenPos) const = 0;
|
||||
|
||||
virtual void setViewportConfig(const ViewportConfig& config) = 0;
|
||||
virtual void updateViewport(int width, int height) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 在 Application 中使用服务
|
||||
|
||||
```cpp
|
||||
// 获取服务
|
||||
auto sceneService = Application::get().scenes();
|
||||
auto timerService = Application::get().timers();
|
||||
auto eventService = Application::get().events();
|
||||
auto cameraService = Application::get().camera();
|
||||
|
||||
// 使用场景服务
|
||||
sceneService->pushScene(myScene);
|
||||
|
||||
// 使用计时器服务
|
||||
timerService->addTimer(1.0f, []() {
|
||||
E2D_LOG_INFO("Timer fired!");
|
||||
});
|
||||
|
||||
// 使用事件服务
|
||||
eventService->addListener(EventType::KeyDown, [](Event& e) {
|
||||
E2D_LOG_INFO("Key pressed: {}", e.key.keycode);
|
||||
});
|
||||
|
||||
// 使用相机服务
|
||||
cameraService->setPosition(Vec2(100.0f, 200.0f));
|
||||
cameraService->setZoom(2.0f);
|
||||
```
|
||||
|
||||
### 注册自定义服务
|
||||
|
||||
```cpp
|
||||
// 定义服务接口
|
||||
class IAudioService : public IService {
|
||||
public:
|
||||
virtual void playSound(const std::string& path) = 0;
|
||||
virtual void stopAll() = 0;
|
||||
};
|
||||
|
||||
// 实现服务
|
||||
class AudioService : public IAudioService {
|
||||
public:
|
||||
ServiceInfo getServiceInfo() const override {
|
||||
ServiceInfo info;
|
||||
info.name = "AudioService";
|
||||
info.priority = ServicePriority::Audio;
|
||||
return info;
|
||||
}
|
||||
|
||||
bool initialize() override {
|
||||
// 初始化音频系统...
|
||||
setState(ServiceState::Running);
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown() override {
|
||||
// 清理音频系统...
|
||||
setState(ServiceState::Stopped);
|
||||
}
|
||||
|
||||
void playSound(const std::string& path) override {
|
||||
// 播放音效...
|
||||
}
|
||||
|
||||
void stopAll() override {
|
||||
// 停止所有音效...
|
||||
}
|
||||
};
|
||||
|
||||
// 注册服务
|
||||
Application::get().registerService<IAudioService>(makeShared<AudioService>());
|
||||
|
||||
// 使用服务
|
||||
auto audio = Application::get().getService<IAudioService>();
|
||||
audio->playSound("explosion.wav");
|
||||
```
|
||||
|
||||
### 测试时注入 Mock 服务
|
||||
|
||||
```cpp
|
||||
// 创建 Mock 服务
|
||||
class MockSceneService : public ISceneService {
|
||||
public:
|
||||
std::vector<std::string> sceneHistory;
|
||||
|
||||
void pushScene(Ptr<Scene> scene) override {
|
||||
sceneHistory.push_back("push:" + scene->getName());
|
||||
}
|
||||
|
||||
void popScene() override {
|
||||
sceneHistory.push_back("pop");
|
||||
}
|
||||
|
||||
// 实现其他必要方法...
|
||||
};
|
||||
|
||||
// 测试代码
|
||||
void testSceneNavigation() {
|
||||
// 注入 Mock 服务
|
||||
auto mockService = makeShared<MockSceneService>();
|
||||
ServiceLocator::instance().registerService<ISceneService>(mockService);
|
||||
|
||||
// 执行测试
|
||||
auto sceneService = ServiceLocator::instance().getService<ISceneService>();
|
||||
sceneService->pushScene(createTestScene("level1"));
|
||||
sceneService->pushScene(createTestScene("level2"));
|
||||
sceneService->popScene();
|
||||
|
||||
// 验证结果
|
||||
assert(mockService->sceneHistory.size() == 3);
|
||||
assert(mockService->sceneHistory[0] == "push:level1");
|
||||
assert(mockService->sceneHistory[1] == "push:level2");
|
||||
assert(mockService->sceneHistory[2] == "pop");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 创建新模块
|
||||
|
||||
### 步骤 1:定义配置类
|
||||
|
||||
```cpp
|
||||
// my_module.h
|
||||
class MyModuleConfig : public IModuleConfig {
|
||||
public:
|
||||
int someSetting = 42;
|
||||
std::string somePath;
|
||||
|
||||
ModuleInfo getModuleInfo() const override {
|
||||
ModuleInfo info;
|
||||
info.name = "MyModule";
|
||||
info.version = "1.0.0";
|
||||
info.priority = ModulePriority::Gameplay;
|
||||
info.priority = ModulePriority::Graphics;
|
||||
info.enabled = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::string getConfigSectionName() const override {
|
||||
return "my_module";
|
||||
}
|
||||
|
||||
bool validate() const override {
|
||||
return someSetting > 0;
|
||||
}
|
||||
|
||||
void resetToDefaults() override {
|
||||
someSetting = 42;
|
||||
somePath.clear();
|
||||
}
|
||||
|
||||
std::string getConfigSectionName() const override { return "my_module"; }
|
||||
bool validate() const override { return someSetting > 0; }
|
||||
void resetToDefaults() override { someSetting = 42; }
|
||||
bool loadFromJson(const void* jsonData) override;
|
||||
bool saveToJson(void* jsonData) const override;
|
||||
};
|
||||
|
|
@ -174,11 +421,10 @@ public:
|
|||
### 步骤 2:定义初始化器
|
||||
|
||||
```cpp
|
||||
// my_module.h
|
||||
class MyModuleInitializer : public IModuleInitializer {
|
||||
public:
|
||||
ModuleId getModuleId() const override { return moduleId_; }
|
||||
ModulePriority getPriority() const override { return ModulePriority::Gameplay; }
|
||||
ModulePriority getPriority() const override { return ModulePriority::Graphics; }
|
||||
std::vector<ModuleId> getDependencies() const override { return {}; }
|
||||
|
||||
bool initialize(const IModuleConfig* config) override {
|
||||
|
|
@ -196,9 +442,7 @@ public:
|
|||
|
||||
void shutdown() override {
|
||||
if (!initialized_) return;
|
||||
|
||||
// 执行清理逻辑...
|
||||
|
||||
initialized_ = false;
|
||||
E2D_LOG_INFO("MyModule shutdown");
|
||||
}
|
||||
|
|
@ -222,9 +466,7 @@ namespace extra2d {
|
|||
|
||||
static ModuleId s_myModuleId = INVALID_MODULE_ID;
|
||||
|
||||
ModuleId get_my_module_id() {
|
||||
return s_myModuleId;
|
||||
}
|
||||
ModuleId get_my_module_id() { return s_myModuleId; }
|
||||
|
||||
void register_my_module() {
|
||||
if (s_myModuleId != INVALID_MODULE_ID) return;
|
||||
|
|
@ -239,34 +481,10 @@ void register_my_module() {
|
|||
);
|
||||
}
|
||||
|
||||
// 自动注册
|
||||
namespace {
|
||||
struct MyModuleAutoRegister {
|
||||
MyModuleAutoRegister() {
|
||||
register_my_module();
|
||||
}
|
||||
};
|
||||
|
||||
static MyModuleAutoRegister s_autoRegister;
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
```
|
||||
|
||||
### 步骤 4:在 Application 中使用
|
||||
|
||||
```cpp
|
||||
// application.cpp
|
||||
#include <extra2d/my_module.h>
|
||||
|
||||
bool Application::init(const AppConfig& config) {
|
||||
// 注册模块
|
||||
register_my_module();
|
||||
|
||||
// 初始化所有模块
|
||||
return initModules();
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
## 内置模块
|
||||
|
||||
|
|
@ -283,11 +501,6 @@ config.fileOutput = true;
|
|||
config.logFilePath = "app.log";
|
||||
```
|
||||
|
||||
**平台支持**:
|
||||
- Windows:彩色控制台输出
|
||||
- Linux/macOS:ANSI 彩色输出
|
||||
- Switch:libnx console
|
||||
|
||||
---
|
||||
|
||||
### Config 模块
|
||||
|
|
@ -310,12 +523,6 @@ config.appConfig = AppConfig::createDefault();
|
|||
**平台特定操作**:
|
||||
- Switch:初始化 romfs 和 socket
|
||||
|
||||
**配置**:
|
||||
```cpp
|
||||
PlatformModuleConfig config;
|
||||
config.targetPlatform = PlatformType::Auto; // 自动检测
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Window 模块
|
||||
|
|
@ -330,7 +537,6 @@ config.targetPlatform = PlatformType::Auto; // 自动检测
|
|||
**配置**:
|
||||
```cpp
|
||||
WindowModuleConfig config;
|
||||
config.backend = "sdl2";
|
||||
config.windowConfig.title = "My App";
|
||||
config.windowConfig.width = 1280;
|
||||
config.windowConfig.height = 720;
|
||||
|
|
@ -352,17 +558,7 @@ config.targetFPS = 60;
|
|||
config.multisamples = 4;
|
||||
```
|
||||
|
||||
## 模块依赖
|
||||
|
||||
模块可以声明依赖关系:
|
||||
|
||||
```cpp
|
||||
std::vector<ModuleId> getDependencies() const override {
|
||||
return { get_window_module_id(), get_config_module_id() };
|
||||
}
|
||||
```
|
||||
|
||||
ModuleRegistry 会自动解析依赖并按正确顺序初始化。
|
||||
---
|
||||
|
||||
## 配置文件格式
|
||||
|
||||
|
|
@ -376,73 +572,70 @@ ModuleRegistry 会自动解析依赖并按正确顺序初始化。
|
|||
"fileOutput": false
|
||||
},
|
||||
"window": {
|
||||
"backend": "sdl2",
|
||||
"title": "My Application",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"vsync": true
|
||||
},
|
||||
"render": {
|
||||
"backend": "opengl",
|
||||
"targetFPS": 60,
|
||||
"multisamples": 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 单一职责
|
||||
|
||||
每个模块只负责一个明确的功能领域:
|
||||
|
||||
```
|
||||
✅ Logger Module → 只管理日志
|
||||
✅ Window Module → 只管理窗口
|
||||
❌ Game Module → 管理输入、渲染、音频(职责过多)
|
||||
```
|
||||
|
||||
### 2. 依赖最小化
|
||||
|
||||
模块应尽量减少对其他模块的依赖:
|
||||
### 1. 模块用于平台初始化,服务用于运行时功能
|
||||
|
||||
```cpp
|
||||
// 好的做法:通过接口获取信息
|
||||
std::vector<ModuleId> getDependencies() const override {
|
||||
return { get_window_module_id() }; // 只依赖窗口
|
||||
// 模块:平台级初始化
|
||||
class WindowModule : public IModuleInitializer {
|
||||
bool initialize(const IModuleConfig* config) override {
|
||||
// 创建窗口、初始化 OpenGL 上下文
|
||||
}
|
||||
};
|
||||
|
||||
// 服务:运行时功能
|
||||
class SceneService : public ISceneService {
|
||||
void update(float deltaTime) override {
|
||||
// 每帧更新场景
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 通过服务定位器解耦依赖
|
||||
|
||||
```cpp
|
||||
// 好的做法:通过服务定位器获取依赖
|
||||
void MyNode::onUpdate(float dt) {
|
||||
auto camera = ServiceLocator::instance().getService<ICameraService>();
|
||||
auto pos = camera->screenToWorld(mousePos);
|
||||
}
|
||||
|
||||
// 不好的做法:依赖过多
|
||||
std::vector<ModuleId> getDependencies() const override {
|
||||
return {
|
||||
get_window_module_id(),
|
||||
get_config_module_id(),
|
||||
get_logger_module_id(),
|
||||
get_render_module_id()
|
||||
};
|
||||
// 不好的做法:直接依赖 Application
|
||||
void MyNode::onUpdate(float dt) {
|
||||
auto& camera = Application::get().camera(); // 耦合度高
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 延迟初始化
|
||||
|
||||
资源在需要时才初始化,不是在构造函数中:
|
||||
### 3. 使用接口便于测试
|
||||
|
||||
```cpp
|
||||
bool initialize(const IModuleConfig* config) override {
|
||||
if (initialized_) return true;
|
||||
// 定义接口
|
||||
class IAudioService : public IService { ... };
|
||||
|
||||
// 在这里初始化资源
|
||||
resource_ = createResource();
|
||||
// 生产环境使用真实实现
|
||||
auto audio = makeShared<AudioService>();
|
||||
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
// 测试环境使用 Mock
|
||||
auto audio = makeShared<MockAudioService>();
|
||||
```
|
||||
|
||||
### 4. 安全关闭
|
||||
|
||||
关闭时要检查状态,避免重复关闭:
|
||||
|
||||
```cpp
|
||||
void shutdown() override {
|
||||
if (!initialized_) return; // 避免重复关闭
|
||||
|
|
@ -450,27 +643,12 @@ void shutdown() override {
|
|||
// 清理资源
|
||||
resource_.reset();
|
||||
|
||||
setState(ServiceState::Stopped);
|
||||
initialized_ = false;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 使用日志
|
||||
|
||||
在关键操作处添加日志:
|
||||
|
||||
```cpp
|
||||
bool initialize(const IModuleConfig* config) override {
|
||||
E2D_LOG_INFO("Initializing MyModule...");
|
||||
|
||||
if (!doSomething()) {
|
||||
E2D_LOG_ERROR("Failed to do something");
|
||||
return false;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("MyModule initialized successfully");
|
||||
return true;
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
## 调试
|
||||
|
||||
|
|
@ -488,18 +666,24 @@ for (ModuleId id : order) {
|
|||
}
|
||||
```
|
||||
|
||||
### 检查模块状态
|
||||
### 查看服务状态
|
||||
|
||||
```cpp
|
||||
auto* initializer = ModuleRegistry::instance().getInitializer(moduleId);
|
||||
if (initializer && initializer->isInitialized()) {
|
||||
E2D_LOG_INFO("Module is initialized");
|
||||
auto services = ServiceLocator::instance().getAllServices();
|
||||
for (const auto& service : services) {
|
||||
auto info = service->getServiceInfo();
|
||||
E2D_LOG_INFO("Service: {} (state: {})",
|
||||
info.name, static_cast<int>(info.state));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 示例
|
||||
|
||||
完整示例请参考:
|
||||
- [examples/basic/main.cpp](../../examples/basic/main.cpp) - 基础示例
|
||||
- [Extra2D/src/services/scene_service.cpp](../../Extra2D/src/services/scene_service.cpp) - Scene 服务实现
|
||||
- [Extra2D/src/services/event_service.cpp](../../Extra2D/src/services/event_service.cpp) - Event 服务实现
|
||||
- [Extra2D/src/platform/window_module.cpp](../../Extra2D/src/platform/window_module.cpp) - Window 模块实现
|
||||
- [Extra2D/src/graphics/render_module.cpp](../../Extra2D/src/graphics/render_module.cpp) - Render 模块实现
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
#include <extra2d/graphics/render_config.h>
|
||||
#include <extra2d/platform/window_config.h>
|
||||
#include <iostream>
|
||||
|
||||
using namespace extra2d;
|
||||
|
|
@ -15,45 +17,40 @@ using namespace extra2d;
|
|||
*
|
||||
* 初始化应用程序,创建场景,运行主循环。
|
||||
*/
|
||||
int main(int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
std::cout << "Extra2D Demo - Starting..." << std::endl;
|
||||
std::cout << "Extra2D Demo - Starting..." << std::endl;
|
||||
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.appName = "Extra2D Demo";
|
||||
config.appVersion = "1.0.0";
|
||||
config.window.title = "Extra2D Demo";
|
||||
config.window.width = 800;
|
||||
config.window.height = 600;
|
||||
config.window.mode = WindowMode::Windowed;
|
||||
config.window.resizable = true;
|
||||
config.window.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.appName = "Extra2D Demo";
|
||||
config.appVersion = "1.0.0";
|
||||
|
||||
Application& app = Application::get();
|
||||
Application &app = Application::get();
|
||||
|
||||
if (!app.init(config)) {
|
||||
std::cerr << "Failed to initialize application!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
if (!app.init(config)) {
|
||||
std::cerr << "Failed to initialize application!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::cout << "Application initialized successfully!" << std::endl;
|
||||
std::cout << "Window: " << app.window().width() << "x" << app.window().height() << std::endl;
|
||||
std::cout << "Running main loop. Press ESC or close window to exit." << std::endl;
|
||||
std::cout << "Application initialized successfully!" << std::endl;
|
||||
std::cout << "Window: " << app.window().width() << "x"
|
||||
<< app.window().height() << std::endl;
|
||||
std::cout << "Running main loop. Press ESC or close window to exit."
|
||||
<< std::endl;
|
||||
|
||||
auto scene = Scene::create();
|
||||
scene->setBackgroundColor(Colors::SkyBlue);
|
||||
scene->setViewportSize(static_cast<float>(config.window.width),
|
||||
static_cast<float>(config.window.height));
|
||||
app.enterScene(scene);
|
||||
auto scene = Scene::create();
|
||||
scene->setBackgroundColor(Colors::SkyBlue);
|
||||
scene->setViewportSize(static_cast<float>(app.window().width()),
|
||||
static_cast<float>(app.window().height()));
|
||||
app.enterScene(scene);
|
||||
|
||||
app.run();
|
||||
app.run();
|
||||
|
||||
std::cout << "Shutting down..." << std::endl;
|
||||
app.shutdown();
|
||||
std::cout << "Shutting down..." << std::endl;
|
||||
app.shutdown();
|
||||
|
||||
std::cout << "Goodbye!" << std::endl;
|
||||
return 0;
|
||||
std::cout << "Goodbye!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
55
xmake.lua
55
xmake.lua
|
|
@ -1,7 +1,14 @@
|
|||
-- ==============================================
|
||||
-- Extra2D - 2D Game Engine
|
||||
-- Build System: Xmake
|
||||
-- Platforms: MinGW (Windows), Nintendo Switch
|
||||
--
|
||||
-- 支持平台:
|
||||
-- - Windows (MinGW)
|
||||
-- - Linux
|
||||
-- - macOS
|
||||
-- - Nintendo Switch
|
||||
--
|
||||
-- 窗口后端: SDL2 (统一)
|
||||
-- ==============================================
|
||||
|
||||
-- 项目元信息
|
||||
|
|
@ -26,34 +33,37 @@ option("debug_logs")
|
|||
set_description("Enable debug logging")
|
||||
option_end()
|
||||
|
||||
option("backend")
|
||||
set_default("sdl2")
|
||||
set_showmenu(true)
|
||||
set_values("sdl2", "glfw")
|
||||
set_description("Platform backend (sdl2, glfw)")
|
||||
option_end()
|
||||
|
||||
-- ==============================================
|
||||
-- 平台检测与配置
|
||||
-- ==============================================
|
||||
|
||||
local host_plat = os.host()
|
||||
local target_plat = get_config("plat") or host_plat
|
||||
local supported_plats = {mingw = true, switch = true}
|
||||
local supported_plats = {mingw = true, windows = true, linux = true, macosx = true, switch = true}
|
||||
|
||||
-- 自动选择平台
|
||||
if not supported_plats[target_plat] then
|
||||
if host_plat == "windows" then
|
||||
target_plat = "mingw"
|
||||
elseif host_plat == "linux" then
|
||||
target_plat = "linux"
|
||||
elseif host_plat == "macosx" then
|
||||
target_plat = "macosx"
|
||||
else
|
||||
error("Unsupported platform: " .. target_plat .. ". Supported platforms: mingw, switch")
|
||||
error("Unsupported platform: " .. target_plat .. ". Supported platforms: mingw, windows, linux, macosx, switch")
|
||||
end
|
||||
end
|
||||
|
||||
set_plat(target_plat)
|
||||
|
||||
-- 设置架构
|
||||
if target_plat == "switch" then
|
||||
set_arch("arm64")
|
||||
elseif target_plat == "mingw" then
|
||||
elseif target_plat == "mingw" or target_plat == "windows" then
|
||||
set_arch("x86_64")
|
||||
elseif target_plat == "linux" then
|
||||
set_arch("x86_64")
|
||||
elseif target_plat == "macosx" then
|
||||
set_arch("x86_64")
|
||||
end
|
||||
|
||||
|
|
@ -69,20 +79,13 @@ elseif target_plat == "mingw" then
|
|||
end
|
||||
|
||||
-- ==============================================
|
||||
-- 添加依赖包 (MinGW)
|
||||
-- 添加依赖包
|
||||
-- ==============================================
|
||||
|
||||
if target_plat == "mingw" then
|
||||
local backend = get_config("backend") or "sdl2"
|
||||
|
||||
if target_plat ~= "switch" then
|
||||
add_requires("glm")
|
||||
add_requires("nlohmann_json")
|
||||
|
||||
if backend == "sdl2" then
|
||||
add_requires("libsdl2")
|
||||
elseif backend == "glfw" then
|
||||
add_requires("glfw")
|
||||
end
|
||||
add_requires("libsdl2")
|
||||
end
|
||||
|
||||
-- ==============================================
|
||||
|
|
@ -134,10 +137,16 @@ target("demo_basic")
|
|||
add_files("examples/basic/main.cpp")
|
||||
|
||||
-- 平台配置
|
||||
local target_plat = get_config("plat") or os.host()
|
||||
if target_plat == "mingw" then
|
||||
local plat = get_config("plat") or os.host()
|
||||
if plat == "mingw" or plat == "windows" then
|
||||
add_packages("glm", "nlohmann_json", "libsdl2")
|
||||
add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi")
|
||||
elseif plat == "linux" then
|
||||
add_packages("glm", "nlohmann_json", "libsdl2")
|
||||
add_syslinks("GL", "dl", "pthread")
|
||||
elseif plat == "macosx" then
|
||||
add_packages("glm", "nlohmann_json", "libsdl2")
|
||||
add_frameworks("OpenGL", "Cocoa", "IOKit", "CoreVideo")
|
||||
end
|
||||
|
||||
-- 构建后安装Shader文件
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
-- ==============================================
|
||||
-- Extra2D 引擎库共享配置
|
||||
-- 被主项目和示例共享使用
|
||||
--
|
||||
-- 窗口后端统一使用 SDL2,支持以下平台:
|
||||
-- - Windows (MinGW)
|
||||
-- - Linux
|
||||
-- - macOS
|
||||
-- - Nintendo Switch
|
||||
-- ==============================================
|
||||
|
||||
-- 获取当前平台
|
||||
|
|
@ -8,11 +14,6 @@ local function get_current_plat()
|
|||
return get_config("plat") or os.host()
|
||||
end
|
||||
|
||||
-- 获取后端配置
|
||||
local function get_backend()
|
||||
return get_config("backend") or "sdl2"
|
||||
end
|
||||
|
||||
-- 定义 Extra2D 引擎库目标
|
||||
function define_extra2d_engine()
|
||||
target("extra2d")
|
||||
|
|
@ -22,42 +23,40 @@ function define_extra2d_engine()
|
|||
add_files("Extra2D/src/**.cpp")
|
||||
add_files("Extra2D/src/glad/glad.c")
|
||||
|
||||
-- 平台后端源文件
|
||||
local plat = get_current_plat()
|
||||
local backend = get_backend()
|
||||
|
||||
if plat == "switch" then
|
||||
add_files("Extra2D/src/platform/backends/switch/*.cpp")
|
||||
add_defines("E2D_BACKEND_SWITCH")
|
||||
elseif backend == "sdl2" then
|
||||
add_files("Extra2D/src/platform/backends/sdl2/*.cpp")
|
||||
add_defines("E2D_BACKEND_SDL2")
|
||||
elseif backend == "glfw" then
|
||||
add_files("Extra2D/src/platform/backends/glfw/*.cpp")
|
||||
add_defines("E2D_BACKEND_GLFW")
|
||||
end
|
||||
-- SDL2 后端源文件(所有平台统一使用)
|
||||
add_files("Extra2D/src/platform/backends/sdl2/*.cpp")
|
||||
add_defines("E2D_BACKEND_SDL2")
|
||||
|
||||
-- 头文件路径
|
||||
add_includedirs("Extra2D/include", {public = true})
|
||||
add_includedirs("Extra2D/include/extra2d/platform", {public = true})
|
||||
|
||||
-- 平台配置
|
||||
local plat = get_current_plat()
|
||||
|
||||
if plat == "switch" then
|
||||
-- Nintendo Switch 平台配置
|
||||
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
|
||||
add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true})
|
||||
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
|
||||
add_syslinks("SDL2", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
||||
add_syslinks("SDL2", "GLESv2", "EGL", "glapi", "drm_nouveau", "nx", "m",
|
||||
{public = true})
|
||||
elseif plat == "mingw" then
|
||||
elseif plat == "mingw" or plat == "windows" then
|
||||
-- Windows (MinGW) 平台配置
|
||||
add_packages("glm", "nlohmann_json", {public = true})
|
||||
|
||||
if backend == "sdl2" then
|
||||
add_packages("libsdl2", {public = true})
|
||||
elseif backend == "glfw" then
|
||||
add_packages("glfw", {public = true})
|
||||
end
|
||||
|
||||
add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi", {public = true})
|
||||
add_packages("libsdl2", {public = true})
|
||||
add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi",
|
||||
{public = true})
|
||||
elseif plat == "linux" then
|
||||
-- Linux 平台配置
|
||||
add_packages("glm", "nlohmann_json", {public = true})
|
||||
add_packages("libsdl2", {public = true})
|
||||
add_syslinks("GL", "dl", "pthread", {public = true})
|
||||
elseif plat == "macosx" then
|
||||
-- macOS 平台配置
|
||||
add_packages("glm", "nlohmann_json", {public = true})
|
||||
add_packages("libsdl2", {public = true})
|
||||
add_frameworks("OpenGL", "Cocoa", "IOKit", "CoreVideo", {public = true})
|
||||
end
|
||||
|
||||
-- 编译器标志 (C 和 C++ 共用)
|
||||
|
|
|
|||
Loading…
Reference in New Issue