From a2c218f69040a6b309ccf1e44563db828c4c2a3c Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Sun, 7 Jun 2026 15:24:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 4 +- docs/APP_AND_CORE_ARCHITECTURE.md | 320 ++++++++++++++++++++++-------- docs/APP_AND_GFX_ARCHITECTURE.md | 262 ------------------------ 4 files changed, 245 insertions(+), 342 deletions(-) delete mode 100644 docs/APP_AND_GFX_ARCHITECTURE.md diff --git a/.gitignore b/.gitignore index ffeef3c..edbf467 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ build-win build-linux build-arm-fb build-arm-sdl +build-check .idea diff --git a/README.md b/README.md index 75c3853..6024a97 100644 --- a/README.md +++ b/README.md @@ -301,8 +301,8 @@ IMX6U-Game/ - 双平台显示后端(SDL2 / Framebuffer) - 离线资源转换工具:PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header - 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap -- Gfx 目录规范化,代码收敛到 `src/Gfx/` -- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力 +- Core 目录规范化,代码收敛到 `src/Core/` +- `Core::DrawContext` 统一绘制入口,封装现有绘制能力 - C++11 兼容代码 - CMake 跨平台构建 diff --git a/docs/APP_AND_CORE_ARCHITECTURE.md b/docs/APP_AND_CORE_ARCHITECTURE.md index 140d5ef..c5f31ce 100644 --- a/docs/APP_AND_CORE_ARCHITECTURE.md +++ b/docs/APP_AND_CORE_ARCHITECTURE.md @@ -1,8 +1,10 @@ -# 应用层与 Core 分层设计 +# 应用层与底层库分层设计 -本文记录当前项目的代码分层。`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。 +本文记录项目的代码分层:`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。文档同时记录当前已实现状态和目标架构。 -## 目录边界 +> 文档分工:IMX6U 运行时性能红线记录在 `DEVELOPMENT_GUIDELINES.md`;坐标、矩阵、深度等数学语义记录在 `CONVENTIONS.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。 + +## 1. 当前目录结构 ```text src/ @@ -17,7 +19,7 @@ src/ Scene/ # Camera、Transform、Mesh、Model Shading/ # Shader 相关代码 Apps/ - Demo/ # Core 能力演示 + Demo/ # 2D sprite/tilemap 性能测试入口 Game/ # Tom 游戏 ``` @@ -26,9 +28,118 @@ src/ - `src/Core`:整个底层库。 - `src/Core/Core`:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。 -后续如果觉得重复命名影响阅读,可以再把 `src/Core/Core` 单独改成 `Runtime/` 或 `Buffer/`,但这次先只做旧底层库名称到 `Core` 的一致性修复。 +后续如果觉得重复命名影响阅读,可以再把 `src/Core/Core` 单独改成 `Runtime/` 或 `Buffer/`。 -## 依赖方向 +## 2. 目标架构 + +后续项目最终应拆成四个逻辑层: + +```text +IMX6U-Game/ +├─ src/ +│ ├─ Core/ # 底层库:可复用、无具体游戏规则 +│ │ ├─ Draw2D/ # DrawContext 统一绘制入口(✅ 已实现) +│ │ ├─ Core/ # FrameBuffer、DepthBuffer、Timer(✅ 已实现) +│ │ ├─ Math/ # 向量、矩阵、数学工具(✅ 已实现) +│ │ ├─ Rasterizer/ # 线段、三角形光栅化(✅ 已实现) +│ │ ├─ RenderData/ # Color、Triangle 等数据结构(✅ 已实现) +│ │ ├─ Scene/ # Camera、Transform(✅ 已实现) +│ │ ├─ Shading/ # 着色器(预留) +│ │ ├─ Platform/ # SDL2 / fb0 显示适配与独立时间源(✅ 已实现) +│ │ └─ Asset/ # 资源加载(✅ 已实现) +│ ├─ Apps/ +│ │ ├─ Demo/ # 2D sprite/tilemap 性能测试入口(✅ 已实现) +│ │ ├─ Game/ # Tom 游戏(✅ 已实现) +│ │ ├─ Launcher/ # 启动器应用(待实现) +│ │ ├─ GameA/ # 第一个游戏(待实现) +│ │ └─ GameB/ # 第二个游戏(待实现) +│ └─ Shared/ # 可选:应用层共享但不属于 Core 的东西 +│ ├─ Save/ # 存档格式、配置读写 +│ ├─ UI/ # 启动器和游戏共用 UI 组件 +│ └─ Assets/ # 应用层资源索引、资源命名约定 +├─ assets/ +│ ├─ font/ # 共享像素字体源文件与生成的 font_atlas +│ ├─ sprite/ # 当前 demo/test sprite 源文件与生成头文件 +│ ├─ launcher/ +│ ├─ game_a/ +│ ├─ game_b/ +│ └─ shared/ +├─ tools/ +│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header +│ └─ png_to_header.py # PNG -> uint32_t RGBA header +└─ docs/ +``` + +## 3. 各层职责 + +### 3.1 Core:底层库 + +职责: + +- 管理 framebuffer、depthbuffer、渲染上下文。 +- 提供基础绘制接口:点、线、矩形、四边形、三角形、sprite、SpriteRegion、tilemap、简单文本等。 +- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。 +- 封装 SDL2 / framebuffer 显示提交、输入轮询,并通过独立 `ITimeSource` 提供单调整数毫秒时间。 +- 提供音频输入、音频输出和按键输入的抽象接口。 +- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas),运行时不直接依赖 PNG/TTF 解码。 +- 只关心"怎么画得快、怎么提交到屏幕",不关心具体游戏规则。 + +禁止: + +- 不依赖 `GameA`、`GameB`、`Launcher`。 +- 不包含具体关卡、角色、菜单流程、游戏状态机。 +- 不直接读取某个游戏专属资源路径。 +- 不在核心绘制接口中暴露 SDL2 类型。 +- 不在每帧热路径中执行图片/字体解码;资源转换应在构建前或工具阶段完成。 + +### 3.2 Apps/GameA 与 Apps/GameB:两个游戏 + +职责: + +- 各自维护自己的游戏规则、状态机、关卡、实体、碰撞、计分、胜负逻辑。 +- 通过 Core 提供的接口绘制画面。 +- 通过平台输入快照读取按键/触摸状态,而不是直接调用 SDL。 +- 管理自己的资源索引和场景切换。 + +禁止: + +- 游戏之间互相 include 对方代码。 +- 游戏直接操作 SDL renderer / texture / window。 +- 游戏直接依赖 framebuffer 内存布局,除非是明确标注的性能特例。 + +### 3.3 Apps/Launcher:启动器 + +职责: + +- 展示游戏列表、图标、说明、版本信息。 +- 选择并启动 GameA 或 GameB。 +- 管理全局设置,例如音量、亮度、输入校准、语言等。 +- 可以复用 Shared/UI,但不承载具体游戏逻辑。 + +启动方式有两种可选方案: + +1. **单进程多应用模式**:Launcher、GameA、GameB 编译成一个可执行文件,内部切换当前 App。 +2. **多进程模式**:Launcher 是独立程序,选择后启动另一个游戏可执行文件。 + +对 IMX6U 初期开发,推荐先用 **单进程多应用模式**,原因: + +- 构建、部署、调试更简单。 +- SDL2 初始化、资源缓存、输入状态可以共用。 +- 避免频繁退出/启动进程带来的黑屏、资源重载和状态恢复问题。 + +等项目成熟后,如果每个游戏体积较大,或者需要独立更新,再考虑多进程拆分。 + +### 3.4 Shared:应用层共享模块 + +Shared 只放"应用层共享,但不属于底层库"的内容,例如: + +- UI 控件:按钮、列表、九宫格面板、菜单焦点管理。 +- 存档/设置:配置文件、最高分、解锁状态。 +- 资源清单:多个应用共用的字体、图标、音效索引。 + +Shared 可以依赖 Core;Core 不能依赖 Shared。 + +## 4. 依赖方向 允许: @@ -37,63 +148,77 @@ Apps -> Shared -> Core -> Platform Apps -> Core ``` -禁止: +禁止反向依赖: ```text -Core -> Apps -Core -> Shared -GameA -> GameB -GameB -> GameA -Platform -> Game +Core -> Apps 禁止 +Core -> Shared 禁止 +GameA -> GameB 禁止 +GameB -> GameA 禁止 +Platform -> Game 禁止 ``` Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。 -## Core 职责 +## 5. 平台层接口 -Core 只提供底层能力: +平台层采用"抽象接口 + 多套后端实现"的模式。游戏代码只能依赖这些 `I*` 接口。 -- 管理 `FrameBuffer`、`DepthBuffer` 和绘制上下文。 -- 提供基础绘制接口:line、triangle、sprite、sprite region、tilemap、bitmap font。 -- 提供基础数学、颜色、图片、三角形、tilemap 等数据结构。 -- 封装 SDL2 / framebuffer 显示提交。 -- 提供独立时间源 `Platform::ITimeSource`。 -- 提供音频输入、音频输出和按键输入的抽象接口。 -- 使用离线转换后的运行时资源,不在热路径解码 PNG/TTF。 +### 5.1 显示 -Core 不做: - -- 不实现具体游戏规则。 -- 不直接读取某个游戏专属资源目录。 -- 不在核心绘制接口暴露 SDL2 类型。 -- 不在每帧热路径中执行图片、字体解码或文件 IO。 - -## 应用职责 - -`src/Apps/*` 负责具体应用流程: - -- 创建具体游戏或 Demo 的主循环。 -- 加载应用自己的资源。 -- 调用 `Core::DrawContext` 绘制画面。 -- 根据平台输入更新游戏状态。 - -当前主程序位于: +`Platform::IDisplay`(`src/Core/Platform/Display.h`): ```text -src/Apps/Game/Main.cpp +Platform::IDisplay + SDLDisplay # PC / SDL2 调试后端 + FBDisplay # Linux /dev/fb0 后端 ``` -它默认启动 Tom 游戏视觉入口。 - -## DrawContext - -`Core::DrawContext` 是当前统一绘制入口,位于: +### 5.2 音频输入 ```text -src/Core/Draw2D/DrawContext.h -src/Core/Draw2D/DrawContext.cpp +Platform::IAudioInput + SdlAudioInput # PC / SDL2 麦克风后端 + AlsaAudioInput # Linux ALSA 录音后端 ``` +### 5.3 音频输出 + +```text +Platform::IAudioOutput + SdlAudioOutput # PC / SDL2 扬声器后端 + AlsaAudioOutput # Linux ALSA 播放后端 +``` + +### 5.4 按键输入 + +```text +Platform::IButtonInput + SdlKeyboardButtonInput # PC / SDL2 键盘后端,默认空格键 + EvdevButtonInput # Linux evdev 按键后端 +``` + +### 5.5 默认后端 + +如果只需要当前构建平台的默认后端,可以使用 `Platform::DefaultAudioInput`、`Platform::DefaultAudioOutput` 和 `Platform::DefaultButtonInput`。 + +### 5.6 时间源 + +时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep。 + +ALSA、evdev、SDL2、`/dev/fb0` 等平台细节只能出现在 `src/Core/Platform` 或明确的平台适配代码中。 + +### 5.7 CMake 后端切换 + +```cmake +-DUSE_FRAMEBUFFER=OFF # 默认,使用 SDLDisplay +-DUSE_FRAMEBUFFER=ON # 使用 FBDisplay +``` + +## 6. DrawContext + +`Core::DrawContext` 是当前统一绘制入口,位于 `src/Core/Draw2D/DrawContext.h`。 + 它封装: - `Core::FrameBuffer` @@ -111,53 +236,92 @@ ctx.draw_text(font, x, y, color, "text"); ctx.present(display); ``` -## 显示后端 +## 7. 应用统一接口(待实现) -平台层采用“抽象接口 + 多套后端实现”的模式。 +推荐为 Launcher、GameA、GameB 提供统一应用接口,例如: -显示层通过 `Platform::IDisplay` 抽象: +```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(Core::DrawContext& ctx) = 0; + virtual AppId next_app() const = 0; +}; +``` + +主循环只认识 `IApp`: ```text -Platform::IDisplay - SDLDisplay # PC / SDL2 调试后端 - FBDisplay # Linux /dev/fb0 后端 +poll input -> update current app -> render current app -> present framebuffer ``` -音频输入通过 `Platform::IAudioInput` 抽象: +这样三个应用层共用同一个主循环、同一套 SDL2 初始化、framebuffer 提交流程和独立时间源。 -```text -Platform::IAudioInput - SdlAudioInput # PC / SDL2 麦克风后端 - AlsaAudioInput # Linux ALSA 录音后端 -``` +注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 `draw_rect`、`draw_quad`、`set_pixel_fast` 这类热路径函数不要虚化。 -音频输出通过 `Platform::IAudioOutput` 抽象: +当前状态:`IApp` 接口尚未实现。`src/Apps/Game/Main.cpp` 直接编写主循环,未经过 `IApp` 抽象。 -```text -Platform::IAudioOutput - SdlAudioOutput # PC / SDL2 扬声器后端 - AlsaAudioOutput # Linux ALSA 播放后端 -``` +## 8. 命名建议 -按键输入通过 `Platform::IButtonInput` 抽象: +- 项目仓库仍叫 `IMX6U-Game`。 +- 底层库命名为 `Core`。 +- 三个应用用明确名字:`Launcher`、`GameA`、`GameB`,后续再替换成真实游戏名。 +- CMake target 可以是: + - `imx6u_core` 静态库 + - `imx6u_launcher` + - `imx6u_game_a` + - `imx6u_game_b` + - 或初期单可执行文件 `imx6u_suite` -```text -Platform::IButtonInput - SdlKeyboardButtonInput # PC / SDL2 键盘后端,默认空格键 - EvdevButtonInput # Linux evdev 按键后端 -``` +## 9. 推荐演进顺序 -游戏代码只能依赖这些 `I*` 接口。ALSA、evdev、SDL2、`/dev/fb0` 等平台细节只能出现在 `src/Core/Platform` 或明确的平台适配代码中。 -如果只需要当前构建平台的默认后端,可以使用 `Platform::DefaultAudioInput`、`Platform::DefaultAudioOutput` 和 `Platform::DefaultButtonInput`。 +1. ~~先抽出统一 `IApp` 和 `AppManager`,让当前 demo 成为一个 app。~~ 未实现,当前直接写主循环。 +2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~ **已完成** +3. ~~建立 `Core::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**(`Core::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_sprite_region、draw_text、draw_tilemap、present) +4. ~~底层代码统一放在 `src/Core/`,Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成** +5. 新增 Launcher app,只做最小菜单和应用切换。 +6. 新增 GameA/GameB 空壳,验证三应用切换。 +7. 再逐步把现有 3D demo 能力恢复为独立验证入口,或把 2D 游戏逻辑迁入对应 Game 目录。 +8. 最后重构 CMake,按 `imx6u_core` + 应用 target 拆分。 -CMake 通过 `USE_FRAMEBUFFER` 选择实现: +## 10. 性能注意事项 -```cmake --DUSE_FRAMEBUFFER=OFF # 默认,使用 SDLDisplay --DUSE_FRAMEBUFFER=ON # 使用 FBDisplay -``` +- 应用切换不应重复销毁/创建 SDL window、renderer、texture。 +- 三个应用共用 framebuffer、输入状态和 `Platform::ITimeSource` 时间源;Display 不承担计时职责。 +- `Core::DrawContext` 是当前统一绘制入口。 +- 每个应用可以有自己的资源缓存,但必须有上限和释放策略。 +- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。 +- Core 的绘制函数要保持小而直接,优先内联和连续内存写入。 +- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。 +- 无 3D 内容的 2D 应用应使用只清颜色缓冲的路径,避免每帧清理 depth buffer。 +- `/dev/fb0` 后端提交可能是主要瓶颈;板端性能分析应拆分 `Frame` 和 `Present` 耗时,并确认 ARM 构建为 Release。 +- 直接写 framebuffer 不是原子换屏,LCD 扫描会让局部动画看起来比完整帧率更连续;判断性能应以计时数据为准。 -## 后续建议 +## 11. 资源转换约定 + +- 运行时资源优先使用离线转换后的简单数组,不在 IMX6U 运行时解码 PNG/TTF。 +- `tools/png_to_header.py` 将 PNG 转为 `uint32_t` RGBA 数组,适用于 sprite、小图标、测试纹理等。 +- `tools/gen_font_atlas.py` 将共享像素字体转为 ASCII bitmap atlas,并输出同名 PNG 预览和 C++ 头文件。 +- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。 +- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应再评估 1-bit/8-bit mask、RLE 或自定义资源包格式。 + +## 12. SpriteRegion 与 Tilemap 约定 + +- `RenderData::SpriteRegion` 只描述某张 atlas 中的子区域,不拥有像素数据;它通过 `const Image* atlas` 引用源图。 +- `DrawContext::draw_sprite_ex` 是底层 sprite 绘制入口,负责源区域检查、目标屏幕裁剪、scale 和 flip;`draw_sprite_region` 系列只是对 atlas 子区域的语义包装。 +- `RenderData::Tilemap` 使用 `uint16_t` tile id 保存地图网格,`Tilemap::EmptyTile` (`0xFFFF`) 表示空 tile。 +- `Tilemap` 当前只支持一个 atlas、固定 tile 宽高和固定 `atlas_columns`;tile id 通过 `tile_id % atlas_columns` / `tile_id / atlas_columns` 映射到 atlas 中的源区域。 +- `DrawContext::draw_tilemap` 的裁剪分两层: + - tilemap 层按 tile 计算需要尝试绘制的可见范围; + - sprite 层按像素裁剪每个 tile,支持 camera 像素级滚动时显示半个 tile。 +- 带 `viewport_w` / `viewport_h` 的 `draw_tilemap` 重载用于子视口绘制,`screen_x` / `screen_y` 是视口左上角;旧重载默认视口延伸到 framebuffer 右下角。 +- 当前实现优先保证清晰语义和可验证行为;后续性能优化可增加 tile region 查表、不透明 tile 行拷贝、chunk/dirty rect 或专用 tile 快路径。 + +## 13. 后续建议 1. 保持 `Core` 不依赖 `Apps`。 2. 新增游戏逻辑放在 `src/Apps/Game` 或新的 `src/Apps/*`。 diff --git a/docs/APP_AND_GFX_ARCHITECTURE.md b/docs/APP_AND_GFX_ARCHITECTURE.md deleted file mode 100644 index 6591a45..0000000 --- a/docs/APP_AND_GFX_ARCHITECTURE.md +++ /dev/null @@ -1,262 +0,0 @@ -# 应用层与图形库分层设计 - -本项目后续包含三个应用层目标:两个游戏和一个启动器;同时还需要沉淀一套可复用的 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/ # 当前板端性能 demo;历史上也用于 3D 立方体验证(✅ 已实现) -│ │ ├─ Launcher/ # 启动器应用(待实现) -│ │ ├─ GameA/ # 第一个游戏(待实现) -│ │ └─ GameB/ # 第二个游戏(待实现) -│ └─ Shared/ # 可选:应用层共享但不属于 Gfx 的东西 -│ ├─ Save/ # 存档格式、配置读写 -│ ├─ UI/ # 启动器和游戏共用 UI 组件 -│ └─ Assets/ # 应用层资源索引、资源命名约定 -├─ assets/ -│ ├─ font/ # 共享像素字体源文件与生成的 font_atlas -│ ├─ sprite/ # 当前 demo/test sprite 源文件与生成头文件 -│ ├─ launcher/ -│ ├─ game_a/ -│ ├─ game_b/ -│ └─ shared/ -├─ tools/ -│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header -│ └─ png_to_header.py # PNG -> uint32_t RGBA header -└─ 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、SpriteRegion、tilemap、简单文本等。 -- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。 -- 封装 SDL2 / framebuffer 显示提交、输入轮询,并通过独立 `ITimeSource` 提供单调整数毫秒时间。 -- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas),运行时不直接依赖 PNG/TTF 解码。 -- 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。 - -禁止: - -- 不依赖 `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/ITimeSource) -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); - void draw_sprite(int32_t x, int32_t y, const RenderData::Image& image); - void draw_sprite_region(int32_t x, int32_t y, - const RenderData::SpriteRegion& region); - void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y, - RenderData::Color color, const char* text); - void draw_tilemap(const RenderData::Tilemap& tilemap, - int32_t screen_x, int32_t screen_y, - int32_t viewport_w, int32_t viewport_h, - int32_t camera_x, int32_t camera_y); - }; -} -``` - -后续再扩展: - -- `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、draw_sprite、draw_sprite_region、draw_text、draw_tilemap、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、输入状态和 `Platform::ITimeSource` 时间源;Display 不承担计时职责。 -- 每个应用可以有自己的资源缓存,但必须有上限和释放策略。 -- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。 -- Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。 -- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。 -- 无 3D 内容的 2D 应用应使用只清颜色缓冲的路径,避免每帧清理 depth buffer。 -- `/dev/fb0` 后端提交可能是主要瓶颈;板端性能分析应拆分 `Frame` 和 `Present` 耗时,并确认 ARM 构建为 Release。 -- 直接写 framebuffer 不是原子换屏,LCD 扫描会让局部动画看起来比完整帧率更连续;判断性能应以计时数据为准。 - -## 9. 资源转换约定 - -- 运行时资源优先使用离线转换后的简单数组,不在 IMX6U 运行时解码 PNG/TTF。 -- `tools/png_to_header.py` 将 PNG 转为 `uint32_t` RGBA 数组,适用于 sprite、小图标、测试纹理等。 -- `tools/gen_font_atlas.py` 将共享像素字体转为 ASCII bitmap atlas,并输出同名 PNG 预览和 C++ 头文件。 -- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。 -- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应再评估 1-bit/8-bit mask、RLE 或自定义资源包格式。 - -## 10. SpriteRegion 与 Tilemap 约定 - -- `RenderData::SpriteRegion` 只描述某张 atlas 中的子区域,不拥有像素数据;它通过 `const Image* atlas` 引用源图。 -- `DrawContext::draw_sprite_ex` 是底层 sprite 绘制入口,负责源区域检查、目标屏幕裁剪、scale 和 flip;`draw_sprite_region` 系列只是对 atlas 子区域的语义包装。 -- `RenderData::Tilemap` 使用 `uint16_t` tile id 保存地图网格,`Tilemap::EmptyTile` (`0xFFFF`) 表示空 tile。 -- `Tilemap` 当前只支持一个 atlas、固定 tile 宽高和固定 `atlas_columns`;tile id 通过 `tile_id % atlas_columns` / `tile_id / atlas_columns` 映射到 atlas 中的源区域。 -- `DrawContext::draw_tilemap` 的裁剪分两层: - - tilemap 层按 tile 计算需要尝试绘制的可见范围; - - sprite 层按像素裁剪每个 tile,支持 camera 像素级滚动时显示半个 tile。 -- 带 `viewport_w` / `viewport_h` 的 `draw_tilemap` 重载用于子视口绘制,`screen_x` / `screen_y` 是视口左上角;旧重载默认视口延伸到 framebuffer 右下角。 -- 当前实现优先保证清晰语义和可验证行为;后续性能优化可增加 tile region 查表、不透明 tile 行拷贝、chunk/dirty rect 或专用 tile 快路径。