Files
Frostbite2D/docs/开发文档.md
Lenheart 648b94e741 refactor(application): 将SquirrelVM执行逻辑改为启动回调
修改Application::run方法,接收一个启动回调函数参数,将原本直接执行的SquirrelVM::run逻辑改为由调用方通过回调控制。同时更新相关文档说明这一变更。
2026-04-01 05:27:55 +08:00

19 KiB
Raw Blame History

Frostbite2D 开发文档

1. 文档定位

这份文档面向 Frostbite2D 的维护者和协作开发者,目标是帮助你快速理解仓库结构、运行主线、模块职责、资源链路和当前开发约定。

仓库目前由两部分组成:

  • Frostbite2D/:引擎核心代码与公开头文件。
  • Game/:示例入口与资源目录,用来验证引擎能力和联调内容管线。

当前项目更像“引擎 + 集成示例”的单仓库结构,而不是严格拆分的引擎库/游戏仓库。

2. 仓库结构

2.1 顶层目录

Frostbite2D/
├─ Frostbite2D/        # 引擎源码与头文件
├─ Game/               # 示例程序与资源
├─ platform/           # XMake 平台配置
├─ docs/               # 项目文档
├─ xmake.lua           # 根构建入口
└─ README.md           # 项目简介

2.2 引擎目录分层

Frostbite2D/
├─ include/frostbite2D/
│  ├─ 2d/              # Actor、Sprite、TextSprite 等 2D 对象
│  ├─ animation/       # 动画数据与播放对象
│  ├─ audio/           # 音频系统与音频资源
│  ├─ base/            # RefObject、RefPtr、对象注册
│  ├─ core/            # Application、Window
│  ├─ event/           # 输入/窗口事件定义
│  ├─ graphics/        # Renderer、Batch、Camera、Texture、Shader
│  ├─ platform/        # 平台相关接口
│  ├─ resource/        # Asset、NPK/PVF/SoundPack 等资源系统
│  ├─ scene/           # Scene、SceneManager
│  ├─ script/          # SquirrelVM
│  ├─ types/           # 数学类型、颜色、UUID、别名
│  └─ utils/           # 通用工具
└─ src/frostbite2D/    # 对应实现

2.3 示例层目录

  • Game/src/main.cpp:当前主入口,也是理解引擎接入方式的最佳起点。
  • Game/assets/示例资源包括字体、着色器、图片、音频、脚本、NPK/PVF 相关内容。

3. 构建与运行

3.1 构建系统

项目使用 XMake根入口在 xmake.lua。根脚本只负责:

  • 设置项目名、版本、编码和语言标准;
  • 根据 plat 选择 platform/<target>.lua
  • 将平台相关依赖、工具链和构建规则下放到平台脚本。

当前已配置的平台脚本:

  • platform/mingw.lua
  • platform/linux.lua
  • platform/switch.lua

Windows 平台最终会映射到 mingw 配置。

3.2 常用命令

xmake build
xmake clean
xmake build -m debug
xmake build -m release
xmake build -p windows
xmake build -p linux
xmake build -p switch

查看当前配置:

xmake f -c

3.3 依赖概览

从平台脚本看,当前主要依赖包括:

  • SDL2
  • SDL2_image
  • SDL2_mixer
  • SDL2_ttf
  • GLM
  • zlib
  • Squirrel
  • Glad源码随仓库提供

Nintendo Switch 平台使用 devkitPro/libnx/portlibs 工具链与库。

3.4 构建产物与资源复制

mingwlinux 平台都会在构建后执行 after_build

  • 复制 Game/assets 到目标输出目录下的 assets/
  • Windows 额外尝试复制 SDL 及相关动态库;
  • Switch 平台还会生成 nro 相关产物,并复制 assets

这意味着示例程序默认依赖“输出目录旁边有 assets/”这一约定。

3.5 运行与验证

仓库当前没有自动化测试框架,验证方式以手动运行构建产物为主。

建议的最小验证流程:

  1. xmake build
  2. 运行生成的二进制
  3. 观察窗口是否正常创建
  4. 观察字体、精灵、动画是否正常显示
  5. 观察脚本、资源包、输入事件日志是否符合预期

4. 启动流程总览

