资源文件系统的设计

云风 2017-12-20 11:47

上次说到,我们的引擎打算在 PC 上开发,设备上直接调试。如果是按传统的开发方式:运行前将 app 打包上载然后再运行,肯定是无法满足开发需要的。所以必须建立一套资源的同步机制。

目前,我们已经实现了基本的资源文件系统,大致是这样工作的:

所有的资源文件,包括程序代码本身(基于 Lua),都是放在开发 PC 上的。开发环境会缓存每个文件的 md5 值,文件系统将用此 md5 值为标准,默认 md5 相同的文件,其内容也是一致的,暂不考虑 md5 冲突的问题。

在设备上,用设备的本地文件系统做一个 cache ,cache 中分为两个区间,一是资源文件区,按所有资源文件的 md5 值为文件名(按 md5 的 16 进制码的前三字节散列在不同子目录中,防止单个目录文件数量过多)保存。二是目录结构区,每个子目录用一个递增数字 id 做文件名,内容则是可读文件名以及其内容对应的 md5 值或子目录编号。其中 id 0 是资源根目录。

例如,我们有一个贴图文件叫 foobar.tex ,它的 md5 值是 xxxxxx ,放在 textures/foobar.tex 。那么在 0 号目录文件中,就有一项记录为 textures 1 ,表示有 textures 这么一个子目录,其编号为 1 。

在 1 这个子目录文件中,有 foobar.tex xxxxxx 的记录,表示该目录下有 foobar.tex 这个文件, md5 值为 xxxxxx 。

如果需要打开 textures/foobar.tex 这个文件,需要先读取 0 号根目录,找到 textures 子目录的编号 1 ,再加载 1 号目录,找到 foobar.tex 的 md5 值 xxxxxx ,根据 md5 值从资源文件区加载对应文件。

游戏运行时,底层的 IO 模块负责管理资源文件。每打开文件时,若 cache 中没有需要的文件,则向编辑器发送一个文件请求;若已经有对应文件,则向编辑器发送一个文件查询请求,查询该文件的 md5 值,当 对应 md5 的数据不存在时,重新请求该文件。

ps. 未来可以做一个优化,在目录文件中记录版本号,版本号在启动时同步,再根据版本号减少文件查询请求的数量。

IO 模块在运行时是不负责删除任何文件的,即使主动删文件,也仅仅是在目录文件中把对应条目删除,而不真正删除资源文件区的数据文件。这样做的好处是,如果你需要经常在多个版本间切换:例如把你的开发机上连接的设备拔下去,插在另一台开发机上使用,而两台开发机处于不同的开发分支上;不必频繁的更新资源文件。

cache 可以主动清空,或是从根目录递归遍历,删除没有引用的数据文件。这样可以避免占用过多的空间。

直接采用 md5 值来索引数据文件,还有一个额外的好处。用户可以按更自然的方式组织资源结构。比如,不必因为两个模型需要共享同一张贴图,就把贴图文件放在独立的公共目录中。直接把贴图放在所属的模型目录下即可。只要贴图内容是一致的,最终在设备上就只有一份文件,资源管理模块也绝对不会在内存中重复加载。

补充一些实现细节:

当数据文件依赖远程加载时,如果通讯部分也是用 lua 实现,那么很可能会利用 lua 的 coroutine 做异步加载。而 lua 的 require 函数是在 C 中实现的,loader 无法 coroutine.yield 。

这里可以使用一个小技巧,自定义一个 require 从 lua 代码实现部分逻辑。在 lua 中判断 package.loaded 以及使用 package.searchpath 判断文件有效性。等异步加载完成后,再转入默认的 require 函数。

另外,IO 模块提供了 prefetch 指令,可以按照需要,发送文件请求,而不阻塞程序运行。这可以用于更上层的资源管理。例如,了解了资源的相互依赖关系的话,可以再加载完一个资源文件后,查询到它可能依赖的其它资源,一次性提交所有的依赖请求。特别是,如果这个资源是 lua 文件,我们可以在运行前,分析出里面是否有新的 require 请求,提前把所有 require 的相关文件请求都发送出去。

[返回] [原文链接]