资源模块的重构

我们的 Component 本质上就是树状的数据结构表达的一组数据。对 Lua 来说,就是一个带或不带层次的 table 。同样,我们也可以把一切外部数据都视为相同的树状表。
所以,一个外部资源文件的加载器,就可以写成:通过一个资源文件名初始化的状态机,产出一张 lua 表。lua 表中可以是 lua 支持的数据类型,如 number string 这些,也可以是引擎再加工的 handle ,如 texture, vb, ib, framebuffer 等。
运行时任何的数据都可以表示为一个普通的 lua 表,也可以是一个资源文件名+字符串路径引用的子树。通过 metatable 的机制,在使用上可以是一致的。但这个 metatable 的行为,可以根据数据的状态:在内存,不在内存,等进行不同的处理。
资源文件我倾向于把类型信息编码进去,也就是不采用额外的 schema 描述。这样,任何保存在文件中的数据,都可以用一致的表达方法。但是,根据不同的资源类型,可以实现不同的加载器。加载器更多的作用是进行数据到运行时的翻译工作:比如,把一块贴图数据变成贴图的 handle 。
因为引擎是围绕数据工作的。所以创建一个 Entity 就是从一个 prefab 文件实例化出来的。prefab 文件本质上就是 Entity 的初始化数据集。在引擎的使用层面,我们甚至不必提供用纯代码构造 Entity 的 API ,唯一的 API 就是从文件实例化,然后允许用户进一步修改。
这样,产生 prefab 文件的工具,就成了日常工作流的一部分。我们之前的开发流程并不是这样做的,我们之前尽可能的用人去写代码来做底层的开发,测试,脱离了工具。导致工具的开发从引擎开发中剥离了出去。正在做引擎的人不依赖正在做工具的人,虽然解开了依赖性,也造成了不吃自己的狗粮。毕竟,最终引擎的用户,面对的就是一个开发工具,而不仅仅是引擎的库。这次对工作流的调整,修改希望终结这种
现象,让工具变得更好用。
之前一直没有这样做的原因之一是我们的脚手架还没有搭建稳定。这里有一个先有鸡还是先有蛋的问题。脚手架没有完工,会让工具无法正确工作,如果库的开发过多依赖尚未稳定工作的工具,则会影响开发本身。这个结就无法解开。而经过了一年多的开发,是时候拆脚手架了。
回到资源模块这个话题。
我们对运行期的数据,如果引用的是资源文件的一部分,就将其实现为一个代理对象。这些代理对象按所属资源文件分类,分别管理。我为每个资源文件中的每个子树都生产一个代理。理论上,整个游戏所引用的这样的数据块的总量是固定的,这个代理对象本身不一定包含真正的数据,它的内存占用是有限的,所以我永远不清除这些代理对象。减少管理的复杂度。
当代理对象代理的数据在内存中时,它指向加载器加载出来的数据表。我们可以通过代理对象的元方法监测数据的使用情况。虽然监测本身有成本,但可以随时将元方法更换为对数据表的直接引用,提高性能。
注:lua metatable 的 index 如果指向一张普通 table 时,性能大大高于指向一个函数。
如果我想知道一张贴图最近是否有人使用,只需要把这张贴图的代理对象的监测方法,以一定周期性的抽查,比如在过去 5 分钟,抽查 100 次。如果没有使用,就可以认为它可以暂时清理出内存。
当代理对象代理的数据不在内存中时,可以把数据所属的资源文件下所有相关的代理对象共用同一个元表,这样,就可以用 O(1) 的代价修改所有对这个文件的引用的代理对象的行为。针对不同类型的资源,可以有不同的策略。
有些小文件,加载起来并不慢,我们可以采用直接阻塞式的惰性加载方案。一旦应用层访问到不在内存的数据,就阻塞住线程,把数据加载进来。
对于某些大文件,例如贴图,可以采用异步加载的方式。先用一张已经在内存的替代贴图顶上。并把加载操作提交到 IO 线程。待加载完毕后,再统一替换。
如果有一些特殊的大文件,即想做异步加载,又无法用替代物顶替,还提供的主动查询的 api 查询一个 lua table 背后的数据是否在内存。不过这需要上层业务了解更多细节,暂时只是在资源管理模块的 api 中预留了这个能力,并没有真的使用。
为了抹平普通运行数据和资源数据的区别,并允许运行时修改那些原本引用的是外部静态资源的对象,提供了一个叫 patch 的 api 。
我们规定,对 Component 的全部或部分的修改,必须通过 patch 进行,即 data.foo = patch(data.foo, { … } ) 这样的形式。第二个参数就是要修改的内容,用 lua 表的形式提供(允许有树层次)。为了减少使用者的失误(例如拼写错误),不允许通过 patch 增加或删除原有数据中不存在的项。
如果想更自由的修改数据,也可以用 data.foo = patch(data.foo, {}) 先打一个空的 patch ,这样会把 data.foo 的第一层转换为运行期的普通表,然后就可以用标准 lua 语法对这一层自由修改了(删除已有项,或增加新项目)。