From 8cb688cf27981d420d25d8dba28af0ee554db2cf Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Wed, 1 Apr 2026 05:12:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0Frostbite2D=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加详细的Frostbite2D引擎开发文档,包含仓库结构、构建运行说明、核心架构、资源系统、事件系统等内容,为维护者和协作开发者提供快速入门指南 --- docs/开发文档.md | 648 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 648 insertions(+) create mode 100644 docs/开发文档.md diff --git a/docs/开发文档.md b/docs/开发文档.md new file mode 100644 index 0000000..bb23548 --- /dev/null +++ b/docs/开发文档.md @@ -0,0 +1,648 @@ +# 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/.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()` +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 + -> 可选执行 SquirrelVM::run + -> 主循环 + -> 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()` 会在主循环前检查 `SquirrelVM` 是否已初始化,如果已初始化则先执行 `SquirrelVM::run()`。 +- `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( + "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()` 中,如果 VM 已初始化,会先执行 `SquirrelVM::run()`。 + +这意味着脚本系统目前是“可选启动模块”,而不是 `Application::init()` 的强制步骤。 + +## 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(); +SceneManager::get().PushScene(scene); + +app.run(); +app.shutdown(); +``` + +如果要继续接入资源、文字或动画,再按需补: + +- `SquirrelVM::init()` +- `PvfArchive::open()` + `init()` +- `NpkArchive::setImagePackDirectory()` + `init()` +- `FontManager::init()` + `registerFont()` + +## 16. 后续文档扩展建议 + +这份文档适合作为总览文档,后续可以继续拆出专题文档: + +- 渲染系统设计说明 +- NPK/PVF 资源格式接入说明 +- 动画系统数据流说明 +- Squirrel 脚本桥接说明 +- 平台移植说明 + +如果未来要对外开放引擎 API,再补一份按模块组织的 API 参考手册会更合适。