Extra2D/docs/module_system.md

16 KiB
Raw Blame History

Extra2D 模块系统

概述

Extra2D 采用模块化架构设计(参考 Kiwano所有核心功能通过模块系统和服务系统管理。系统提供

  • 自动发现注册:模块定义即注册,无需手动调用
  • 统一的生命周期管理:初始化、更新、渲染、关闭
  • 优先级排序:确保模块按正确顺序初始化
  • Context 模式:使用上下文对象遍历模块链
  • 依赖注入:通过服务定位器解耦模块间依赖

架构图

graph TB
    Application["Application<br/>(协调模块和服务)"]
    
    Application --> ModuleRegistry
    Application --> ServiceLocator
    
    ModuleRegistry["ModuleRegistry<br/>(模块注册器)"]
    ServiceLocator["ServiceLocator<br/>(服务定位器)"]
    
    ModuleRegistry --> ConfigModule["ConfigModule"]
    ModuleRegistry --> WindowModule["WindowModule"]
    
    ServiceLocator --> SceneService["SceneService"]
    ServiceLocator --> TimerService["TimerService"]

模块自动注册

E2D_MODULE 宏

模块通过 E2D_MODULE 宏自动注册,无需手动调用任何注册函数:

// config_module.cpp
#include "config_module.h"

namespace extra2d {
// 模块实现...
}

// 在文件末尾namespace 外部
E2D_MODULE(ConfigModule, 0)  // 名称, 优先级

带依赖的模块

// window_module.cpp
E2D_MODULE(WindowModule, 20, "ConfigModule")  // 依赖 ConfigModule

// render_module.cpp
E2D_MODULE(RenderModule, 40, "WindowModule", "ConfigModule")  // 多个依赖

带属性的模块

// 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 配置

-- 引擎库(静态库)
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")

自定义模块配置

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")

模块定义

// hello_module.cpp
#include "hello_module.h"

namespace extra2d {
// 模块实现...
}

// 在文件末尾namespace 外部
E2D_MODULE(HelloModule, 1000)

main.cpp

#include "hello_module.h"
#include <extra2d/extra2d.h>

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 配置

-- 引擎库(动态库)
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")

模块定义

// hello_module.cpp
#include "hello_module.h"

namespace extra2d {
// 模块实现...
}

// 使用 E2D_MODULE_EXPORT自动生成导出函数
E2D_MODULE_EXPORT(HelloModule, 1000)

main.cpp

#include "hello_module.h"
#include <extra2d/extra2d.h>

// 声明外部模块的导出函数
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

add_ldflags("-Wl,--whole-archive", {force = true})
add_deps("extra2d")
add_ldflags("-Wl,--no-whole-archive", {force = true})

macOS

macOS 使用 -force_load 代替 --whole-archive

if is_plat("macosx") then
    add_ldflags("-force_load", {force = true})
end
add_deps("extra2d")

Windows (MSVC)

MSVC 使用 /WHOLEARCHIVE

if is_plat("windows") and is_toolchain("msvc") then
    add_ldflags("/WHOLEARCHIVE:extra2d", {force = true})
end
add_deps("extra2d")

Windows (MinGW)

add_ldflags("-Wl,--whole-archive", {force = true})
add_deps("extra2d")
add_ldflags("-Wl,--no-whole-archive", {force = true})

模块基类

Module

所有模块只需继承 Module 基类,实现需要的生命周期方法:

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; }
};

模块上下文

用于遍历模块链,支持链式调用:

/**
 * @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:

#pragma once
#include <extra2d/core/module.h>
#include <string>

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:

#include "hello_module.h"
#include <extra2d/utils/logger.h>

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:

#include "hello_module.h"
#include <extra2d/extra2d.h>

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:

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

extra2d::AppConfig config;
config.appName = "My Application";
config.appVersion = "1.0.0";

Window 模块

职责:窗口创建和管理

优先级20

后端:统一使用 SDL2


Input 模块

职责:输入设备管理(键盘、鼠标、手柄)

优先级30


Render 模块

职责:渲染器初始化和管理

优先级40


服务系统

IService

服务接口基类:

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

使用服务

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<extra2d::KeyEvent>(e.data);
    E2D_LOG_INFO("Key pressed: {}", keyEvent.keyCode);
});

输入事件系统

事件类型

enum class EventType {
    // 键盘
    KeyPress,
    KeyRelease,
    KeyRepeat,
    
    // 鼠标
    MousePress,
    MouseRelease,
    MouseMove,
    MouseScroll,
    
    // 手柄
    GamepadConnect,
    GamepadDisconnect,
    GamepadPress,
    GamepadRelease,
    
    // 触摸
    TouchBegin,
    TouchMove,
    TouchEnd,
    
    // 窗口
    WindowResize,
    WindowClose,
};

事件监听

auto eventService = app.events();

// 监听键盘事件
eventService->addListener(EventType::KeyPress, [](Event& e) {
    auto& key = std::get<KeyEvent>(e.data);
    if (key.scancode == static_cast<int>(Key::Escape)) {
        Application::get().quit();
    }
});

// 监听鼠标事件
eventService->addListener(EventType::MousePress, [](Event& e) {
    auto& mouse = std::get<MouseEvent>(e.data);
    E2D_LOG_INFO("Click at ({}, {})", mouse.position.x, mouse.position.y);
});

场景图系统

Node 基类

class Node : public std::enable_shared_from_this<Node> {
public:
    // 层级管理
    void addChild(Ptr<Node> child);
    void removeChild(Ptr<Node> child);
    Ptr<Node> 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 形状节点

// 创建形状节点
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)

示例

完整示例请参考:


最佳实践

1. 模块优先级

// 核心模块使用低优先级
class LoggerModule : public Module {
    int getPriority() const override { return -1; }
};

// 用户模块使用高优先级
class MyModule : public Module {
    int getPriority() const override { return 1000; }
};

2. 链式调用

void onUpdate(UpdateContext& ctx) override {
    // 执行更新逻辑
    doSomething();
    
    // 继续下一个模块
    ctx.next();
}

3. 检查初始化状态

void onUpdate(UpdateContext& ctx) override {
    if (!isInitialized()) {
        ctx.next();
        return;
    }
    
    // 执行更新逻辑
    ctx.next();
}

4. 静态链接优先

静态链接更简单,无需额外配置,推荐用于大多数场景。