当前主入口位于 Game/src/main.cpp,可以把启动过程概括为:

  1. 创建 AppConfig
  2. 初始化 Application
  3. 初始化脚本系统和资源归档
  4. 创建场景并压入 SceneManager
  5. 创建文本、动画、精灵、测试 Actor
  6. 调用 Application::run(callback)
  7. 退出时调用 Application::shutdown()

一个简化后的运行主线如下:

main
  -> Application::init
     -> 平台初始化
     -> initCoreModules
        -> Asset 工作目录
        -> SDL 初始化
        -> Window 创建
        -> Renderer 初始化
        -> Camera 创建并绑定 Renderer
  -> 初始化 SquirrelVM / PvfArchive / NpkArchive / FontManager
  -> SceneManager::PushScene
  -> 创建 Sprite / TextSprite / Animation / Actor
  -> Application::run(callback)
     -> 调用方提供的启动回调
     -> 主循环
        -> SDL_PollEvent
        -> SDL 事件转换为引擎事件
        -> SceneManager::Update
        -> SceneManager::Render
        -> Window::swap
  -> Application::shutdown

5. 核心架构

5.1 Application

Application 是运行时总控单例,职责包括:

  • 保存 AppConfig
  • 初始化平台与核心模块;
  • 维护主循环状态;
  • 计算 deltaTime、总运行时间和 FPS
  • 从 SDL 轮询事件并转换成 Frostbite2D 事件;
  • 将更新和渲染委托给 SceneManager
  • 在退出时按顺序释放场景、渲染器、音频和资源归档。

关键接口:

  • Application::get()
  • init() / init(const AppConfig&)
  • run()
  • quit()
  • shutdown()
  • getWindow()
  • getRenderer()
  • deltaTime()
  • fps()

需要注意的实现细节:

  • initCoreModules() 内部会先设置 Asset 工作目录,再做 SDL/窗口/渲染器初始化。
  • run(callback) 会在主循环前执行一次调用方传入的启动回调;Application 本身不再直接依赖 Squirrel。
  • shutdown() 中会清空场景栈、关闭渲染器、关闭音频系统、关闭归档系统,然后销毁窗口。

5.2 Window

Window 是 SDL 窗口与 OpenGL 上下文的包装层,负责:

  • 创建/销毁窗口;
  • 交换缓冲区;
  • 设置标题、大小、位置、全屏和 VSync
  • 管理光标状态;
  • 暴露 resize/close/focus 回调。

窗口配置由 WindowConfig 提供,常用字段包括:

  • width
  • height
  • title
  • resizable
  • fullscreen
  • multisamples
  • vsync

当前 Application 在窗口尺寸变化时会同步更新:

  • Renderer::setViewport
  • Camera::setViewport

5.3 Renderer / Camera / Batch

渲染子系统由 Renderer 统一对外,内部组合了:

  • ShaderManager
  • Batch
  • Camera

Renderer 当前职责:

  • 初始化渲染状态;
  • 控制帧开始/结束;
  • 设置清屏色和视口;
  • 绑定相机;
  • 提供 drawQuaddrawSprite 等基础绘制接口;
  • 在合适时机 flush() 批处理。

Camera 当前由 Application 创建并设置到 Renderer。初始化时会:

  • 设置视口尺寸;
  • 打开 flipY,使 (0, 0) 更符合 2D 左上角坐标习惯。

5.4 SceneManager / Scene

场景系统采用栈式管理:

  • PushScene:旧场景 onExit(),新场景入栈后 onEnter()
  • PopScene:当前场景 onExit() 后弹出,若还有上层场景则重新 onEnter()
  • ReplaceScene:本质上是 PopScene() + PushScene()
  • ClearAll:退出时清空所有场景

更新和渲染只作用于栈顶场景:

  • SceneManager::Update(deltaTime)
  • SceneManager::Render()

Scene 继承自 Actor,默认行为很薄:

  • Update() 直接更新子节点
  • Render() 直接渲染子节点
  • OnEvent() 先处理自身,再向子节点分发

5.5 Actor 树

Actor 是 2D 对象树的基础节点,也是当前最重要的扩展点之一。

