« 重新启程 | 返回首页 | 一个游戏的点子 »

Ant 的资源内存管理

这两天着手做游戏 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 清理?我认为是这样的:

  1. 规模较小的游戏或 demo 事实上并不需要在运行时清理,程序退出时一并释放干净即可。
  2. 规模较大的游戏,通常在场景切换时做清理,且不应让清理粒度太小,那样会增加很多的开发难度。

清理问题的难点在哪?

如果一个对象引用了某个 Asset 数据,通常我们不能直接清理它(除非是贴图管理那样的特例,偷偷替换实际的数据)。引擎中很难找到所有对象和 Asset 数据的引用关系,因为维护这样一张表有额外的复杂度成本。

但是,所有 Asset 数据目前都必定是引擎 ECS 中的 entity 引用,固然可以通过遍历 entity 的特定 component 找到引用关系,但整个 ECS 的 world 被销毁或重启时是一个更好的时机。因为这时,所有 entity 都被销毁了。

在切换场景时,我们建议重新创建 world 中的所有 entity ,这样,资源管理模块就可以把所有的 Asset 全部清理干净了。当然,实际实现时,我们不必真的释放所有的数据。做一个 cache 更好,如果同样的 asset 不久之后又加重新加载,就可以直接利用 cache 中的数据了。

顺着这个思路,我打算重构 Ant 中的 Asset 管理模块。先从 mesh 的管理改起,如果没有问题,就可以推广到所有的 Asset 。

  1. mesh 的 handle 只创建, 不单独销毁。不再区分从文件里读进来的 mesh 和程序创建的数据。
  2. 提供一个方法把内存中所有的 mesh 全部销毁。调用者会保证它不被引用。我们放在 world 的重建过程中,就能保证这点。
  3. 用一个 ltask 服务管理所有的 mesh , 它会做一些 cache 工作。 当 2 发生时不会立刻销毁数据,而根据 cache 算法来决定何时清理。

关于程序创建的 mesh ,我想新增一组 api ,在创建 mesh 时,同时用字符串命名,这样就可以和文件加载的 mesh 统一管理。如果程序创建 mesh 时有参数,将参数也编码到这个字符串中,保证字符串和数据有唯一对应。

Cache 我想使用一个简单的算法:

  1. 将数据分为 new 和 old 两个区,new 表示当前必须持有的数据,old 表示还在内存但可以被清理的数据。
  2. 在刷新时,将 cache 容量设置为 old/2 + new * 2 。然后把当前内存中所有数据置为 old 。
  3. 加载数据时:如数据已在内存中,把数据标记为 new ;否则,加载数据,并检查 new 上限,若超出上限,删除任意一个 old 数据。

Post a comment

非这个主题相关的留言请到:留言本