OMNIDREAM GAMES 是一家核心团队在卡牌品类有着超过8年积累的开发商,旗下以《新斗罗大陆》为代表的系列产品累积流水破40亿,用户总数4000万+。
在多年开发过程中,OMNIDREAM GAMES 团队积累了一套游戏优化方案。今天我将基于团队一款全新的卡牌游戏「omniheros」,和大家分享几个行之有效的卡牌游戏优化方法,以及自己的一些心得。本文为将重点介绍以下6个优化方法:
红点设计:减少判断逻辑重复计算,支持多个显示类型和周期刷新。
音频加速:支持不同音频自定义变速。
ASTC 分块压缩:减少内存、提升加载速度、更精确地选择压缩块大小。
性能分级:让高、低端机性能均有提升。
动态绘制:减少 overdraw、降低 drawcall、提高帧率。
小包策略:减少首包包体、提升流畅度。
1、红点设计
解决问题
减少判断逻辑重复计算。
红点之间父子关系清晰可见。
支持多种显示类型。
支持周期刷新。
红点树的构想
每个 UI 需要显示的红点都对应有一个 id。
用树结构建立红点与红点之间的层级关系。
使用数据结构,保存红点的激活状态。
红点树的设计
只有叶子节点才有且必须有自己判断逻辑。
叶子节点的判断方法不需要参数或者需要固定的参数。
非叶子节点的红点显示状态取决于他的子节点的显示状态。
业务层只需要关心每个子节点的判断逻辑,何时刷新,刷新时是否需要传参数即可。
红点激活流程
104,105,106,107,108为叶子节点,分别绑定了自己的 check 方法。
104的 check 方法里有一个固定传参1,这个参数在每次判断的时候都会传入。
当105在 func(2) 的判断下从未激活变成了激活,会将状态传给101,101有一个激活状态的子节点,不需要管其他子节点,自己也会变成激活, 并将激活状态传给100,而使得100也变成了激活。
当107在 func() 的判断下变成了未激活,会将状态传给103,103只是根据107的未激活并不能直接决定自己的激活状态,它需要检查其他子节点的状态,所以需要检查108的激活状态,若108是激活的,则自己激活;若108未激活,则自己未激活,并将状态传给100。
如果107检查前和检查后的状态有变化则会将状态传给父节点103,如果没有变化则不用向父节点传递。
参数混合树状结构
叶子节点所需要的参数不固定,即需要动态参数。
非叶节点的子节点需要相同的动态参数。
非叶节点的子节点需要不同的动态参数。
红点激活流程
104,105需要相同的动态参数 e1,即101的子节点需要相同的动态参数。
107需要动态参数 g1,108需要动态参数 g2,即103的子节点需要不同的动态参数。
在动态参数的树结构里,对于叶节点107或者105,没法确定其他兄弟叶节点的参数是否与自己一致,所以这里需要遵循一个原则,只有激活状态可以传给父节点,动态参数不能传。
对于103,子节点需要不同动态参数,所以对于107和108的刷新只能选择各自分别刷新,然后同时将状态传给103。
对于101,子节点需要相同动态参数,所以对于104和105的刷新可以选择各自分别刷新,也可以选择刷新101并且将动态参数传给子节点104和105。
列表结构红点
因为列表每个元素不固定,元素数量不固定,所以没法绑定红点 id,即列表的红点处理目前选择自己判断,不走树结构。
红点配置表
id 为红点对应 id,有些 id 可能为虚拟 id,在 UI 里没有对应界面。
name 为注释。
parent 红点的父节点,只有大的根节点没有父节点。
update_type 更新类型:
默认为0前端判断,通过绑定的检查方法判断;
可以设置为1后端判断,因为在登录游戏到主城过程里,部分功能是不需要拉取对应的消息的,那这部分的红点激活状态没法通过数据的逻辑判断,只能靠服务器判断之后将结果返回,等到进入对应功能拉取到该功能信息以后,才接管为绑定的检查方法判断。
refresh_type 刷新类型:
默认0及时刷新,无需标记;
1为登录刷新,每次登录只红一次,标记过后这次登录就不会再红了;
2为每天刷新,每天只红一次,标记过后这天就不会再红了;
3为每周刷新,每周只红一次,标记过后这周就不会再红了。
function_id 为功能 id,用于红点功能开启判断,减少无效的判断耗时。
priority 优先级,在 update_type 为1时,可以设置优先级让比较简单的判断逻辑先判断,减少后端判断耗时。
show_type 为显示类型,配合显示优先级使用,优先级越高先显示,可自由设置每个显示类型对应的显示优先级:
默认0,红色的点;
1为绿色箭头,只显示绿色箭头;
2为满,只显示满;
3为新,只显示新;
-1为任意类型,取决于判断逻辑或子节点传给的显示优先级最高的类型。
红点业务开发工作
配置红点 id 关系表。
在 UI 上挂载红点显示组件。
绑定红点 id 的检查方法。
在需要变化的时候刷新红点。
标记红点是否点击过。
2、音频变速
解决问题
音频可以设置变速。
不同音效可自定义不同的变速。
音频变速几种方式
修改底层支持音频变速。
接入 wwise 引擎。
接入 fmod 引擎。
修改底层支持音频变速
修改 AudioMixerController.cpp,支持音频缩放。
优点:
开发速度快,只需要修改底层代码。
实现成本低,没有额外花销。
缺点:
只有一个通道,所有音频只能同时变速。
接入 wwise 引擎
wwise 是一款针对游戏开发的综合音频中间件解决方案,可创建精密、丰富的互动音频。它由紧密集成的设计工具和声音引擎构成,可让声音设计师和程序员在更短的时间内更经济地完成令人震憾的音频。
wwise 官网:
https://www.audiokinetic.com/zh/products/wwise/
优点:
音频效果相对较好。
音频共享,音频资源大幅缩减。
对程序比较友好,发事件即可。
缺点:
需要学习 wwise 软件。
价格相对较贵,成本高。
web 端没法支持,没法直接播放 MP3,开发不方便。
预编译库特别大。
接入 fmod 引擎
fmod 引擎是一种跨平台的音频引擎,可以在 windows,安卓,iOS,web 等开发平台使用。可设置多个通道,对单个通道进行变速,例如游戏只希望对技能音效进行变速。
fmod 官网:
https://www.fmod.com/
优点:
支持不同通道变速。
可以直接播放 MP3。
较为便宜。
缺点:
文档全英文。
3、ASTC 分块压缩方案
解决问题
减少图片的内存。
提升图片加载速度。
更为精确选择压缩块大小。
什么是 ASTC?
同 pvrtc、etc1 etc2,一种图片压缩纹理格式。
优点:
压缩倍率更高,压缩率可自由选择。
显示效果更好。
兼容 iOS 和安卓。
支持情况:
需要 OpenGL ES3.0 以上才支持,根据调查,我们发现海外有99.2%的玩家手机是支持,不支持率仅占0.8%,而且这种情况在国内是更为乐观的。针对这部分玩家,我们可以选择抛弃,也可以选择做软解支持一下,只是这软解会比较慢。
Cocos Creator 接入 ASTC:
Creator 3.0 以后支持 ASTC。
Creator 2.4 需要修改 C++ 底层逻辑来加载 ASTC,这部分网上是有现成例子的。
ASTC 压缩分块
我们了解过 ASTC 后,发现这种纹理其实是有很多分块压缩方式。如图,分块越大,压缩倍率越高,当然图片的质量会越低,那么我们应该如何选择分块呢?
对此很多团队其实提出一个比较好的方式就是对文件夹配置白名单,如下图:
这样确实能找到比较好的压缩分块,但是如果下面两张图片在同一个文件夹:
左边的图片细节比较多,颜色比较丰富,选择 6x6 的方块才能有比较好的显示效果,而右边的图片色彩较为单一,只需要选择 10x10 甚至 10x12 都可以,如果我们按照文件夹的方式,对右图进行 6x6,其实会是压缩倍率不够造成浪费的,所以对文件夹进行白名单方式是比较难对单个图片找到最合适的分块方案!对此我们提出了一个较为准确的一个方案——相似度匹配的方案。
相似度匹配的方案
对每一张图片把所有的分块都提前转好。
按块的尺寸从大到小和原图进行相似度匹配。
拟定一个相似度值,例如:80%为通过。
部分图片也支持白名单方式:
这样主要是比较原图与压缩图的相似度,找到我们认为比较合适的压缩比。那么如何去实现这相似度匹配呢,这里我们提供了一个相似度匹配算法仅供参考。
相似度算法
按照所选的块大小把图片分成很多块。
每一块的每个像素 rgb 值的差值,乘以该像素的透明度,求出差值 rgb 的亮度。
把单个块所有像素的差值亮度求一个均值。
比较所有块的亮度差值,得到一个最大的亮度差值。
转换成一个0到100的百分比,作为可配置参数。
如图,A 的分块颜色信息较多,B 的颜色较为单一,压缩后A的差异性是所有方块最大的,我们就会选择 A 作为整个图片的差异性,目的是为了显示效果更加准确,优先保美术品质。
相似度算法优缺点
优点:
对图片选块较为准确,对每张图片都单独选块。
游戏性能与表现效果,优点考虑了表现效果。
缺点:
需要在电脑里缓存所有的块图片。
比较慢(git 钩子)。
4、性能分级方案
解决问题
高端机有更好的效果。
低端机也能有比较好的体验。
分级流程
统计2021海外 top150,做出分级(iOS 直接白名单)。
获取手机设备信息(手机型号、CPU 最大频率、GPU 频率、最大内存、剩余外存、核心数)。
设定各个参数对应的分数系数,计算总分:
score = cpuScore*Pcpu*Rcpu + gpuScore*Pgpu + ramScore*Pram + memScore*Pmem
根据总分做出高中低分级,进行不同的操作:
LOW:score < Smiddle
MIDDLE:Smiddle <= score < Shigh
HIGH:score >= Shigh
分级处理
预加载数量设置
设置界面是否常驻
对象缓存数量
调节游戏帧率
资源回收频率
特效播放分级
……
性能测试标准分级
性能分级后,不同机型的性能标准不一样
这种分级解决了大部分表现效果对手机硬件的匹配,但这个过程中我们观察到部分手机可能 CPU 计算能力不怎么好,但是内存比较高,所以我们做了更为细化的分级方案。
细化分级
主要为了充分利用手机硬件的各项优势:
CPU 分级:帧率、回收频率、特效播放分级……
内存分级:预加载、界面常驻、对象缓存数量……
5、动态绘制
解决问题
减少 overdraw。
降低 drawcall。
提高帧率。
Cocos 渲染痛点
画 UI 的方式,没有非透明的剔除机制,针对这一点,一般的优化手段就是减少绘画次数,画一次就保存起来,对此很多游戏都用一种比较实用的优化方式。
动态绘制弹框
在弹框显示时,设置截图模糊垫在弹框后面。
然后将模糊图后面的场景都隐藏掉。利用这个思路,在此基础上可以做一些其他优化。
动态相机优化
如下图,对 camera 新建一个分组 postRender。
把需要优化的节点,如比较复杂的列表,分配到 postRender 这个分组,这样主 camera 就不会渲染列表了。
postRender 对列表绘制一次,保存贴图 postImage,把 postImage 渲染给主 camera。
在界面静止的状态下,关闭 postRender 绘制,有变动再打开。
这样的目的是利用另一个相机对这个复杂的 UI 只渲染一次,让主相机对这个复杂 UI 的渲染转变成对一张贴图的渲染,这样的方式结果就是:
多了一张 postImage 内存。
列表部分就只有一个 drawcall。
效率提升了很多,如下图,drawcall 减少了很多,fps 也提升比较大。
优化前
优化后
动态大地图优化
下图是一个大地图 UI,手机显示的部分只有中间蓝色框那么大,但是地图却超出了很多,且有多个分层和特效,而且会多复制一份作为循环移动的衔接,所以加载这么一个大地图 UI 需要的时间不少,比较差的机型打开这个界面必然会出现黑屏时间,为了解决这个黑屏时间,我们也做了一个比较好的优化。
优化步骤
在游戏根节点上新建一个常驻缓存节点 preloadNode。
第一次打开地图加载完成时,渲染截图一张第一屏的场景。
将截图绑定到一个新建的 node,缓存在 preload 节点上。
后续打开的时候,先将缓存的 node 先放在地图节点显示。
等大地图加载完成,再把新的渲染场景重新截图绑定到缓存节点,放回 preload 节点。
优化结果
这样利用这个缓存的截图可以有效的避免了加载黑屏时间,除了大地图,也可适用于其他加载黑屏的场景,如果觉得这个缓存截图可能会造成较大的内存开销,可以利用贴图尺寸减半的方式减少内存开销
6、小包方案
解决问题
减小首包包体(200M 以下)。
资源可以边玩边下载。
玩家玩的过程尽可能流畅。
乐变小包流程
跑一遍游戏前期,得到这个过程所用到的小包资源。
重新打成小包,剩余资源作为小包的补包打成 zip 分包,整包 = 小包 + 分包。
上传一份包含所有散文件资源的文件包在服务器。
小包安装启动后,后台会下载分包,每下载完成一个就解压。
游戏过程中如果用到手机里没有的资源,则立马从有所有散文件资源的服务器下载。
存在问题
这个流程整体还是比较完善的,但是会有一个问题,下载分包需要时间,如果在玩游戏过程中,用到的分包里的资源,就需要从散文集列表的下载,等待下载完成才能继续进行,造成游戏的卡顿,为了优化这个问题,结合我们游戏是散文件更新方式的形式,我们在此小包基础上做了优化。
天神小包流程
打包时,我们记下这个包所有的资源为 List_all。
跑游戏前期,标记所有用到的资源,记录资源列表 List_small,并且这个 List_small 是有序的。
按照小包大小要求打包,把 List_small 里按顺序打进包里,直到达到小包限定大小,没有打进包里的列表重新生成一个 List_pre 作为优先下载队列。
进入游戏后首先执行正常的强制更新(对比的是 List_all 和当前线上版本资源的差异)。
游戏过程中优先下载 List_pre 里的资源,如果有用到的资源而本地没有也会立即去下载。
其他小包缺少的资源在后台下载(List_small 和当前线上版本资源缺少的部分)。
后台需要限制下载线程数量,保证玩家正常游戏。
提升点
小包大小可以按照要求自由控制。
List_pre 可以按照顺序优先下载,尽量保证用到的资源能提前下载好。
可控制的下载线程保证玩家体验。
个人心得
其实,各种优化方向都是在原有基础上提出更高的要求,需要我们去大胆尝试解决方法并充分验证,最后再合理的范围内做出取舍,从而实现更好的游戏优化效果。
另外,在游戏开发中,性能方面其实有很多方面都可以有优化空间。本次选取的是我们团队积累的比较典型的几个方案,如果小伙伴有兴趣,可以在评论区一起交流。
往期精彩