制作自己的引擎(四)资源保存
开篇
资源保存大概是引擎里最不显眼但最折磨人的部分。渲染有画面,物理有碰撞,脚本能写玩法,资源系统做对了往往没人注意,做错了就是“我草,我场景呢。昨天还能打开的,今天怎么全丢引用了”。
最早项目起步的时候确实可以偷懒:图片按路径加载,脚本记文件名,场景里直接写资源路径。只要文件不改名不移动,问题不大。但这显然不是编辑器项目的常态。资源会整理目录,Prefab 会复制,材质会换 Shader,Demo 做着做着文件夹就会开始膨胀。
所以最后还是得做 AssetDatabase、.meta、GUID、Scene 序列化和 Prefab。听起来有点像 Unity 那套,实际上小引擎做到这个阶段也很难绕开这些东西。
资源身份
GUID 与 .meta
路径引用最大的问题是路径太脆。比如一张图原来在Assets/Sprites/Bird.png,将其挪到 Assets/Sprites/Characters/Bird.png,它本质上还是那张图,但所有保存旧路径的地方都会失效。场景、预制体、材质全都可能跟着挂。
所以资源需要一个和路径分离的身份,也就是 GUID。Ditto 会给资源生成旁边的 .meta 文件,资源移动时 .meta 跟着移动,引用方保存 GUID。AssetDatabase 扫描项目后建立 GUID 到路径的映射。这样路径变了,GUID 没变,引用就还有机会恢复。
AssetDatabase 索引
AssetDatabase 不是资源加载器本身,更像项目资产的索引。它不负责把 png 解成纹理,也不负责把 shader 编译成 pipeline,它主要负责文件映射和信息校验。
这类东西平时看着很基础,但编辑器一旦没有它,Project 窗口就不敢让用户随便移动文件。因为移动一次文件,就可能把场景引用弄断。做到 AssetDatabase 后,编辑器才算稍微有了“项目管理”的底气。
Import Cache
另一个需要区分的是源资源和导入产物。用户维护的是 Assets 里的源文件,但运行时不一定直接用源文件。模型、图片、Shader、字体、音频都可能需要转换或生成中间数据。Ditto 里用了 .ditto 相关目录保存 import cache 和 artifacts。大概可以理解成:
1 | Assets -> 用户真正关心的源资源 |
这个边界很重要。归档时真正该保留的是 Assets 和 .meta,缓存坏了可以重建;反过来如果 GUID 或源资源丢了,缓存再完整也没什么意义。
场景资产
Scene 序列化
Scene 保存的是当前世界的 GameObject 树。每个 GameObject 有自己的名字、激活状态、子物体和组件列表,组件再保存各自的数据。
1 | Scene |
这里比较关键的是:场景保存状态,不保存资源本体。SpriteRenderer 用了一张图片,场景里应该保存图片引用,而不是把图片内容塞进场景文件。Renderer 用了 Material,也应该保存 Material 的引用,而不是展开整个材质依赖树。这点一开始很容易写歪。因为直接把当前对象所有东西一股脑序列化最省事,但后面资源复用、替换和构建都会很难处理。
Prefab 实例
Prefab 是另一个早晚会需要的东西。场景里如果有一堆重复对象,每个都手动复制 GameObject,短期可以,稍微改一次结构就要全场景同步,非常折磨。Ditto 的 PrefabAsset 主要支持三个操作:
- Instantiate:从 prefab 创建实例
- Apply:把实例改动写回 prefab
- Revert:丢掉实例改动,恢复源 prefab
难点不在“把一个 GameObject 存成文件”,而在实例和源资产的关系。一个实例要知道自己来自哪个 prefab,Editor 也要能判断它和源 prefab 的差异。当前这块没有做到 Unity 那种很细的 override 系统,更多是基于整棵 GameObject 序列化结果做 diff,然后 Apply 整体写回、Revert 整体替换。粗糙一点,但对现在的 Demo 规模已经够用。
这个功能做出来后,对 Demo 制作帮助很大。像 AngryBirdsDemo 这种有重复物体、UI、特效和关卡元素的项目,如果全靠手动复制节点,后续维护会很快失控。
资源诊断与构建
Asset Health
资源系统还有个很现实的需求:能自检。因为 .meta 和 GUID 这套东西一旦出问题,表现通常不是“这里坏了”,而是“某个场景突然加载不出来”。所以 Editor 里加了 Asset Health 相关窗口,用来检查缺失与损坏的.meta和GUID。
这类工具没什么展示效果,但归档时我反而觉得挺重要。一个项目只要开始有资源身份,就必须有诊断工具,不然出了问题只能靠肉眼翻文件,基本等于给未来的自己上刑。
构建资源
Editor 里能跑不代表打包后能跑。Windows 构建时除了 native 可执行文件,还要带上脚本程序集、DittoEngine.dll、Mono runtime,以及场景引用到的 shader、material、texture、audio 等资源。
当前构建流程里,项目 Assets 会被递归复制,但 .cs、.cpp、.h、.obj 这类源文件和中间产物会跳过,.meta、.mat、.shader、贴图、音频这些则会跟着出去。除此之外还会单独补引擎自带 shader、模型、ShaderCache 和 Mono 相关依赖,避免 Editor 里靠搜索路径能找到的东西到了 Player 里突然丢失。
这也是资源系统不能只服务 Editor 的原因。Editor 里通过 AssetDatabase 能找到资源,Player 里也得按构建后的目录找到对应文件。否则编辑器越做越完整,最后一打包反而什么都缺。
小结

资源保存做完后,引擎才真正有了项目感。GameObject 能组织对象,C# 能写逻辑,RHI 能画画面,而 AssetDatabase、Scene 和 Prefab 则负责让这些东西在关闭编辑器后还能留下来。这四篇合起来差不多就是 Ditto 归档时最核心的结构。当然里面还有很多模块没展开,比如 Audio、UI、Animator、ParticleSystem、2D Physics 等。但如果只保留精简篇幅,个人觉得这四条线最能说明一个小引擎是怎么从“能跑”变成“能做项目”的。为了证明引擎能力,个人也在此基础上做了愤怒的小鸟demo。
但接下来应该会弃坑很长时间了,如你所见这个系列的四篇断断续续的持续了小半年,中途不断修改删除调整架构,个人也从秋招跨到春招再到现在的入职。当时做这个引擎的初心是Unity的各种收费作死,UE个人认为过重了,Godot目前也不大完善,所以想搓个引擎看能不能支持自己日后的独游,另外也可以探下自身引擎能力。能将引擎做到现在这步已经是我当前的极限了,而这功能上还有很多欠缺,离能用来做独游还差的很远。本科做独游时画工不能展现所想要表达的;硕士做引擎也止步于简易版定向复刻小游戏……还是,做不到吗?





