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