核心能力包括:

  • 父子层级管理:AddChild / RemoveChild
  • 变换管理:位置、旋转、缩放、尺寸、锚点
  • 世界变换缓存:懒更新 localTransform_ / worldTransform_
  • 可见性与透明度控制
  • zOrder 排序插入
  • 事件监听与派发
  • UUID 注册到 ObjectRegistry

当前节点树是更新/渲染/事件分发的基础:

  • UpdateChildren() 只更新可见子节点
  • RenderChildren() 只渲染可见子节点
  • SetZOrder() 会触发父节点内重新插入排序

需要注意:

  • Scene 也是 Actor,所以场景本身就是树根节点。
  • AddChild() 时会把子节点的 scene_ 设为当前节点的 scene_
  • markTransformDirty() 会递归标脏所有子节点。

6. 事件系统

6.1 SDL 到引擎事件

Application::convertSDLEvent() 负责把 SDL 事件转换为引擎事件对象。当前已覆盖:

  • 窗口事件:关闭、尺寸变化、焦点变化、最小化、最大化、恢复
  • 键盘事件:按下、抬起、文本输入
  • 鼠标事件:移动、按键、滚轮
  • 触摸事件
  • 手柄事件:按钮与摇杆轴

转换完成后,Application::dispatchEvent() 会把事件交给当前场景:

SDL_Event -> Event -> CurrentScene->OnEvent()

6.2 Actor 事件处理模型

Actor 提供两类事件扩展方式:

  • 注册监听器:AddEventListener(EventType, callback)
  • 继承覆写:OnKeyDownOnMouseDownOnJoystickAxis 等虚函数

事件是否会进入某个节点,取决于:

  • 是否启用了 EnableEventReceive()
  • 是否启用了 EnableEventIntercept()
  • 当前场景对子节点按 eventPriority 排序后的分发结果

当前分发规则大致为:

  • Scene::OnEvent() 先尝试自身处理;
  • 再筛选启用了事件接收的子节点;
  • 根据 eventPriority 排序;
  • 开启拦截的节点直接 OnEvent()
  • 未开启拦截的节点通过 DispatchEvent() 递归向下派发。

这一套模型适合做 UI 控件、输入代理、全局拦截节点等。

7. 资源系统

7.1 Asset

Asset 是底层文件系统包装器,主要职责:

  • 维护工作目录与资源根目录;
  • 处理路径拼接、规范化和绝对路径解析;
  • 提供文本/二进制文件读写;
  • 提供目录创建、删除、遍历;
  • 提供文件信息、扩展名、父路径等辅助能力。

运行时最关键的约定是:

  • Application::initCoreModules() 会把 SDL_GetBasePath() 作为默认工作目录;
  • 构建后 assets/ 会被复制到输出目录;
  • 因此多数相对路径资源访问都会基于运行产物目录工作。

如果资源定位异常,优先检查:

  1. 构建输出目录下是否存在 assets/
  2. Asset 当前工作目录是否符合预期
  3. 平台差异下路径分隔符或编码问题是否被绕开

7.2 NpkArchive

NpkArchive 面向图片包资源,当前示例中通过:

NpkArchive::get().setImagePackDirectory("assets/ImagePacks2");
NpkArchive::get().setDefaultImg("sprite/interface/base.img", 0);
NpkArchive::get().init();

可以把它理解为“图像资源索引与缓存层”,当前职责包括:

  • 扫描 NPK 文件;
  • 解析 img 引用;
  • 根据帧索引取图像帧;
  • 管理缓存大小与驱逐逻辑;
  • 提供默认 img 回退。

Sprite::createFromNpk() 依赖这套系统工作。

7.3 PvfArchive

PvfArchive 面向 PVF 资源包,当前职责包括:

  • 打开 PVF 文件;
  • 解析头部并建立文件索引;
  • 加载 stringtable.bin
  • 加载 n_string.lst
  • 提供文件内容、原始字节和字符串查询能力;
  • 为脚本解析、动画数据读取等模块提供底层数据。

示例初始化方式:

auto& pvf = PvfArchive::get();
if (pvf.open("assets/Script.pvf")) {
  pvf.init();
}

当前动画系统和脚本解析系统都依赖 PVF。

7.4 ScriptParser

ScriptParser 用于解析 PVF 中的二进制脚本数据,支持:

  • RawData 或字节数组构造;
  • 顺序读取值;
  • 回退一个指令;
  • 一次性 parseAll()
  • 根据 PVF 字符串表进行解码。

它更像“底层解析器”,通常不会直接在游戏逻辑层大范围使用,而是作为资源模块或工具模块的实现基础。

7.5 SoundPackArchive / AudioDatabase

音频资源目前分成两层:

  • SoundPackArchive:面向音频包内容;
  • AudioDatabase:面向 audio.xml 这种逻辑名到文件路径的映射。

Game/src/main.cpp 中已经保留了典型接入方式,但部分代码仍处于注释状态,说明这部分还在联调或验证阶段。

8. 图形与内容对象

8.1 Sprite

Sprite 是最基础的图形节点之一,当前至少支持两种常见来源:

  • 从普通文件创建
  • 从 NPK 资源创建

典型流程:

auto sprite = Sprite::createFromNpk("sprite/newtitle/nangua.img", 0);
if (sprite) {
  sprite->SetPosition(220, 10);
  scene->AddChild(sprite);
}

它继承自 Actor,因此天然具备层级、变换、透明度和事件能力。

8.2 TextSprite / FontManager

文字显示由 TextSpriteFontManager 协作完成,当前流程是:

  1. FontManager::init()
  2. registerFont(name, path, size)
  3. TextSprite::create(text, fontName)
  4. 设置颜色、位置等属性
  5. 加入场景树

示例中已经验证:

  • 字体从 assets/Fonts/ 加载
  • 中文文本可以进入显示链路

8.3 Animation / AnimationData

动画系统当前与 PVF 资源紧密耦合。示例中通过:

auto ani = MakePtr<Animation>(
  "monster/event/bluemarble/goblin/animation_goblin2/move.ani");

可以推断当前能力包括:

  • 从 PVF 路径解析 .ani
  • 关联 .als 或其他描述文件
  • 生成可加入场景树的动画对象

这一块是项目里较有特色的内容链路,后续扩展新动画能力时,应优先梳理 AnimationAnimationDataPvfArchive 的数据流。

9. 脚本系统

脚本运行时由 SquirrelVM 单例管理,当前公开能力包括:

  • 设置脚本目录
  • 打开/关闭 VM
  • 加载脚本文件
  • 执行脚本
  • 控制 debug 模式

示例中的接入顺序:

SquirrelVM::get().setScriptDirectory("assets/scripts");
SquirrelVM::get().init();

随后由调用方在 Application::run(callback) 的启动回调中决定是否执行 SquirrelVM::run()

这意味着脚本系统目前是“由上层控制的可选启动模块”,而不是 Application::init()Application::run() 的强制步骤。

10. 音频系统

音频播放能力由以下对象协作:

  • AudioSystem:全局初始化、音量控制、暂停/恢复/停止
  • Music:背景音乐
  • Sound:音效
  • SoundPackArchive:音频资源包
  • AudioDatabase:逻辑名到实际资源路径的映射

从现有代码看,典型接入顺序应是:

  1. AudioSystem::init()
  2. 配置主音量、音效音量、音乐音量
  3. 初始化 SoundPackArchive
  4. 加载 AudioDatabase
  5. 通过 MusicSound 加载并播放

不过示例中的音频演示代码大部分仍是注释状态,因此目前更适合把它视为“已具备基础设施,正在逐步验证的模块”。

11. 示例程序现状

Game/src/main.cpp 目前承担三类职责:

  • 展示引擎最小启动路径;
  • 验证资源包、动画、文本、事件等模块是否正常联动;
  • 作为日常回归测试入口。

示例里当前已经体现的能力包括:

  • 设置窗口尺寸和标题
  • 初始化脚本目录与 SquirrelVM
  • 打开 PVF 并初始化字符串资源
  • 初始化 NPK 图像包
  • 创建并压入场景
  • 注册字体并创建文本节点
  • 创建动画节点
  • 创建测试 Actor 并监听手柄事件
  • 从 NPK 中创建精灵并加入场景

因此,阅读示例时建议优先关注“启动顺序”和“模块初始化顺序”,而不是把它当成最终游戏逻辑模板。

