开篇

  资源保存大概是引擎里最不显眼但最折磨人的部分。渲染有画面,物理有碰撞,脚本能写玩法,资源系统做对了往往没人注意,做错了就是“我昨天还能打开的场景今天怎么全丢引用了”。

  最早项目小的时候确实可以偷懒:图片按路径加载,脚本记文件名,场景里直接写资源路径。只要文件不改名不移动,问题不大。但这显然不是编辑器项目的常态。资源会整理目录,Prefab 会复制,材质会换 Shader,Demo 做着做着文件夹就会开始膨胀。

  所以最后还是得做 AssetDatabase、.meta、GUID、Scene 序列化和 Prefab。听起来有点像 Unity 那套,实际上小引擎做到这个阶段也很难绕开这些东西。

路径不是身份

  路径引用最大的问题是路径太脆。比如一张图原来叫:

1
Assets/Sprites/Bird.png

  你把它挪到 Assets/Sprites/Characters/Bird.png,它本质上还是那张图,但所有保存旧路径的地方都会失效。场景、Prefab、材质全都可能跟着挂。

  所以资源需要一个和路径分离的身份,也就是 GUID。Ditto 会给资源生成旁边的 .meta 文件:

1
2
Assets/Sprites/Bird.png
Assets/Sprites/Bird.png.meta

  资源移动时 .meta 跟着移动,引用方保存 GUID。AssetDatabase 扫描项目后建立 GUID 到路径的映射。这样路径变了,GUID 没变,引用就还有机会恢复。

AssetDatabase

  AssetDatabase 不是资源加载器本身,更像项目资产的索引。它不负责把 png 解成纹理,也不负责把 shader 编译成 pipeline,它主要负责回答几个问题:

1
2
3
4
5
这个资源有没有 .meta?
这个 GUID 对应哪个路径?
有没有重复 GUID?
有没有能被校验出来的缺失 GUID 引用?
资源移动或重命名后引用还能不能找回来?

  这类东西平时看着很基础,但编辑器一旦没有它,Project 窗口就不敢让用户随便移动文件。因为移动一次文件,就可能把场景引用弄断。做到 AssetDatabase 后,编辑器才算稍微有了“项目管理”的底气。

Import Cache

  另一个需要区分的是源资源和导入产物。用户维护的是 Assets 里的源文件,但运行时不一定直接用源文件。模型、图片、Shader、字体、音频都可能需要转换或生成中间数据。

  Ditto 里用了 .ditto 相关目录保存 import cache 和 artifacts。大概可以理解成:

1
2
Assets  -> 用户真正关心的源资源
.ditto -> 引擎生成的缓存和导入结果

  这个边界很重要。归档时真正该保留的是 Assets 和 .meta,缓存坏了可以重建;反过来如果 GUID 或源资源丢了,缓存再完整也没什么意义。

场景保存

  Scene 保存的是当前世界的 GameObject 树。每个 GameObject 有自己的名字、激活状态、子物体和组件列表,组件再保存各自的数据。

1
2
3
4
5
Scene
-> Root GameObject
-> Transform
-> Renderer / Camera / Script / UI / Physics / Audio
-> Children

  这里比较关键的是:场景保存状态,不保存资源本体。SpriteRenderer 用了一张图片,场景里应该保存图片引用,而不是把图片内容塞进场景文件。Renderer 用了 Material,也应该保存 Material 的引用,而不是展开整个材质依赖树。

  这点一开始很容易写歪。因为直接把当前对象所有东西一股脑序列化最省事,但后面资源复用、替换和构建都会很难处理。

Prefab

  Prefab 是另一个早晚会需要的东西。场景里如果有一堆重复对象,每个都手动复制 GameObject,短期可以,稍微改一次结构就要全场景同步,非常折磨。

  Ditto 的 PrefabAsset 主要支持三个操作:

1
2
3
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 相关窗口,用来检查:

1
2
3
4
缺失 .meta
损坏 .meta
重复 GUID
缺失 GUID 引用(目前偏接口和入口)

  这里也要稍微说清楚:缺失引用目前不是完整的全项目自动扫描,更像是 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
2
3
4
I   引擎基础:GameObject、Component、Editor、早期渲染和物理
II 脚本支持:Mono、C# API、编译、热重载、native binding
III 渲染改进:RHI、多后端、Shader、Material、RenderTarget
IV 资源保存:AssetDatabase、GUID、Scene、Prefab、构建资源

  当然里面还有很多模块没展开,比如 Audio、UI、Animator、ParticleSystem、2D Physics 等。但如果只保留精简篇幅,个人觉得这四条线最能说明一个小引擎是怎么从“能跑”变成“能做项目”的。