Skip to content

HTML5游戏的资源内存管理 #133

@smallnewer

Description

@smallnewer

[吐槽,github要是有一个草稿功能就好了]
游戏的资源管理是个大话题,这里主要记录关于资源的内存方面的管理。

设备的资源(内存/存储空间/CPU等)总是有限的,而为了游戏效果更好,大家会倾向尽量不压缩图片(更占空间),使用更多的帧动画来提高游戏表现。现代游戏中,有许多常见的能够降低内存占用的技术,如Sprite9、骨骼动画、compress_texture……它们有的可以借鉴到HTML5游戏中,有些却很难。
HTML5游戏最大的特点之一就是资源走网络而非磁盘(和页游类似,但Flash渲染引擎并非用脚本语言写成,它能够精确的控制内存),传统的端游手游更多的是资源包加载到本地,游戏开始后,就从本地资源加载。(它们的瓶颈在于磁盘读性能,即使用了SSD,像魔兽世界这种需要读取大量资源的游戏,依旧会对文件读取方面做大量的优化)。
资源走网络是非常昂贵的(但带来的是即点即玩),和磁盘比起来,任何一个有经验的程序员都不敢不做cache。所以我们可以看到市面上主流的H5游戏引擎,都对资源做了cache。做cache没有问题,是该做的,但是并没有提供对应的资源内存管理方案,这个难题被抛给了开发者。
试想,你加载了一些资源,可能你自己都不知道到底加载了多少,这些资源最后都堆积在内存里,还不停的占着GC-CPU消耗,直到最后宿主内存不够而崩溃。相信有无数的H5团队遇到过这个问题。
我们团队至今仍然需要不停的和内存占用做斗争,因为你能提供多少,团队总会用掉更多。
那么我们把目前常见的资源管理方案移植到HTML5游戏引擎中是否可行呢?有些游戏是适合的,比如过关类,比如刀塔传奇类这种场景界定非常明确的地方。但是目前主流的挂机RPG,场景界定非常模糊,这个地方用的地图,出来这个场景可能还会用到;这个界面里的帧动画,到其他界面里可能还会有用,甚至战斗里都在用。那么基于场景显然适用度有限。
那接下来你可能会想,我们可能需要寻求的是一种分组策略,不同的组,需要不同的处理策略;那么何种分组策略才是能够降低心智负担、同时行之有效的呢?
个人觉得资源的内存管理,本质上就是生命周期的管理。我们能不能不用分组,毕竟不同的游戏,对分组的需求不同,就像编程语言支持的特性越多,其学习者就要为自己用不到的特性支付更多的学习成本。
这几天放假,我在想,能否用LRU来管理生命周期呢?我们能用的内存有限,那么我们就在这些内存里,放置使用量最频繁的资源,当内存不够用的时候,就淘汰那些不常用的,如果需要用到,那么再加载就好了(引擎层面上,所有的资源都是懒加载,不要留预加载的概念,这样保证使用者的简单)。这么一想思路就清晰多了,是的,有多少内存就用多少,实在不够用,就重新加载,反正不能让宿主环境崩溃。
在此基础上,我们再引入分组的概念,有些资源是贯穿整个游戏声明周期的,或压根不可以释放的(如战斗音效,共用UI资源),有些本身就是懒加载,释放了重新加载就好,只要程序能正常继续运转即可。我们在LRU的基础上支持一下永久资源、手动释放两个功能即可。而永久资源是必须在游戏运行时设置的,因为有些战斗资源,在动态切换,我们希望无论主角变成什么样,它的资源永远都处于内存中;或者如MMORPG,无论如何视野范围内的资源是不可以释放的,而视野随时可能会移动。
更靠谱的一点是,可以让不同分组的资源使用不同的内存上限,只要保证这些上限不会超出设置的总上限即可。这样做的好处是,比如战斗场景,总是希望多占用一些,以提高核心表现。而有些UI的动画,则是可以掠过去的。
在这套机制的基础上,加上一些运行时内存统计;再加上预分析内存占用,提前知道整个项目的不同分组资源的占用情况。辅助团队估算内存占用。事实上也只能辅助,哪怕是同类游戏,哪怕都是RPG,不同的玩法设计、同屏的人数,都导致无法估算,只能团队手动设计场景,或伪造一个类似的场景,查看内存实际占用(省去的只是开启xcode,估算内存这个繁琐的步骤)

当然,这套方案还是有缺点,其本质还是依赖开发者手动配置,哪些资源重要。而我期望的是,这些配置来的越少越好(完全消灭是不可能的)。引起这个问题,主要还是LRU不够贴合游戏,是按什么作为依靠的?按照最近渲染次数。但试想,一个模块隐藏且最近都不会打开了,那它引用的资源显然是很难被释放的(需要时间,因为最近渲染次数很高)。而一个模块刚打开,它的资源却因为最近渲染次数较低而被清除掉。那么我们按照最后渲染时间呢?越靠后的优先级越高,目前想着是没什么问题。但是它会和一个优化策略有冲突(若一个区域被遮挡,那渲染优化可能会把这个区域跳过,这样我们的资源最后渲染时间就会停滞不前),不过理论上讲,释放资源是按照先用先释放的顺序,到这种情况还会被释放,证明内存峰值已经无法满足这个场景了。
如果我们采用最后渲染时间来作为LRU的对照基础,那就不需要开发者做太多的手动配置。不过我们还需要加一个关联素材这样的特性,有时为了优化,我们会把同一个Entity的素材打散成多份加载,假设该Entity一直处于一个动作(如站立),那其他动作很有可能释放掉,等到使用时,会变成空白,就显得很奇怪。可能还有其他类似的场景,我们需要把这些素材关联起来,让系统把他们看做一个整体。
上面说的虽然是渲染,但如音效也可适用同方案。

以上算是思路记录。依赖JS完成是难度很大的(不是不可能哦),但借助wasm使用C++/Rust是更靠谱的。如果我要开始做,我会选择Rust^_^。可以说上面思路的最早起点,就是Rust的生命周期概念。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions