# Extra2D 模块系统 ## 概述 Extra2D 采用模块化架构设计(参考 Kiwano),所有核心功能通过模块系统和服务系统管理。系统提供: - **自动发现注册**:模块定义即注册,无需手动调用 - **统一的生命周期管理**:初始化、更新、渲染、关闭 - **优先级排序**:确保模块按正确顺序初始化 - **Context 模式**:使用上下文对象遍历模块链 - **依赖注入**:通过服务定位器解耦模块间依赖 ## 架构图 ```mermaid graph TB Application["Application
(协调模块和服务)"] Application --> ModuleRegistry Application --> ServiceLocator ModuleRegistry["ModuleRegistry
(模块注册器)"] ServiceLocator["ServiceLocator
(服务定位器)"] ModuleRegistry --> ConfigModule["ConfigModule"] ModuleRegistry --> WindowModule["WindowModule"] ServiceLocator --> SceneService["SceneService"] ServiceLocator --> TimerService["TimerService"] ``` --- ## 模块自动注册 ### E2D_MODULE 宏 模块通过 `E2D_MODULE` 宏自动注册,无需手动调用任何注册函数: ```cpp // config_module.cpp #include "config_module.h" namespace extra2d { // 模块实现... } // 在文件末尾,namespace 外部 E2D_MODULE(ConfigModule, 0) // 名称, 优先级 ``` ### 带依赖的模块 ```cpp // window_module.cpp E2D_MODULE(WindowModule, 20, "ConfigModule") // 依赖 ConfigModule // render_module.cpp E2D_MODULE(RenderModule, 40, "WindowModule", "ConfigModule") // 多个依赖 ``` ### 带属性的模块 ```cpp // render_module.cpp E2D_MODULE_BEGIN(RenderModule) E2D_PRIORITY(40) E2D_DEPENDENCIES("WindowModule") E2D_PROPERTY(vsync, bool) E2D_PROPERTY(targetFPS, int) E2D_MODULE_END() ``` --- ## 链接方式详解 ### 静态链接 vs 动态链接 | 特性 | 静态链接 | 动态链接 | |------|---------|---------| | **引擎库** | `.a` / `.lib` | `.dll` / `.so` | | **模块注册** | 需要 `--whole-archive` 或直接编译 | 自动注册 | | **自定义模块** | 直接编译到可执行文件 | 编译为独立 DLL | | **额外代码** | 无 | 需要 `E2D_FORCE_LINK` | | **分发** | 单一可执行文件 | 需要 DLL 文件 | | **热更新** | 不支持 | 支持(重新加载 DLL) | ### 静态链接方案(推荐) #### 原理 静态链接时,链接器会优化掉未引用的代码。解决方案: 1. **引擎库**:使用 `--whole-archive` 强制链接所有符号 2. **自定义模块**:直接编译到可执行文件 #### xmake 配置 ```lua -- 引擎库(静态库) target("extra2d") set_kind("static") -- ... 其他配置 -- 可执行文件 target("demo_basic") set_kind("binary") -- 强制链接引擎静态库 add_ldflags("-Wl,--whole-archive", {force = true}) add_deps("extra2d") add_ldflags("-Wl,--no-whole-archive", {force = true}) add_files("main.cpp") ``` #### 自定义模块配置 ```lua target("demo_hello_module") set_kind("binary") -- 强制链接引擎静态库 add_ldflags("-Wl,--whole-archive", {force = true}) add_deps("extra2d") add_ldflags("-Wl,--no-whole-archive", {force = true}) -- 直接编译模块源文件到可执行文件 add_files("main.cpp", "hello_module.cpp") add_includedirs("hello_module") ``` #### 模块定义 ```cpp // hello_module.cpp #include "hello_module.h" namespace extra2d { // 模块实现... } // 在文件末尾,namespace 外部 E2D_MODULE(HelloModule, 1000) ``` #### main.cpp ```cpp #include "hello_module.h" #include int main() { extra2d::Application& app = extra2d::Application::get(); // 无需任何注册代码! // 模块在静态初始化时自动注册 extra2d::AppConfig config; config.appName = "My App"; if (!app.init(config)) return 1; app.run(); return 0; } ``` ### 动态链接方案 #### xmake 配置 ```lua -- 引擎库(动态库) target("extra2d") set_kind("shared") add_defines("E2D_BUILDING_DLL", {public = false}) -- ... 其他配置 -- 自定义模块 DLL target("hello_module_lib") set_kind("shared") add_defines("E2D_BUILDING_DLL", {public = false}) add_deps("extra2d") add_files("hello_module.cpp") add_includedirs("hello_module", {public = true}) -- 可执行文件 target("demo_hello_module") set_kind("binary") add_deps("hello_module_lib") add_files("main.cpp") ``` #### 模块定义 ```cpp // hello_module.cpp #include "hello_module.h" namespace extra2d { // 模块实现... } // 使用 E2D_MODULE_EXPORT(自动生成导出函数) E2D_MODULE_EXPORT(HelloModule, 1000) ``` #### main.cpp ```cpp #include "hello_module.h" #include // 声明外部模块的导出函数 E2D_DECLARE_FORCE_LINK(HelloModule); int main() { // 触发 DLL 加载 E2D_CALL_FORCE_LINK(HelloModule); extra2d::Application& app = extra2d::Application::get(); if (!app.init(config)) return 1; app.run(); return 0; } ``` --- ## 平台兼容性 ### Linux ```lua add_ldflags("-Wl,--whole-archive", {force = true}) add_deps("extra2d") add_ldflags("-Wl,--no-whole-archive", {force = true}) ``` ### macOS macOS 使用 `-force_load` 代替 `--whole-archive`: ```lua if is_plat("macosx") then add_ldflags("-force_load", {force = true}) end add_deps("extra2d") ``` ### Windows (MSVC) MSVC 使用 `/WHOLEARCHIVE`: ```lua if is_plat("windows") and is_toolchain("msvc") then add_ldflags("/WHOLEARCHIVE:extra2d", {force = true}) end add_deps("extra2d") ``` ### Windows (MinGW) ```lua add_ldflags("-Wl,--whole-archive", {force = true}) add_deps("extra2d") add_ldflags("-Wl,--no-whole-archive", {force = true}) ``` --- ## 模块基类 ### Module 所有模块只需继承 `Module` 基类,实现需要的生命周期方法: ```cpp class Module { public: virtual ~Module() = default; /** * @brief 设置模块(初始化) */ virtual void setupModule() {} /** * @brief 销毁模块 */ virtual void destroyModule() {} /** * @brief 更新时 */ virtual void onUpdate(UpdateContext& ctx) { ctx.next(); } /** * @brief 渲染前 */ virtual void beforeRender(RenderContext& ctx) { ctx.next(); } /** * @brief 渲染时 */ virtual void onRender(RenderContext& ctx) { ctx.next(); } /** * @brief 渲染后 */ virtual void afterRender(RenderContext& ctx) { ctx.next(); } /** * @brief 事件处理 */ virtual void handleEvent(EventContext& ctx) { ctx.next(); } /** * @brief 获取模块名称 */ virtual const char* getName() const = 0; /** * @brief 获取模块优先级 */ virtual int getPriority() const { return 0; } }; ``` ### 模块上下文 用于遍历模块链,支持链式调用: ```cpp /** * @brief 模块上下文基类 */ class ModuleContext { public: void next(); // 遍历下一个模块 bool isDone() const; // 检查是否完成 }; /** * @brief 更新上下文 */ class UpdateContext : public ModuleContext { public: float getDeltaTime() const; // 获取帧间隔时间 }; /** * @brief 渲染上下文 */ class RenderContext : public ModuleContext { public: enum class Phase { Before, On, After }; Phase getPhase() const; }; /** * @brief 事件上下文 */ class EventContext : public ModuleContext { }; ``` --- ## 模块 vs 服务 | 特性 | 模块 (Module) | 服务 (Service) | |-----|--------------|---------------| | 用途 | 平台级初始化 | 运行时功能 | | 生命周期 | Application 管理 | ServiceLocator 管理 | | 注册方式 | 自动发现 | `locator.registerService()` | | 可替换性 | 编译时确定 | 运行时可替换 | | 示例 | Window, Render, Input | Scene, Timer, Event, Camera | --- ## 模块优先级 模块按优先级从小到大初始化,关闭时逆序执行: | 模块 | 优先级 | 说明 | |------|--------|------| | LoggerModule | -1 | 最先初始化 | | ConfigModule | 0 | 配置加载 | | PlatformModule | 10 | 平台初始化 | | WindowModule | 20 | 窗口创建 | | InputModule | 30 | 输入系统 | | RenderModule | 40 | 渲染系统 | | ScriptModule | 500 | 脚本系统 | | 用户模块 | 1000+ | 用户自定义 | --- ## 创建新模块 ### 完整示例(静态链接) **hello_module.h:** ```cpp #pragma once #include #include namespace extra2d { struct HelloModuleConfig { std::string greeting = "Hello!"; int repeatCount = 1; }; class HelloModule : public Module { public: HelloModule(); ~HelloModule() override; const char* getName() const override { return "HelloModule"; } int getPriority() const override { return 1000; } void setupModule() override; void destroyModule() override; void onUpdate(UpdateContext& ctx) override; void setConfig(const HelloModuleConfig& config) { config_ = config; } void sayHello() const; private: HelloModuleConfig config_; float time_ = 0.0f; }; } // namespace extra2d ``` **hello_module.cpp:** ```cpp #include "hello_module.h" #include namespace extra2d { HelloModule::HelloModule() = default; HelloModule::~HelloModule() { if (isInitialized()) { destroyModule(); } } void HelloModule::setupModule() { if (isInitialized()) return; setInitialized(true); E2D_LOG_INFO("HelloModule initialized"); E2D_LOG_INFO(" Greeting: {}", config_.greeting); E2D_LOG_INFO(" Repeat Count: {}", config_.repeatCount); sayHello(); } void HelloModule::destroyModule() { if (!isInitialized()) return; E2D_LOG_INFO("HelloModule shutdown"); setInitialized(false); } void HelloModule::onUpdate(UpdateContext& ctx) { if (!isInitialized()) { ctx.next(); return; } time_ += ctx.getDeltaTime(); if (time_ >= 5.0f) { sayHello(); time_ = 0.0f; } ctx.next(); } void HelloModule::sayHello() const { for (int i = 0; i < config_.repeatCount; ++i) { E2D_LOG_INFO("[HelloModule] {}", config_.greeting); } } } // namespace extra2d // 自动注册(在 namespace 外部) E2D_MODULE(HelloModule, 1000) ``` **main.cpp:** ```cpp #include "hello_module.h" #include int main() { extra2d::Application& app = extra2d::Application::get(); extra2d::AppConfig config; config.appName = "Hello Module Demo"; // 无需手动注册!模块已自动注册 if (!app.init(config)) return 1; app.run(); return 0; } ``` **xmake.lua:** ```lua target("demo_hello_module") set_kind("binary") -- 强制链接引擎静态库 add_ldflags("-Wl,--whole-archive", {force = true}) add_deps("extra2d") add_ldflags("-Wl,--no-whole-archive", {force = true}) -- 直接编译模块源文件 add_files("main.cpp", "hello_module.cpp") ``` --- ## 内置模块 ### Config 模块 **职责**:管理 ConfigManager 和应用配置 **优先级**:0 ```cpp extra2d::AppConfig config; config.appName = "My Application"; config.appVersion = "1.0.0"; ``` --- ### Window 模块 **职责**:窗口创建和管理 **优先级**:20 **后端**:统一使用 SDL2 --- ### Input 模块 **职责**:输入设备管理(键盘、鼠标、手柄) **优先级**:30 --- ### Render 模块 **职责**:渲染器初始化和管理 **优先级**:40 --- ## 服务系统 ### IService 服务接口基类: ```cpp class IService { public: virtual ~IService() = default; virtual ServiceInfo getServiceInfo() const = 0; virtual bool initialize() = 0; virtual void shutdown() = 0; virtual void update(float deltaTime); }; ``` ### 内置服务 | 服务 | 用途 | 优先级 | |-----|------|-------| | EventService | 事件分发 | 100 | | TimerService | 计时器 | 200 | | SceneService | 场景管理 | 300 | | CameraService | 相机系统 | 400 | ### 使用服务 ```cpp auto& app = extra2d::Application::get(); // 获取服务 auto sceneService = app.scenes(); auto timerService = app.timers(); auto eventService = app.events(); auto cameraService = app.camera(); // 使用场景服务 sceneService->pushScene(myScene); // 使用计时器服务 timerService->addTimer(1.0f, []() { E2D_LOG_INFO("Timer fired!"); }); // 使用事件服务 eventService->addListener(extra2d::EventType::KeyPress, [](extra2d::Event& e) { auto& keyEvent = std::get(e.data); E2D_LOG_INFO("Key pressed: {}", keyEvent.keyCode); }); ``` --- ## 输入事件系统 ### 事件类型 ```cpp enum class EventType { // 键盘 KeyPress, KeyRelease, KeyRepeat, // 鼠标 MousePress, MouseRelease, MouseMove, MouseScroll, // 手柄 GamepadConnect, GamepadDisconnect, GamepadPress, GamepadRelease, // 触摸 TouchBegin, TouchMove, TouchEnd, // 窗口 WindowResize, WindowClose, }; ``` ### 事件监听 ```cpp auto eventService = app.events(); // 监听键盘事件 eventService->addListener(EventType::KeyPress, [](Event& e) { auto& key = std::get(e.data); if (key.scancode == static_cast(Key::Escape)) { Application::get().quit(); } }); // 监听鼠标事件 eventService->addListener(EventType::MousePress, [](Event& e) { auto& mouse = std::get(e.data); E2D_LOG_INFO("Click at ({}, {})", mouse.position.x, mouse.position.y); }); ``` --- ## 场景图系统 ### Node 基类 ```cpp class Node : public std::enable_shared_from_this { public: // 层级管理 void addChild(Ptr child); void removeChild(Ptr child); Ptr getParent() const; // 变换属性 void setPos(const Vec2& pos); void setRotation(float degrees); void setScale(const Vec2& scale); // 世界变换 Vec2 toWorld(const Vec2& localPos) const; glm::mat4 getWorldTransform() const; // 生命周期回调 virtual void onEnter(); virtual void onExit(); virtual void onUpdate(float dt); virtual void onRender(RenderBackend& renderer); }; ``` ### ShapeNode 形状节点 ```cpp // 创建形状节点 auto rect = ShapeNode::createFilledRect( Rect(0, 0, 100, 100), Color(1.0f, 0.4f, 0.4f, 1.0f) ); auto circle = ShapeNode::createFilledCircle( Vec2(0, 0), 50, Color(0.4f, 0.4f, 1.0f, 1.0f) ); auto triangle = ShapeNode::createFilledTriangle( Vec2(0, -40), Vec2(-35, 30), Vec2(35, 30), Color(0.4f, 1.0f, 0.4f, 1.0f) ); ``` --- ## 常见问题 ### Q: 模块没有被注册? **静态链接:** - 确保使用了 `--whole-archive` - 自定义模块要直接编译到可执行文件 **动态链接:** - 确保使用了 `E2D_MODULE_EXPORT` - 确保调用了 `E2D_CALL_FORCE_LINK` ### Q: 链接错误 "undefined reference"? 检查链接顺序,`--whole-archive` 要在 `add_deps` 之前。 ### Q: 模块初始化顺序错误? 使用 `getPriority()` 控制顺序,数字小的先初始化。 ### Q: 如何调试模块注册? 查看日志输出: ``` [INFO ] ModuleRegistry: 4 modules registered [INFO ] - ConfigModule (priority: 0) [INFO ] - WindowModule (priority: 20) [INFO ] - InputModule (priority: 30) [INFO ] - RenderModule (priority: 40) ``` --- ## 示例 完整示例请参考: - [examples/hello_module/](../../examples/hello_module/) - 自定义模块示例 - [examples/basic/main.cpp](../../examples/basic/main.cpp) - 基础示例 --- ## 最佳实践 ### 1. 模块优先级 ```cpp // 核心模块使用低优先级 class LoggerModule : public Module { int getPriority() const override { return -1; } }; // 用户模块使用高优先级 class MyModule : public Module { int getPriority() const override { return 1000; } }; ``` ### 2. 链式调用 ```cpp void onUpdate(UpdateContext& ctx) override { // 执行更新逻辑 doSomething(); // 继续下一个模块 ctx.next(); } ``` ### 3. 检查初始化状态 ```cpp void onUpdate(UpdateContext& ctx) override { if (!isInitialized()) { ctx.next(); return; } // 执行更新逻辑 ctx.next(); } ``` ### 4. 静态链接优先 静态链接更简单,无需额外配置,推荐用于大多数场景。