12. 推荐阅读路径

第一次接手这个项目,建议按下面顺序看代码:

  1. Game/src/main.cpp
  2. Frostbite2D/include/frostbite2D/core/application.h
  3. Frostbite2D/src/frostbite2D/core/application.cpp
  4. Frostbite2D/include/frostbite2D/scene/scene.h
  5. Frostbite2D/include/frostbite2D/2d/actor.h
  6. Frostbite2D/include/frostbite2D/resource/asset.h
  7. Frostbite2D/include/frostbite2D/resource/npk_archive.h
  8. Frostbite2D/include/frostbite2D/resource/pvf_archive.h
  9. Frostbite2D/include/frostbite2D/graphics/renderer.h
  10. Frostbite2D/include/frostbite2D/script/squirrel_vm.h

这样能先建立运行主线,再理解渲染、资源和扩展系统。

13. 开发约定与维护建议

13.1 已观察到的代码风格

  • 类名使用 PascalCase
  • 方法名大量使用大写开头的引擎风格命名,如 SetPositionGetScene
  • 成员变量普遍使用尾随下划线
  • 单例模式被广泛使用
  • 错误处理倾向于返回 bool 和 SDL 日志,而不是异常

需要注意:仓库规范文档里建议方法使用 camelCase但现有代码中同时存在 SetPosition / GetCurrentScene / PushScene 这类风格,后续维护应优先保持同模块内一致性。

13.2 初始化顺序很重要

当前模块间存在明显依赖顺序:

  • 必须先有 Application,再有窗口和渲染器;
  • 必须先初始化资源系统,再读取 PVF/NPK
  • 必须先注册字体,再创建 TextSprite
  • 若要在启动时跑脚本,需先初始化 SquirrelVM
  • 若要播放音频,必须先初始化 AudioSystem

排查问题时,优先确认是不是初始化顺序错误。

13.3 路径与资源问题

项目强依赖运行目录和资源复制行为:

  • 不要假设 IDE 工作目录一定正确;
  • 尽量通过 Asset 统一处理路径;
  • 构建后若缺 assets,资源系统基本都会失效;
  • Windows 下中文路径和 UTF-8 问题应优先通过 Asset 和统一路径处理规避。

13.4 当前测试策略

目前没有单元测试或集成测试框架,项目依赖:

  • 编译通过
  • 示例启动成功
  • 场景与资源肉眼验证
  • SDL 日志辅助定位问题

如果后续要提高可维护性,优先值得补的是:

  • 资源解析层的离线测试
  • 数学/路径/解析器这类无图形依赖模块的单元测试
  • 示例程序的最小化 smoke test

14. 当前已知边界

在写功能或继续扩展前,建议先接受这些现状:

  • 当前仓库主产物是一个二进制程序,而不是独立分发的静态/动态引擎库。
  • Game 不只是 demo也承载了不少联调职责。
  • 音频系统虽然已有接口,但示例联调还不完整。
  • 文档和代码注释里存在部分编码显示异常,不影响源码结构判断,但会影响阅读体验。
  • 自动化测试缺失,因此回归成本主要落在手动验证上。

15. 一个最小接入模板

如果要快速新建一个最小实验入口,可以参考下面的初始化顺序:

AppConfig config = AppConfig::createDefault();
config.windowConfig.width = 1280;
config.windowConfig.height = 720;
config.windowConfig.title = "My Frostbite2D App";

Application& app = Application::get();
if (!app.init(config)) {
  return -1;
}

auto scene = MakePtr<Scene>();
SceneManager::get().PushScene(scene);

app.run([]() {
  // optional startup logic
});
app.shutdown();

如果要继续接入资源、文字或动画,再按需补:

  • SquirrelVM::init()
  • PvfArchive::open() + init()
  • NpkArchive::setImagePackDirectory() + init()
  • FontManager::init() + registerFont()

16. 后续文档扩展建议

这份文档适合作为总览文档,后续可以继续拆出专题文档:

  • 渲染系统设计说明
  • NPK/PVF 资源格式接入说明
  • 动画系统数据流说明
  • Squirrel 脚本桥接说明
  • 平台移植说明

如果未来要对外开放引擎 API再补一份按模块组织的 API 参考手册会更合适。