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

651 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frostbite2D 开发文档
## 1. 文档定位
这份文档面向 Frostbite2D 的维护者和协作开发者,目标是帮助你快速理解仓库结构、运行主线、模块职责、资源链路和当前开发约定。
仓库目前由两部分组成:
- `Frostbite2D/`:引擎核心代码与公开头文件。
- `Game/`:示例入口与资源目录,用来验证引擎能力和联调内容管线。
当前项目更像“引擎 + 集成示例”的单仓库结构,而不是严格拆分的引擎库/游戏仓库。
## 2. 仓库结构
### 2.1 顶层目录
```text
Frostbite2D/
├─ Frostbite2D/ # 引擎源码与头文件
├─ Game/ # 示例程序与资源
├─ platform/ # XMake 平台配置
├─ docs/ # 项目文档
├─ xmake.lua # 根构建入口
└─ README.md # 项目简介
```
### 2.2 引擎目录分层
```text
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 常用命令
```bash
xmake build
xmake clean
xmake build -m debug
xmake build -m release
xmake build -p windows
xmake build -p linux
xmake build -p switch
```
查看当前配置:
```bash
xmake f -c
```
## 3.3 依赖概览
从平台脚本看,当前主要依赖包括:
- SDL2
- SDL2_image
- SDL2_mixer
- SDL2_ttf
- GLM
- zlib
- Squirrel
- Glad源码随仓库提供
Nintendo Switch 平台使用 devkitPro/libnx/portlibs 工具链与库。
## 3.4 构建产物与资源复制
`mingw``linux` 平台都会在构建后执行 `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()`
一个简化后的运行主线如下:
```text
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` 当前职责:
- 初始化渲染状态;
- 控制帧开始/结束;
- 设置清屏色和视口;
- 绑定相机;
- 提供 `drawQuad``drawSprite` 等基础绘制接口;
- 在合适时机 `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()` 会把事件交给当前场景:
```text
SDL_Event -> Event -> CurrentScene->OnEvent()
```
## 6.2 Actor 事件处理模型
`Actor` 提供两类事件扩展方式:
- 注册监听器:`AddEventListener(EventType, callback)`
- 继承覆写:`OnKeyDown``OnMouseDown``OnJoystickAxis` 等虚函数
事件是否会进入某个节点,取决于:
- 是否启用了 `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` 面向图片包资源,当前示例中通过:
```cpp
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`
- 提供文件内容、原始字节和字符串查询能力;
- 为脚本解析、动画数据读取等模块提供底层数据。
示例初始化方式:
```cpp
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 资源创建
典型流程:
```cpp
auto sprite = Sprite::createFromNpk("sprite/newtitle/nangua.img", 0);
if (sprite) {
sprite->SetPosition(220, 10);
scene->AddChild(sprite);
}
```
它继承自 `Actor`,因此天然具备层级、变换、透明度和事件能力。
## 8.2 TextSprite / FontManager
文字显示由 `TextSprite``FontManager` 协作完成,当前流程是:
1. `FontManager::init()`
2. `registerFont(name, path, size)`
3. `TextSprite::create(text, fontName)`
4. 设置颜色、位置等属性
5. 加入场景树
示例中已经验证:
- 字体从 `assets/Fonts/` 加载
- 中文文本可以进入显示链路
## 8.3 Animation / AnimationData
动画系统当前与 PVF 资源紧密耦合。示例中通过:
```cpp
auto ani = MakePtr<Animation>(
"monster/event/bluemarble/goblin/animation_goblin2/move.ani");
```
可以推断当前能力包括:
- 从 PVF 路径解析 `.ani`
- 关联 `.als` 或其他描述文件
- 生成可加入场景树的动画对象
这一块是项目里较有特色的内容链路,后续扩展新动画能力时,应优先梳理 `Animation``AnimationData``PvfArchive` 的数据流。
## 9. 脚本系统
脚本运行时由 `SquirrelVM` 单例管理,当前公开能力包括:
- 设置脚本目录
- 打开/关闭 VM
- 加载脚本文件
- 执行脚本
- 控制 debug 模式
示例中的接入顺序:
```cpp
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. 通过 `Music``Sound` 加载并播放
不过示例中的音频演示代码大部分仍是注释状态,因此目前更适合把它视为“已具备基础设施,正在逐步验证的模块”。
## 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
- 方法名大量使用大写开头的引擎风格命名,如 `SetPosition``GetScene`
- 成员变量普遍使用尾随下划线
- 单例模式被广泛使用
- 错误处理倾向于返回 `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. 一个最小接入模板
如果要快速新建一个最小实验入口,可以参考下面的初始化顺序:
```cpp
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 参考手册会更合适。