# 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(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( "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(); 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 参考手册会更合适。