# 应用层与图形库分层设计 本项目后续包含三个应用层目标:两个游戏和一个启动器;同时还需要沉淀一套可复用的 IMX6U 轻量图形库。为了避免后期耦合和性能返工,必须明确区分“应用层”和“底层库”。 ## 1. 推荐总体结构 推荐拆成四个逻辑层: ```text IMX6U-Game/ ├─ src/ │ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则 │ │ ├─ Draw2D/ # DrawContext 统一绘制入口(✅ 已实现) │ │ ├─ Core/ # FrameBuffer、DepthBuffer(✅ 已实现) │ │ ├─ Math/ # 向量、矩阵、数学工具(✅ 已实现) │ │ ├─ Rasterizer/ # 线段、三角形光栅化(✅ 已实现) │ │ ├─ RenderData/ # Color、Triangle 等数据结构(✅ 已实现) │ │ ├─ Scene/ # Camera、Transform(✅ 已实现) │ │ ├─ Shading/ # 着色器(预留) │ │ ├─ Platform/ # SDL2 / fb0 平台适配(✅ 已实现) │ │ └─ Asset/ # 资源加载(✅ 已实现) │ ├─ Apps/ │ │ ├─ Demo/ # 当前 3D 立方体 demo(✅ 已实现) │ │ ├─ Launcher/ # 启动器应用(待实现) │ │ ├─ GameA/ # 第一个游戏(待实现) │ │ └─ GameB/ # 第二个游戏(待实现) │ └─ Shared/ # 可选:应用层共享但不属于 Gfx 的东西 │ ├─ Save/ # 存档格式、配置读写 │ ├─ UI/ # 启动器和游戏共用 UI 组件 │ └─ Assets/ # 应用层资源索引、资源命名约定 ├─ assets/ │ ├─ launcher/ │ ├─ game_a/ │ ├─ game_b/ │ └─ shared/ └─ docs/ ``` ~~当前项目已有 `Core/Math/Platform/Rasterizer/RenderData/Scene` 等目录,短期不必一次性搬迁;但新增代码应按上面的边界收敛。等功能稳定后,再把现有底层代码整体移动到 `src/Gfx/`。~~ **已完成**:底层代码已整体迁移到 `src/Gfx/`,包括 Core、Math、Rasterizer、RenderData、Scene、Shading、Platform、Asset 和新增的 Draw2D。Demo 入口位于 `src/Apps/Demo/`。 ## 2. 四个层级的职责 ### 2.1 Gfx:底层图形库 职责: - 管理 framebuffer、depthbuffer、渲染上下文。 - 提供基础绘制接口:点、线、矩形、四边形、三角形、sprite、tile、简单文本等。 - 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。 - 封装 SDL2 / framebuffer 显示提交、输入轮询、时间源。 - 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。 禁止: - 不依赖 `GameA`、`GameB`、`Launcher`。 - 不包含具体关卡、角色、菜单流程、游戏状态机。 - 不直接读取某个游戏专属资源路径。 - 不在核心绘制接口中暴露 SDL2 类型。 ### 2.2 Apps/GameA 与 Apps/GameB:两个游戏 职责: - 各自维护自己的游戏规则、状态机、关卡、实体、碰撞、计分、胜负逻辑。 - 通过 Gfx 提供的接口绘制画面。 - 通过平台输入快照读取按键/触摸状态,而不是直接调用 SDL。 - 管理自己的资源索引和场景切换。 禁止: - 游戏之间互相 include 对方代码。 - 游戏直接操作 SDL renderer / texture / window。 - 游戏直接依赖 framebuffer 内存布局,除非是明确标注的性能特例。 ### 2.3 Apps/Launcher:启动器 职责: - 展示游戏列表、图标、说明、版本信息。 - 选择并启动 GameA 或 GameB。 - 管理全局设置,例如音量、亮度、输入校准、语言等。 - 可以复用 Shared/UI,但不承载具体游戏逻辑。 启动方式有两种可选方案: 1. **单进程多应用模式**:Launcher、GameA、GameB 编译成一个可执行文件,内部切换当前 App。 2. **多进程模式**:Launcher 是独立程序,选择后启动另一个游戏可执行文件。 对 IMX6U 初期开发,推荐先用 **单进程多应用模式**,原因: - 构建、部署、调试更简单。 - SDL2 初始化、资源缓存、输入状态可以共用。 - 避免频繁退出/启动进程带来的黑屏、资源重载和状态恢复问题。 等项目成熟后,如果每个游戏体积较大,或者需要独立更新,再考虑多进程拆分。 ### 2.4 Shared:应用层共享模块 Shared 只放“应用层共享,但不属于底层图形库”的内容,例如: - UI 控件:按钮、列表、九宫格面板、菜单焦点管理。 - 存档/设置:配置文件、最高分、解锁状态。 - 资源清单:多个应用共用的字体、图标、音效索引。 Shared 可以依赖 Gfx;Gfx 不能依赖 Shared。 ## 3. 依赖方向 必须保持单向依赖: ```text Apps/GameA ─┐ Apps/GameB ─┼─> Shared ─> Gfx ─> Platform(SDL2/fb0) Launcher ─┘ Apps/GameA ─┐ Apps/GameB ─┼──────────> Gfx Launcher ─┘ ``` 禁止反向依赖: ```text Gfx -> Apps 禁止 Gfx -> Shared 禁止 GameA -> GameB 禁止 GameB -> GameA 禁止 Platform -> Game 禁止 ``` ## 4. 应用统一接口 推荐为 Launcher、GameA、GameB 提供统一应用接口,例如: ```cpp class IApp { public: virtual ~IApp() {} virtual void on_enter() = 0; virtual void on_exit() = 0; virtual void update(uint32_t fixed_delta_ms) = 0; virtual void render(Gfx::DrawContext& ctx) = 0; virtual AppId next_app() const = 0; }; ``` 主循环只认识 `IApp`: ```text poll input -> update current app -> render current app -> present framebuffer ``` 这样三个应用层共用同一个主循环、同一套 SDL2 初始化和 framebuffer 提交流程。 注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 `draw_rect`、`draw_quad`、`set_pixel_fast` 这类热路径函数不要虚化。 ## 5. 底层图形库优先提供的 API Gfx 初期建议先做 2D 基础能力,不要一开始就做复杂引擎: ```cpp namespace Gfx { struct RectI { int32_t x, y, w, h; }; struct PointI { int32_t x, y; }; struct Color32 { uint32_t rgba; }; class DrawContext { public: void clear(Color32 color); void draw_pixel(int32_t x, int32_t y, Color32 color); void draw_line(PointI a, PointI b, Color32 color); void draw_rect(RectI rect, Color32 color); void fill_rect(RectI rect, Color32 color); void draw_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color); void fill_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color); }; } ``` 后续再扩展: - `draw_sprite` - `draw_sprite_region` - `draw_tilemap` - `draw_text_bitmap_font` - `set_clip_rect` - `set_camera_2d` - `batch sprite/tile` ## 6. 命名建议 为了避免“项目名、库名、游戏名”混乱,建议: - 项目仓库仍叫 `IMX6U-Game`。 - 底层图形库命名为 `Gfx` 或 `MiniGfx`。 - 三个应用用明确名字:`Launcher`、`GameA`、`GameB`,后续再替换成真实游戏名。 - CMake target 可以是: - `imx6u_gfx` 静态库 - `imx6u_launcher` - `imx6u_game_a` - `imx6u_game_b` - 或初期单可执行文件 `imx6u_suite` ## 7. 推荐演进顺序 1. ~~先抽出统一 `IApp` 和 `AppManager`,让当前 demo 成为一个 app。~~ 2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~ 3. ~~建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**(`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、present) 4. ~~底层代码迁移到 `src/Gfx/`,Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成** 5. 新增 Launcher app,只做最小菜单和应用切换。 6. 新增 GameA/GameB 空壳,验证三应用切换。 7. 再逐步把现有 3D demo 或 2D 游戏逻辑迁入对应 Game 目录。 8. 最后重构 CMake,按 `imx6u_gfx` + 应用 target 拆分。 ## 8. 性能注意事项 - 应用切换不应重复销毁/创建 SDL window、renderer、texture。 - 三个应用共用 framebuffer、输入状态和时间源。 - 每个应用可以有自己的资源缓存,但必须有上限和释放策略。 - Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。 - Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。 - UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。