diff --git a/Extra2D/include/extra2d/extra2d.h b/Extra2D/include/extra2d/extra2d.h index c6b04ec..c9aa31b 100644 --- a/Extra2D/include/extra2d/extra2d.h +++ b/Extra2D/include/extra2d/extra2d.h @@ -22,7 +22,9 @@ #include #include #include +#include #include +#include // Graphics #include @@ -54,6 +56,12 @@ #include #include +// Services +#include +#include +#include +#include + // Application #include diff --git a/Extra2D/src/app/application.cpp b/Extra2D/src/app/application.cpp index 89ea356..13e6a29 100644 --- a/Extra2D/src/app/application.cpp +++ b/Extra2D/src/app/application.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,7 @@ bool Application::init(const AppConfig& config) { register_config_module(); register_platform_module(); register_window_module(); + register_input_module(); register_render_module(); auto* configInit = ModuleRegistry::instance().getInitializer(get_config_module_id()); @@ -74,6 +76,7 @@ bool Application::init(const std::string& configPath) { register_config_module(); register_platform_module(); register_window_module(); + register_input_module(); register_render_module(); auto* configInit = ModuleRegistry::instance().getInitializer(get_config_module_id()); @@ -90,7 +93,7 @@ bool Application::init(const std::string& configPath) { void Application::registerCoreServices() { auto& locator = ServiceLocator::instance(); - if (!locator.hasService()) { + if (!locator.hasService()) { locator.registerService(makeShared()); } @@ -98,10 +101,6 @@ void Application::registerCoreServices() { locator.registerService(makeShared()); } - if (!locator.hasService()) { - locator.registerService(makeShared()); - } - if (!locator.hasService()) { auto cameraService = makeShared(); if (window_) { @@ -119,6 +118,12 @@ void Application::registerCoreServices() { } bool Application::initModules() { + auto& locator = ServiceLocator::instance(); + + if (!locator.hasService()) { + locator.registerService(makeShared()); + } + auto initOrder = ModuleRegistry::instance().getInitializationOrder(); for (ModuleId moduleId : initOrder) { diff --git a/Extra2D/src/graphics/opengl/gl_renderer.cpp b/Extra2D/src/graphics/opengl/gl_renderer.cpp index e7daa8c..6452568 100644 --- a/Extra2D/src/graphics/opengl/gl_renderer.cpp +++ b/Extra2D/src/graphics/opengl/gl_renderer.cpp @@ -698,9 +698,15 @@ void GLRenderer::addShapeVertex(float x, float y, const Color &color) { if (shapeVertexCount_ >= MAX_SHAPE_VERTICES) { flushShapeBatch(); } + + glm::vec4 pos(x, y, 0.0f, 1.0f); + if (!transformStack_.empty()) { + pos = transformStack_.back() * pos; + } + ShapeVertex &v = shapeVertexCache_[shapeVertexCount_++]; - v.x = x; - v.y = y; + v.x = pos.x; + v.y = pos.y; v.r = color.r; v.g = color.g; v.b = color.b; @@ -717,9 +723,15 @@ void GLRenderer::addLineVertex(float x, float y, const Color &color) { if (lineVertexCount_ >= MAX_LINE_VERTICES) { flushLineBatch(); } + + glm::vec4 pos(x, y, 0.0f, 1.0f); + if (!transformStack_.empty()) { + pos = transformStack_.back() * pos; + } + ShapeVertex &v = lineVertexCache_[lineVertexCount_++]; - v.x = x; - v.y = y; + v.x = pos.x; + v.y = pos.y; v.r = color.r; v.g = color.g; v.b = color.b; diff --git a/Extra2D/src/platform/input_module.cpp b/Extra2D/src/platform/input_module.cpp index 9888efc..6babecc 100644 --- a/Extra2D/src/platform/input_module.cpp +++ b/Extra2D/src/platform/input_module.cpp @@ -1,10 +1,13 @@ #include #include +#include #include #include #include #include +#include "backends/sdl2/sdl2_input.h" + using json = nlohmann::json; namespace extra2d { @@ -146,6 +149,20 @@ bool InputModuleInitializer::initialize(const IModuleConfig* config) { return false; } + SDL2Input* sdl2Input = dynamic_cast(input_); + if (sdl2Input) { + auto eventService = ServiceLocator::instance().getService(); + if (eventService) { + sdl2Input->setEventCallback([eventService](const Event& event) { + Event mutableEvent = event; + eventService->dispatch(mutableEvent); + }); + E2D_LOG_INFO("Input events connected to EventService"); + } else { + E2D_LOG_WARN("EventService not available - input events will not be dispatched"); + } + } + initialized_ = true; E2D_LOG_INFO("Input module initialized"); E2D_LOG_INFO(" Deadzone: {}", config_.deadzone); diff --git a/Extra2D/src/scene/node.cpp b/Extra2D/src/scene/node.cpp index 1054c0d..2e5b06f 100644 --- a/Extra2D/src/scene/node.cpp +++ b/Extra2D/src/scene/node.cpp @@ -520,11 +520,15 @@ void Node::onRender(RenderBackend &renderer) { if (!visible_) return; + renderer.pushTransform(getLocalTransform()); + onDraw(renderer); for (auto &child : children_) { child->onRender(renderer); } + + renderer.popTransform(); } /** diff --git a/Extra2D/src/scene/shape_node.cpp b/Extra2D/src/scene/shape_node.cpp index 98ca690..95c8a88 100644 --- a/Extra2D/src/scene/shape_node.cpp +++ b/Extra2D/src/scene/shape_node.cpp @@ -268,25 +268,24 @@ Rect ShapeNode::getBounds() const { * @param renderer 渲染后端引用 * * 根据形状类型调用相应的渲染方法进行绘制 + * 注意:变换矩阵已由 Node::onRender 通过 pushTransform 应用, + * 此处直接使用本地坐标即可。 */ void ShapeNode::onDraw(RenderBackend &renderer) { if (points_.empty()) { return; } - Vec2 offset = getPosition(); - switch (shapeType_) { case ShapeType::Point: if (!points_.empty()) { - renderer.fillCircle(points_[0] + offset, lineWidth_ * 0.5f, color_, 8); + renderer.fillCircle(points_[0], lineWidth_ * 0.5f, color_, 8); } break; case ShapeType::Line: if (points_.size() >= 2) { - renderer.drawLine(points_[0] + offset, points_[1] + offset, color_, - lineWidth_); + renderer.drawLine(points_[0], points_[1], color_, lineWidth_); } break; @@ -295,11 +294,11 @@ void ShapeNode::onDraw(RenderBackend &renderer) { if (filled_) { Rect rect(points_[0].x, points_[0].y, points_[2].x - points_[0].x, points_[2].y - points_[0].y); - renderer.fillRect(Rect(rect.origin + offset, rect.size), color_); + renderer.fillRect(rect, color_); } else { for (size_t i = 0; i < points_.size(); ++i) { - Vec2 start = points_[i] + offset; - Vec2 end = points_[(i + 1) % points_.size()] + offset; + Vec2 start = points_[i]; + Vec2 end = points_[(i + 1) % points_.size()]; renderer.drawLine(start, end, color_, lineWidth_); } } @@ -310,41 +309,31 @@ void ShapeNode::onDraw(RenderBackend &renderer) { if (points_.size() >= 2) { float radius = points_[1].x; if (filled_) { - renderer.fillCircle(points_[0] + offset, radius, color_, segments_); + renderer.fillCircle(points_[0], radius, color_, segments_); } else { - renderer.drawCircle(points_[0] + offset, radius, color_, segments_, - lineWidth_); + renderer.drawCircle(points_[0], radius, color_, segments_, lineWidth_); } } break; case ShapeType::Triangle: if (points_.size() >= 3) { - Vec2 p1 = points_[0] + offset; - Vec2 p2 = points_[1] + offset; - Vec2 p3 = points_[2] + offset; if (filled_) { - renderer.fillTriangle(p1, p2, p3, color_); + renderer.fillTriangle(points_[0], points_[1], points_[2], color_); } else { - renderer.drawLine(p1, p2, color_, lineWidth_); - renderer.drawLine(p2, p3, color_, lineWidth_); - renderer.drawLine(p3, p1, color_, lineWidth_); + renderer.drawLine(points_[0], points_[1], color_, lineWidth_); + renderer.drawLine(points_[1], points_[2], color_, lineWidth_); + renderer.drawLine(points_[2], points_[0], color_, lineWidth_); } } break; case ShapeType::Polygon: if (!points_.empty()) { - std::vector transformedPoints; - transformedPoints.reserve(points_.size()); - for (const auto &p : points_) { - transformedPoints.push_back(p + offset); - } - if (filled_) { - renderer.fillPolygon(transformedPoints, color_); + renderer.fillPolygon(points_, color_); } else { - renderer.drawPolygon(transformedPoints, color_, lineWidth_); + renderer.drawPolygon(points_, color_, lineWidth_); } } break; diff --git a/docs/module_system.md b/docs/module_system.md index 8087ffc..033381f 100644 --- a/docs/module_system.md +++ b/docs/module_system.md @@ -6,9 +6,9 @@ Extra2D 采用模块化架构设计,所有核心功能通过模块系统和服 - **统一的生命周期管理**:初始化、关闭、依赖处理 - **优先级排序**:确保模块/服务按正确顺序初始化 -- **配置驱动**:每个模块可独立配置 +- **模块化配置**:每个模块独立管理自己的配置 - **依赖注入**:通过服务定位器解耦模块间依赖 -- **可测试性**:支持 Mock 服务进行单元测试 +- **可扩展性**:新增模块无需修改引擎核心代码 ## 架构图 @@ -31,25 +31,96 @@ Extra2D 采用模块化架构设计,所有核心功能通过模块系统和服 │ Config │ │ Window │ │ Scene │ │ Timer │ │ Module │ │ Module │ │ Service │ │ Service │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ - │ │ - ┌─────┴─────┐ ┌─────┴─────┐ - ▼ ▼ ▼ ▼ - ┌──────────┐ ┌──────────┐ ┌──────────┐ - │ Event │ │ Camera │ │ ... │ - │ Service │ │ Service │ │ │ - └──────────┘ └──────────┘ └──────────┘ ``` +## 模块化配置系统 + +### 设计原则 + +Extra2D 采用**模块化配置系统**,遵循开闭原则: + +- **AppConfig** 只包含应用级别配置(appName, appVersion, organization 等) +- **各模块配置** 由模块自己管理,实现 `IModuleConfig` 接口 +- **新增模块** 无需修改引擎核心代码 + +### 配置文件结构 + +``` +Extra2D/include/extra2d/ +├── config/ +│ ├── app_config.h # 应用级别配置 +│ ├── module_config.h # 模块配置接口 +│ └── config_manager.h # 配置管理器 +├── platform/ +│ ├── window_config.h # 窗口模块配置 +│ └── input_config.h # 输入模块配置 +├── graphics/ +│ └── render_config.h # 渲染模块配置 +├── audio/ +│ └── audio_config.h # 音频模块配置 +├── debug/ +│ └── debug_config.h # 调试模块配置 +└── resource/ + └── resource_config.h # 资源模块配置 +``` + +### AppConfig 结构 + +```cpp +struct AppConfig { + std::string appName = "Extra2D App"; + std::string appVersion = "1.0.0"; + std::string organization = ""; + std::string configFile = "config.json"; + PlatformType targetPlatform = PlatformType::Auto; + + static AppConfig createDefault(); + bool validate() const; + void reset(); + void merge(const AppConfig& other); +}; +``` + +### 模块配置示例 + +```cpp +// window_config.h +struct WindowConfigData { + std::string title = "Extra2D Application"; + int width = 1280; + int height = 720; + WindowMode mode = WindowMode::Windowed; + bool vsync = true; + bool resizable = true; + // ... +}; + +// window_module.h +class WindowModuleConfig : public IModuleConfig { +public: + WindowConfigData windowConfig; + + ModuleInfo getModuleInfo() const override; + std::string getConfigSectionName() const override { return "window"; } + bool validate() const override; + void applyPlatformConstraints(PlatformType platform) override; + bool loadFromJson(const void* jsonData) override; + bool saveToJson(void* jsonData) const override; +}; +``` + +--- + ## 模块 vs 服务 | 特性 | 模块 (Module) | 服务 (Service) | |-----|--------------|---------------| | 用途 | 平台级初始化 | 运行时功能 | | 生命周期 | Application 管理 | ServiceLocator 管理 | +| 配置管理 | 独立配置文件 | 无配置 | | 依赖方式 | 通过 ModuleRegistry | 通过 ServiceLocator | | 可替换性 | 编译时确定 | 运行时可替换 | -| 测试支持 | 需要重构 | 原生支持 Mock | -| 示例 | Window, Render, Config | Scene, Timer, Event, Camera | +| 示例 | Window, Render, Input | Scene, Timer, Event, Camera | ## 模块优先级 @@ -57,25 +128,12 @@ Extra2D 采用模块化架构设计,所有核心功能通过模块系统和服 | 优先级值 | 枚举名称 | 用途 | 模块示例 | |---------|---------|------|---------| -| 0 | `Core` | 核心模块,最先初始化 | Logger, Config, Platform, Window | +| 0 | `Core` | 核心模块,最先初始化 | Config, Platform, Window | +| 50 | `Input` | 输入系统 | Input | | 100 | `Graphics` | 图形渲染 | Render | | 200 | `Audio` | 音频系统 | Audio | | 500 | `Resource` | 资源管理 | Resource | -## 服务优先级 - -服务按优先级从小到大初始化: - -| 优先级值 | 枚举名称 | 用途 | 服务示例 | -|---------|---------|------|---------| -| 0 | `Core` | 核心服务 | - | -| 100 | `Event` | 事件系统 | EventService | -| 200 | `Timer` | 计时器 | TimerService | -| 300 | `Scene` | 场景管理 | SceneService | -| 400 | `Camera` | 相机系统 | CameraService | -| 500 | `Resource` | 资源管理 | - | -| 600 | `Audio` | 音频系统 | - | - --- ## 模块系统 @@ -134,12 +192,353 @@ public: IModuleConfig* getModuleConfig(ModuleId id); IModuleInitializer* getInitializer(ModuleId id); + std::vector getAllModules() const; std::vector getInitializationOrder() const; }; ``` --- +## 创建新模块 + +### 步骤 1:定义配置数据结构 + +```cpp +// my_module_config.h +#pragma once + +#include + +namespace extra2d { + +struct MyModuleConfigData { + int someSetting = 42; + bool enabled = true; + std::string path = "default"; +}; + +} +``` + +### 步骤 2:定义配置类 + +```cpp +// my_module.h +#pragma once + +#include +#include +#include "my_module_config.h" + +namespace extra2d { + +class MyModuleConfig : public IModuleConfig { +public: + MyModuleConfigData config; + + ModuleInfo getModuleInfo() const override { + ModuleInfo info; + info.id = 0; + info.name = "MyModule"; + info.version = "1.0.0"; + info.priority = ModulePriority::Graphics; + info.enabled = true; + return info; + } + + std::string getConfigSectionName() const override { return "my_module"; } + bool validate() const override { return config.someSetting > 0; } + + void resetToDefaults() override { + config = MyModuleConfigData{}; + } + + void applyPlatformConstraints(PlatformType platform) override { +#ifdef __SWITCH__ + config.someSetting = 30; // Switch 平台优化 +#else + (void)platform; +#endif + } + + bool loadFromJson(const void* jsonData) override; + bool saveToJson(void* jsonData) const override; +}; + +} +``` + +### 步骤 3:定义初始化器 + +```cpp +// my_module.h (续) +class MyModuleInitializer : public IModuleInitializer { +public: + MyModuleInitializer(); + ~MyModuleInitializer() override; + + ModuleId getModuleId() const override { return moduleId_; } + ModulePriority getPriority() const override { return ModulePriority::Graphics; } + std::vector getDependencies() const override; + + bool initialize(const IModuleConfig* config) override; + void shutdown() override; + bool isInitialized() const override { return initialized_; } + + void setModuleId(ModuleId id) { moduleId_ = id; } + +private: + ModuleId moduleId_ = INVALID_MODULE_ID; + bool initialized_ = false; + MyModuleConfigData config_; +}; + +ModuleId get_my_module_id(); +void register_my_module(); +``` + +### 步骤 4:实现模块 + +```cpp +// my_module.cpp +#include "my_module.h" +#include +#include +#include + +using json = nlohmann::json; + +namespace extra2d { + +static ModuleId s_myModuleId = INVALID_MODULE_ID; + +ModuleId get_my_module_id() { return s_myModuleId; } + +bool MyModuleConfig::loadFromJson(const void* jsonData) { + if (!jsonData) return false; + try { + const json& j = *static_cast(jsonData); + if (j.contains("someSetting")) config.someSetting = j["someSetting"].get(); + if (j.contains("enabled")) config.enabled = j["enabled"].get(); + if (j.contains("path")) config.path = j["path"].get(); + return true; + } catch (...) { + return false; + } +} + +bool MyModuleConfig::saveToJson(void* jsonData) const { + if (!jsonData) return false; + try { + json& j = *static_cast(jsonData); + j["someSetting"] = config.someSetting; + j["enabled"] = config.enabled; + j["path"] = config.path; + return true; + } catch (...) { + return false; + } +} + +MyModuleInitializer::MyModuleInitializer() : moduleId_(INVALID_MODULE_ID), initialized_(false) {} + +MyModuleInitializer::~MyModuleInitializer() { if (initialized_) shutdown(); } + +std::vector MyModuleInitializer::getDependencies() const { + return {}; // 无依赖 +} + +bool MyModuleInitializer::initialize(const IModuleConfig* config) { + if (initialized_) return true; + + const MyModuleConfig* cfg = dynamic_cast(config); + if (!cfg) { + E2D_LOG_ERROR("Invalid MyModule config"); + return false; + } + + config_ = cfg->config; + + // 执行初始化逻辑... + + initialized_ = true; + E2D_LOG_INFO("MyModule initialized with setting: {}", config_.someSetting); + return true; +} + +void MyModuleInitializer::shutdown() { + if (!initialized_) return; + // 执行清理逻辑... + initialized_ = false; + E2D_LOG_INFO("MyModule shutdown"); +} + +void register_my_module() { + if (s_myModuleId != INVALID_MODULE_ID) return; + + s_myModuleId = ModuleRegistry::instance().registerModule( + makeUnique(), + []() -> UniquePtr { + auto initializer = makeUnique(); + initializer->setModuleId(s_myModuleId); + return initializer; + } + ); +} + +namespace { + struct MyModuleAutoRegister { + MyModuleAutoRegister() { register_my_module(); } + }; + static MyModuleAutoRegister s_autoRegister; +} + +} +``` + +--- + +## 内置模块 + +### Config 模块 + +**职责**:管理 ConfigManager 和应用配置 + +**配置**: +```cpp +AppConfig config; +config.appName = "My Application"; +config.appVersion = "1.0.0"; +``` + +--- + +### Platform 模块 + +**职责**:平台检测和平台特定初始化 + +**支持平台**: +- Windows +- Linux +- macOS +- Nintendo Switch + +**平台能力查询**: +```cpp +auto* platformConfig = createPlatformConfig(); +const auto& caps = platformConfig->capabilities(); +if (caps.supportsGamepad) { + // 支持手柄 +} +``` + +--- + +### Window 模块 + +**职责**:窗口创建和管理 + +**后端**:统一使用 SDL2,支持所有平台 + +**配置**: +```cpp +WindowConfigData config; +config.title = "My App"; +config.width = 1280; +config.height = 720; +config.mode = WindowMode::Windowed; +config.vsync = true; +``` + +**平台约束**: +- Switch 平台自动强制全屏模式 + +--- + +### Input 模块 + +**职责**:输入设备管理(键盘、鼠标、手柄) + +**配置**: +```cpp +InputConfigData config; +config.deadzone = 0.15f; +config.mouseSensitivity = 1.0f; +config.enableVibration = true; +``` + +**使用示例**: +```cpp +IInput* input = app.window().input(); + +// 键盘 +if (input->pressed(Key::Space)) { + // 空格键刚按下 +} + +// 鼠标 +if (input->down(Mouse::Left)) { + Vec2 pos = input->mouse(); +} + +// 手柄 +if (input->gamepad()) { + Vec2 stick = input->leftStick(); + if (input->pressed(Gamepad::A)) { + input->vibrate(0.5f, 0.5f); // 振动反馈 + } +} +``` + +--- + +### Render 模块 + +**职责**:渲染器初始化和管理 + +**配置**: +```cpp +RenderConfigData config; +config.backend = BackendType::OpenGL; +config.vsync = true; +config.targetFPS = 60; +config.multisamples = 4; +``` + +--- + +## 配置文件格式 + +配置使用 JSON 格式,每个模块有独立的配置节: + +```json +{ + "app": { + "name": "My Application", + "version": "1.0.0", + "organization": "MyCompany" + }, + "window": { + "title": "My Application", + "width": 1280, + "height": 720, + "mode": "windowed", + "vsync": true + }, + "render": { + "targetFPS": 60, + "multisamples": 4 + }, + "input": { + "deadzone": 0.15, + "mouseSensitivity": 1.0, + "enableVibration": true + } +} +``` + +--- + ## 服务系统 ### IService @@ -154,8 +553,6 @@ public: 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; @@ -164,126 +561,22 @@ public: }; ``` -### ServiceLocator - -服务定位器,实现依赖注入和服务发现: - -```cpp -class ServiceLocator { -public: - static ServiceLocator& instance(); - - // 注册服务实例 - template - void registerService(SharedPtr service); - - // 注册服务工厂(延迟创建) - template - void registerFactory(ServiceFactory factory); - - // 获取服务 - template - SharedPtr getService() const; - - // 检查服务是否存在 - template - bool hasService() const; - - // 批量操作 - bool initializeAll(); - void shutdownAll(); - void updateAll(float deltaTime); - void pauseAll(); - void resumeAll(); - void clear(); -}; -``` - ### 内置服务 -#### SceneService +| 服务 | 用途 | 优先级 | +|-----|------|-------| +| SceneService | 场景管理 | 300 | +| TimerService | 计时器 | 200 | +| EventService | 事件分发 | 100 | +| CameraService | 相机系统 | 400 | -场景管理服务,包装 SceneManager: - -```cpp -class ISceneService : public IService { -public: - virtual void runWithScene(Ptr scene) = 0; - virtual void replaceScene(Ptr scene) = 0; - virtual void pushScene(Ptr scene) = 0; - virtual void popScene() = 0; - - virtual Ptr 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); @@ -294,357 +587,146 @@ timerService->addTimer(1.0f, []() { }); // 使用事件服务 -eventService->addListener(EventType::KeyDown, [](Event& e) { - E2D_LOG_INFO("Key pressed: {}", e.key.keycode); +eventService->addListener(EventType::KeyPressed, [](Event& e) { + auto& keyEvent = std::get(e.data); + E2D_LOG_INFO("Key pressed: {}", keyEvent.keyCode); +}); +``` + +--- + +## 输入事件系统 + +### 事件类型 + +```cpp +enum class EventType { + // 键盘 + KeyPressed, + KeyReleased, + KeyRepeat, + + // 鼠标 + MouseButtonPressed, + MouseButtonReleased, + MouseMoved, + MouseScrolled, + + // 手柄 + GamepadConnected, + GamepadDisconnected, + GamepadButtonPressed, + GamepadButtonReleased, + + // 触摸 + TouchBegan, + TouchMoved, + TouchEnded, + + // 窗口 + WindowResize, + WindowClose, + // ... +}; +``` + +### 事件监听 + +```cpp +auto eventService = Application::get().events(); + +// 监听键盘事件 +eventService->addListener(EventType::KeyPressed, [](Event& e) { + auto& key = std::get(e.data); + E2D_LOG_INFO("Key: {}, mods: {}", key.keyCode, key.mods); }); -// 使用相机服务 -cameraService->setPosition(Vec2(100.0f, 200.0f)); -cameraService->setZoom(2.0f); +// 监听鼠标事件 +eventService->addListener(EventType::MouseButtonPressed, [](Event& e) { + auto& mouse = std::get(e.data); + E2D_LOG_INFO("Mouse button: {} at ({}, {})", + mouse.button, mouse.position.x, mouse.position.y); +}); ``` -### 注册自定义服务 +--- + +## 平台支持 + +### 支持的平台 + +| 平台 | 窗口后端 | 图形 API | 特殊处理 | +|-----|---------|---------|---------| +| Windows | SDL2 | OpenGL ES 3.2 | - | +| Linux | SDL2 | OpenGL ES 3.2 | - | +| macOS | SDL2 | OpenGL ES 3.2 | - | +| Nintendo Switch | SDL2 | OpenGL ES 3.2 | romfs, 强制全屏 | + +### 平台检测 ```cpp -// 定义服务接口 -class IAudioService : public IService { -public: - virtual void playSound(const std::string& path) = 0; - virtual void stopAll() = 0; -}; +PlatformType platform = PlatformDetector::detect(); +const char* name = getPlatformTypeName(platform); -// 实现服务 -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(makeShared()); - -// 使用服务 -auto audio = Application::get().getService(); -audio->playSound("explosion.wav"); -``` - -### 测试时注入 Mock 服务 - -```cpp -// 创建 Mock 服务 -class MockSceneService : public ISceneService { -public: - std::vector sceneHistory; - - void pushScene(Ptr scene) override { - sceneHistory.push_back("push:" + scene->getName()); - } - - void popScene() override { - sceneHistory.push_back("pop"); - } - - // 实现其他必要方法... -}; - -// 测试代码 -void testSceneNavigation() { - // 注入 Mock 服务 - auto mockService = makeShared(); - ServiceLocator::instance().registerService(mockService); - - // 执行测试 - auto sceneService = ServiceLocator::instance().getService(); - 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"); +switch (platform) { + case PlatformType::Windows: // Windows 处理 + case PlatformType::Switch: // Switch 处理 + // ... } ``` ---- - -## 创建新模块 - -### 步骤 1:定义配置类 +### 平台能力 ```cpp -class MyModuleConfig : public IModuleConfig { -public: - int someSetting = 42; - - ModuleInfo getModuleInfo() const override { - ModuleInfo info; - info.name = "MyModule"; - info.version = "1.0.0"; - 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; } - bool loadFromJson(const void* jsonData) override; - bool saveToJson(void* jsonData) const override; -}; -``` +auto* config = createPlatformConfig(); +const auto& caps = config->capabilities(); -### 步骤 2:定义初始化器 - -```cpp -class MyModuleInitializer : public IModuleInitializer { -public: - ModuleId getModuleId() const override { return moduleId_; } - ModulePriority getPriority() const override { return ModulePriority::Graphics; } - std::vector getDependencies() const override { return {}; } - - bool initialize(const IModuleConfig* config) override { - if (initialized_) return true; - - const MyModuleConfig* cfg = dynamic_cast(config); - if (!cfg) return false; - - // 执行初始化逻辑... - - initialized_ = true; - E2D_LOG_INFO("MyModule initialized"); - return true; - } - - void shutdown() override { - if (!initialized_) return; - // 执行清理逻辑... - initialized_ = false; - E2D_LOG_INFO("MyModule shutdown"); - } - - bool isInitialized() const override { return initialized_; } - void setModuleId(ModuleId id) { moduleId_ = id; } - -private: - ModuleId moduleId_ = INVALID_MODULE_ID; - bool initialized_ = false; -}; -``` - -### 步骤 3:注册模块 - -```cpp -// my_module.cpp -#include - -namespace extra2d { - -static ModuleId s_myModuleId = INVALID_MODULE_ID; - -ModuleId get_my_module_id() { return s_myModuleId; } - -void register_my_module() { - if (s_myModuleId != INVALID_MODULE_ID) return; - - s_myModuleId = ModuleRegistry::instance().registerModule( - makeUnique(), - []() -> UniquePtr { - auto initializer = makeUnique(); - initializer->setModuleId(s_myModuleId); - return initializer; - } - ); -} - -} // namespace extra2d -``` - ---- - -## 内置模块 - -### Logger 模块 - -**职责**:管理日志系统初始化和关闭 - -**配置**: -```cpp -LoggerModuleConfig config; -config.logLevel = LogLevel::Info; -config.consoleOutput = true; -config.fileOutput = true; -config.logFilePath = "app.log"; -``` - ---- - -### Config 模块 - -**职责**:管理 ConfigManager 和应用配置 - -**配置**: -```cpp -ConfigModuleConfig config; -config.configPath = "config.json"; -config.appConfig = AppConfig::createDefault(); -``` - ---- - -### Platform 模块 - -**职责**:平台检测和平台特定初始化 - -**平台特定操作**: -- Switch:初始化 romfs 和 socket - ---- - -### Window 模块 - -**职责**:窗口创建和后端管理 - -**后端支持**: -- SDL2:跨平台 -- GLFW:跨平台 -- Switch:原生 - -**配置**: -```cpp -WindowModuleConfig config; -config.windowConfig.title = "My App"; -config.windowConfig.width = 1280; -config.windowConfig.height = 720; -config.windowConfig.vsync = true; -``` - ---- - -### Render 模块 - -**职责**:渲染器初始化和管理 - -**配置**: -```cpp -RenderModuleConfig config; -config.backend = BackendType::OpenGL; -config.vsync = true; -config.targetFPS = 60; -config.multisamples = 4; -``` - ---- - -## 配置文件格式 - -模块配置使用 JSON 格式: - -```json -{ - "logger": { - "logLevel": 2, - "consoleOutput": true, - "fileOutput": false - }, - "window": { - "title": "My Application", - "width": 1280, - "height": 720, - "vsync": true - }, - "render": { - "targetFPS": 60, - "multisamples": 4 - } -} +if (caps.supportsWindowed) { /* 支持窗口模式 */ } +if (caps.supportsGamepad) { /* 支持手柄 */ } +if (caps.supportsTouch) { /* 支持触摸 */ } ``` --- ## 最佳实践 -### 1. 模块用于平台初始化,服务用于运行时功能 +### 1. 模块配置独立化 ```cpp -// 模块:平台级初始化 -class WindowModule : public IModuleInitializer { - bool initialize(const IModuleConfig* config) override { - // 创建窗口、初始化 OpenGL 上下文 - } +// 好的做法:模块管理自己的配置 +class WindowModuleConfig : public IModuleConfig { + WindowConfigData windowConfig; // 模块内部配置 }; -// 服务:运行时功能 -class SceneService : public ISceneService { - void update(float deltaTime) override { - // 每帧更新场景 - } +// 不好的做法:所有配置放在 AppConfig +struct AppConfig { + WindowConfigData window; // 耦合度高 + RenderConfigData render; + // ... 新增模块需要修改 AppConfig }; ``` -### 2. 通过服务定位器解耦依赖 +### 2. 使用平台约束 ```cpp -// 好的做法:通过服务定位器获取依赖 -void MyNode::onUpdate(float dt) { - auto camera = ServiceLocator::instance().getService(); - auto pos = camera->screenToWorld(mousePos); -} - -// 不好的做法:直接依赖 Application -void MyNode::onUpdate(float dt) { - auto& camera = Application::get().camera(); // 耦合度高 +void WindowModuleConfig::applyPlatformConstraints(PlatformType platform) { +#ifdef __SWITCH__ + windowConfig.mode = WindowMode::Fullscreen; + windowConfig.resizable = false; +#else + (void)platform; +#endif } ``` -### 3. 使用接口便于测试 +### 3. 模块自动注册 ```cpp -// 定义接口 -class IAudioService : public IService { ... }; - -// 生产环境使用真实实现 -auto audio = makeShared(); - -// 测试环境使用 Mock -auto audio = makeShared(); -``` - -### 4. 安全关闭 - -```cpp -void shutdown() override { - if (!initialized_) return; // 避免重复关闭 - - // 清理资源 - resource_.reset(); - - setState(ServiceState::Stopped); - initialized_ = false; +namespace { + struct MyModuleAutoRegister { + MyModuleAutoRegister() { register_my_module(); } + }; + static MyModuleAutoRegister s_autoRegister; // 程序启动时自动注册 } ``` @@ -683,7 +765,6 @@ for (const auto& service : services) { 完整示例请参考: - [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/platform/input_module.cpp](../../Extra2D/src/platform/input_module.cpp) - Input 模块实现 - [Extra2D/src/graphics/render_module.cpp](../../Extra2D/src/graphics/render_module.cpp) - Render 模块实现 diff --git a/examples/basic/main.cpp b/examples/basic/main.cpp index 379ccc0..04b7240 100644 --- a/examples/basic/main.cpp +++ b/examples/basic/main.cpp @@ -1,30 +1,112 @@ /** * @file main.cpp - * @brief Extra2D 基础示例程序 + * @brief Extra2D 场景图测试示例 * - * 演示如何使用 Extra2D 引擎创建一个简单的窗口应用程序。 + * 演示场景图功能: + * - 节点层级关系 + * - 变换(位置、旋转、缩放) + * - 形状节点渲染 + * - 输入事件处理 */ #include -#include -#include #include using namespace extra2d; +/** + * @brief 创建场景图测试 + */ +void createSceneGraph(Scene *scene) { + float width = scene->getWidth(); + float height = scene->getHeight(); + + auto root = makeShared(); + root->setName("Root"); + root->setPos(width / 2, height / 2); + scene->addChild(root); + + auto parent1 = makeShared(); + parent1->setName("Parent1"); + parent1->setPos(-200, 0); + root->addChild(parent1); + + auto rect1 = ShapeNode::createFilledRect(Rect(-50, -50, 100, 100), + Color(1.0f, 0.4f, 0.4f, 1.0f)); + rect1->setName("RedRect"); + parent1->addChild(rect1); + + auto child1 = makeShared(); + child1->setName("Child1"); + child1->setPos(80, 0); + child1->setRotation(45); + child1->setScale(0.5f); + parent1->addChild(child1); + + auto smallRect = ShapeNode::createFilledRect(Rect(-30, -30, 60, 60), + Color(1.0f, 0.8f, 0.4f, 1.0f)); + smallRect->setName("OrangeRect"); + child1->addChild(smallRect); + + auto parent2 = makeShared(); + parent2->setName("Parent2"); + parent2->setPos(200, 0); + root->addChild(parent2); + + auto circle1 = ShapeNode::createFilledCircle(Vec2(0, 0), 60, + Color(0.4f, 0.4f, 1.0f, 1.0f)); + circle1->setName("BlueCircle"); + parent2->addChild(circle1); + + auto child2 = makeShared(); + child2->setName("Child2"); + child2->setPos(0, 100); + parent2->addChild(child2); + + auto triangle = ShapeNode::createFilledTriangle( + Vec2(0, -40), Vec2(-35, 30), Vec2(35, 30), Color(0.4f, 1.0f, 0.4f, 1.0f)); + triangle->setName("GreenTriangle"); + child2->addChild(triangle); + + auto line = ShapeNode::createLine(Vec2(-300, -200), Vec2(300, -200), + Color(1.0f, 1.0f, 1.0f, 1.0f), 2.0f); + line->setName("BottomLine"); + root->addChild(line); + + auto polygon = ShapeNode::createFilledPolygon( + {Vec2(0, -50), Vec2(50, 0), Vec2(30, 50), Vec2(-30, 50), Vec2(-50, 0)}, + Color(1.0f, 0.4f, 1.0f, 1.0f)); + polygon->setName("PurplePolygon"); + polygon->setPos(0, -150); + root->addChild(polygon); + + std::cout << "\n=== Scene Graph Structure ===" << std::endl; + std::cout << "Scene (root)" << std::endl; + std::cout << " └── Root (center)" << std::endl; + std::cout << " ├── Parent1 (left)" << std::endl; + std::cout << " │ ├── RedRect (100x100)" << std::endl; + std::cout << " │ └── Child1 (rotated 45°, scaled 0.5)" << std::endl; + std::cout << " │ └── OrangeRect (60x60)" << std::endl; + std::cout << " ├── Parent2 (right)" << std::endl; + std::cout << " │ ├── BlueCircle (radius 60)" << std::endl; + std::cout << " │ └── Child2 (below)" << std::endl; + std::cout << " │ └── GreenTriangle" << std::endl; + std::cout << " ├── BottomLine" << std::endl; + std::cout << " └── PurplePolygon (pentagon)" << std::endl; + std::cout << "=============================\n" << std::endl; +} + /** * @brief 主函数 - * - * 初始化应用程序,创建场景,运行主循环。 */ int main(int argc, char *argv[]) { (void)argc; (void)argv; - std::cout << "Extra2D Demo - Starting..." << std::endl; + std::cout << "Extra2D Scene Graph Demo - Starting..." << std::endl; AppConfig config = AppConfig::createDefault(); - config.appName = "Extra2D Demo"; + config.appName = "Extra2D Scene Graph Demo"; config.appVersion = "1.0.0"; Application &app = Application::get(); @@ -37,15 +119,40 @@ int main(int argc, char *argv[]) { 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 eventService = app.events(); + if (eventService) { + eventService->addListener(EventType::KeyPressed, [](Event &e) { + auto &keyEvent = std::get(e.data); + + if (keyEvent.keyCode == static_cast(Key::Escape)) { + e.handled = true; + Application::get().quit(); + } + }); + + eventService->addListener(EventType::MouseButtonPressed, [](Event &e) { + auto &mouseEvent = std::get(e.data); + std::cout << "[Click] Button " << mouseEvent.button << " at (" + << mouseEvent.position.x << ", " << mouseEvent.position.y << ")" + << std::endl; + }); + } auto scene = Scene::create(); - scene->setBackgroundColor(Colors::SkyBlue); + scene->setBackgroundColor(Color(0.12f, 0.12f, 0.16f, 1.0f)); scene->setViewportSize(static_cast(app.window().width()), static_cast(app.window().height())); + + createSceneGraph(scene.get()); + app.enterScene(scene); + std::cout << "\nControls:" << std::endl; + std::cout << " ESC - Exit" << std::endl; + std::cout << " Mouse Click - Print position" << std::endl; + std::cout << "\nRunning main loop...\n" << std::endl; + app.run(); std::cout << "Shutting down..." << std::endl;