这两天着手做游戏 demo 时发现 Ant 的 Asset 管理模块之前还留有一些工作没有完成。
那就是,当游戏程序加载 Asset 后,资源管理模块何时释放它们的问题。在 ant.asset 模块中,我们为每种 asset (以文件后缀名区分)定义了 loader unloader reloader 三个接口,分别处理加载、卸载、重载的工作。
但在实际实现时,几乎都没有实现 unloader 。当时是偷懒,因为我们之前的游戏即使把全部资源都加载到内存,也没多少数据,并不需要动态卸载释放内存。而即使实现了 unloader ,管理器也没有实现很好的策略去调用它。只能靠用户主动调用卸载 api 。事实上,一个个资源文件主动卸载也不实用。
考虑到占用内存最大的 asset 是贴图,我们又对贴图做了一些特殊处理:
所有的贴图都可以用一张空白贴图作为替代。引擎有权在任何时候(通常是内存不足时)主动释放长期未使用的贴图,并换用替代。这个特性也可以很好的适配异步加载过程。
所以,未释放的贴图并不会撑满内存。
今天在查看引擎的预制形状相关的 API 实现时想起,我们对一些预制的模型,又有一些特殊处理。
预制模型,例如平板、箭头、方块等,通常在调试或写一些简单 demo 时使用。它们不是从 asset 文件中加载而来的,而是通过一些代码直接填写顶点数据创建出来。所以,这样的网格数据(mesh)并不在 entity 间共享,而是每个 entity 独有一份。目前其生命期跟随 entity ,即在 entity 销毁时主动销毁 mesh 相关数据。
所以,我们就在相关数据结构上打了个标记。拥有这个标记的数据,会在 entity 销毁时做销毁处理,以免造成资源泄露。
我觉得这个设计有不好的味道,所以这次想把资源管理模块重新做一下,统一文件加载的 asset 和程序化生成的数据的管理。
回到前面的问题:到底应该什么时候清理内存中的 Asset 数据?提供怎样的 API 清理?我认为是这样的:
清理问题的难点在哪?
如果一个对象引用了某个 Asset 数据,通常我们不能直接清理它(除非是贴图管理那样的特例,偷偷替换实际的数据)。引擎中很难找到所有对象和 Asset 数据的引用关系,因为维护这样一张表有额外的复杂度成本。
但是,所有 Asset 数据目前都必定是引擎 ECS 中的 entity 引用,固然可以通过遍历 entity 的特定 component 找到引用关系,但整个 ECS 的 world 被销毁或重启时是一个更好的时机。因为这时,所有 entity 都被销毁了。
在切换场景时,我们建议重新创建 world 中的所有 entity ,这样,资源管理模块就可以把所有的 Asset 全部清理干净了。当然,实际实现时,我们不必真的释放所有的数据。做一个 cache 更好,如果同样的 asset 不久之后又加重新加载,就可以直接利用 cache 中的数据了。
顺着这个思路,我打算重构 Ant 中的 Asset 管理模块。先从 mesh 的管理改起,如果没有问题,就可以推广到所有的 Asset 。
关于程序创建的 mesh ,我想新增一组 api ,在创建 mesh 时,同时用字符串命名,这样就可以和文件加载的 mesh 统一管理。如果程序创建 mesh 时有参数,将参数也编码到这个字符串中,保证字符串和数据有唯一对应。
Cache 我想使用一个简单的算法: