IMX6U-Game/docs/APP_AND_CORE_ARCHITECTURE.md

14 KiB
Raw Blame History

应用层与底层库分层设计

本文记录项目的代码分层:src/Core 是可复用底层库,src/Apps 放具体应用和游戏。文档同时记录当前已实现状态和目标架构。

文档分工IMX6U 运行时性能红线记录在 DEVELOPMENT_GUIDELINES.md;坐标、矩阵、深度等数学语义记录在 CONVENTIONS.md。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。

1. 当前目录结构

src/
  Core/                 # 底层库:渲染、数学、资源、平台适配
    Asset/              # 离线资源格式加载
    Core/               # FrameBuffer、DepthBuffer、Renderer、Timer
    Draw2D/             # Core::DrawContext
    Math/               # Vector、Matrix、MathUtil
    Platform/           # 显示、时间、音频、按键等平台接口和后端
    Rasterizer/         # 线段和三角形光栅化
    RenderData/         # Color、Image、Triangle、Tilemap 等数据结构
    Scene/              # Camera、Transform、Mesh、Model
    Shading/            # Shader 相关代码
  Apps/
    Demo/               # 2D sprite/tilemap 性能测试入口
    Game/               # Tom 游戏

src/Core/Core 这个二级目录保留的是原底层库里的核心运行时对象。它和顶层 src/Core 名称重复,但含义不同:

  • src/Core:整个底层库。
  • src/Core/Core:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。

后续如果觉得重复命名影响阅读,可以再把 src/Core/Core 单独改成 Runtime/Buffer/

2. 目标架构

后续项目最终应拆成四个逻辑层:

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 解码。
  • 只关心"怎么画得快、怎么提交到屏幕",不关心具体游戏规则。

禁止:

  • 不依赖 GameAGameBLauncher
  • 不包含具体关卡、角色、菜单流程、游戏状态机。
  • 不直接读取某个游戏专属资源路径。
  • 不在核心绘制接口中暴露 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 可以依赖 CoreCore 不能依赖 Shared。

4. 依赖方向

允许:

Apps -> Shared -> Core -> Platform
Apps -> Core

禁止反向依赖:

Core -> Apps        禁止
Core -> Shared      禁止
GameA -> GameB     禁止
GameB -> GameA     禁止
Platform -> Game   禁止

Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。

5. 平台层接口

平台层采用"抽象接口 + 多套后端实现"的模式。游戏代码只能依赖这些 I* 接口。

5.1 显示

Platform::IDisplaysrc/Core/Platform/Display.h

Platform::IDisplay
  SDLDisplay   # PC / SDL2 调试后端
  FBDisplay    # Linux /dev/fb0 后端

5.2 音频输入

Platform::IAudioInput
  SdlAudioInput    # PC / SDL2 麦克风后端
  AlsaAudioInput   # Linux ALSA 录音后端

5.3 音频输出

Platform::IAudioOutput
  SdlAudioOutput   # PC / SDL2 扬声器后端
  AlsaAudioOutput  # Linux ALSA 播放后端

5.4 按键输入

Platform::IButtonInput
  SdlKeyboardButtonInput  # PC / SDL2 键盘后端,默认空格键
  EvdevButtonInput        # Linux evdev 按键后端

5.5 默认后端

如果只需要当前构建平台的默认后端,可以使用 Platform::DefaultAudioInputPlatform::DefaultAudioOutputPlatform::DefaultButtonInput

5.6 时间源

时间源不挂在 IDisplay 上;核心逻辑从 Platform::ITimeSource 读取单调整数毫秒,并使用 Core::Timer 生成整数 tick / fixed timestep。

ALSA、evdev、SDL2、/dev/fb0 等平台细节只能出现在 src/Core/Platform 或明确的平台适配代码中。

5.7 CMake 后端切换

-DUSE_FRAMEBUFFER=OFF  # 默认,使用 SDLDisplay
-DUSE_FRAMEBUFFER=ON   # 使用 FBDisplay

6. DrawContext

Core::DrawContext 是当前统一绘制入口,位于 src/Core/Draw2D/DrawContext.h

它封装:

  • Core::FrameBuffer
  • Core::DepthBuffer
  • Rasterizer::Rasterizer
  • Rasterizer::TriangleRasterizer

对外提供:

Core::DrawContext ctx(width, height);
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.draw_sprite(x, y, image);
ctx.draw_text(font, x, y, color, "text");
ctx.present(display);

7. 应用统一接口(待实现)

推荐为 Launcher、GameA、GameB 提供统一应用接口,例如:

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

poll input -> update current app -> render current app -> present framebuffer

这样三个应用层共用同一个主循环、同一套 SDL2 初始化、framebuffer 提交流程和独立时间源。

注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 draw_rectdraw_quadset_pixel_fast 这类热路径函数不要虚化。

当前状态:IApp 接口尚未实现。src/Apps/Game/Main.cpp 直接编写主循环,未经过 IApp 抽象。

8. 命名建议

  • 项目仓库仍叫 IMX6U-Game
  • 底层库命名为 Core
  • 三个应用用明确名字:LauncherGameAGameB,后续再替换成真实游戏名。
  • CMake target 可以是:
    • imx6u_core 静态库
    • imx6u_launcher
    • imx6u_game_a
    • imx6u_game_b
    • 或初期单可执行文件 imx6u_suite

9. 推荐演进顺序

  1. 先抽出统一 IAppAppManager,让当前 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. 最后重构 CMakeimx6u_core + 应用 target 拆分。

10. 性能注意事项

  • 应用切换不应重复销毁/创建 SDL window、renderer、texture。
  • 三个应用共用 framebuffer、输入状态和 Platform::ITimeSource 时间源Display 不承担计时职责。
  • Core::DrawContext 是当前统一绘制入口。
  • 每个应用可以有自己的资源缓存,但必须有上限和释放策略。
  • Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
  • Core 的绘制函数要保持小而直接,优先内联和连续内存写入。
  • UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。
  • 无 3D 内容的 2D 应用应使用只清颜色缓冲的路径,避免每帧清理 depth buffer。
  • /dev/fb0 后端提交可能是主要瓶颈;板端性能分析应拆分 FramePresent 耗时,并确认 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 和 flipdraw_sprite_region 系列只是对 atlas 子区域的语义包装。
  • RenderData::Tilemap 使用 uint16_t tile id 保存地图网格,Tilemap::EmptyTile (0xFFFF) 表示空 tile。
  • Tilemap 当前只支持一个 atlas、固定 tile 宽高和固定 atlas_columnstile id 通过 tile_id % atlas_columns / tile_id / atlas_columns 映射到 atlas 中的源区域。
  • DrawContext::draw_tilemap 的裁剪分两层:
    • tilemap 层按 tile 计算需要尝试绘制的可见范围;
    • sprite 层按像素裁剪每个 tile支持 camera 像素级滚动时显示半个 tile。
  • viewport_w / viewport_hdraw_tilemap 重载用于子视口绘制,screen_x / screen_y 是视口左上角;旧重载默认视口延伸到 framebuffer 右下角。
  • 当前实现优先保证清晰语义和可验证行为;后续性能优化可增加 tile region 查表、不透明 tile 行拷贝、chunk/dirty rect 或专用 tile 快路径。

13. 后续建议

  1. 保持 Core 不依赖 Apps
  2. 新增游戏逻辑放在 src/Apps/Game 或新的 src/Apps/*
  3. 新增底层绘制、数学、资源格式、平台显示能力放在 src/Core
  4. 如果继续清理命名,优先处理 src/Core/Core 这个重复目录名。