6. 卡顿优化
相信很多研发者或玩家,都遇到这种情况:游戏大部时间运行都很流畅,但在战斗的某些时刻或者打开某些界面会卡一下,甚至卡很久。这个现象就是卡顿。引发卡顿的原因有很多,但主要有:
- 突发大量IO。
- 短时大量内存操作。
- 渲染物体突然暴涨。
- 触发GC。
- 加载资源量多的场景或界面。
- 触发过多过复杂的逻辑。
避免或者缓解卡顿的技法也是围绕以上原因展开。
6.1 降帧法
跟3.3的方法类似,通过强制降低更新频率,减缓卡顿的时间。
6.2 摊帧法
摊帧法就是本来需要在同一帧处理的逻辑分为若干份,分摊到若干帧去处理,从而缓解同一帧的处理时间,减缓卡顿现象。例如,本来在同一帧需要创建10个小兵,这个很可能会引发卡顿,那么可以每帧只创建2个,分摊到5帧创建完。适用此法的还有资源的加载,AI的更新,物理的更新,耗时逻辑的处理等等。此外,还可以用预处理(3.2),主次法(3.4)来避免卡顿。
6.3 限制数量法
如果降帧法,摊帧法,预处理,主次法都无法解决现象,卡顿原因又刚好是因为物体数量过多,那么限制数量就非常有必要了。做法就非常简单,当场景内创建某种物体(角色,特效,血条等)的数量到底最大值时,便强制不再创建。此法可能会引起逻辑的一些错误和不好的游戏体验,需谨慎使用和处理。
6.4 逻辑优化
如果卡顿是逻辑过于复杂引起的,就需要针对性地优化逻辑。每个项目的逻辑不一样,这里无法给出具体的优化措施。
6.5 IO优化
因IO慢引起主线程等待,从而导致游戏卡顿的现象非常普遍,下面有一些常用的优化技法。
6.5.1 预加载
将耗时的IO提前到某个时刻(游戏启动时,场景加载时,进入主界面时等)加载,比如有些角色资源大,可以在加载战斗场景时提前加载,以免战斗过程中卡顿。
6.5.2 异步加载
将IO异步化,以避免卡主线程。此技法应用非常普遍了,不再累述。
6.5.3 压缩资源
将本来零散的文件压缩成单个文件,或者对大文件利用一定算法(如哈夫曼编码)压缩,减少文件大小。这样也可以降低IO时间。当然,压缩资源也有副作用,需占用多一份内存,解压缩过程也要耗费额外的CPU。
6.5.4 多级缓存
我们都知道CPU的频率是最高的,目前家用PC的主频可达3.2GHz甚至更高,CPU内有L1L3缓存,它们速度略有差别;内存的存取速度远低于CPU,一般是23GHz,约是CPU的1/10。硬盘存取速度又远低于内存,普遍是0.1Gb/s,远低于内存读取速度。而网络更慢,目前即便是光纤,也不过0.02Gb/s。通常我们能操控的是内存/磁盘和网络的数据,所以只要关注它们的速度,它们的速度关系大致如下图。
所以,多级缓存策略应运而生。做法跟缓存法类似,只是多了层磁盘缓存,实现伪代码:
map<string, ObjectType> _memoryCache;
ObjectType CreateObject(string objectPath)
{
// 1. 先尝试从内存缓存中读取,有就直接返回。
if (_memoryCache.count(objectPath) > 0)
{
return _memoryCache[objectPath];
}
ObjectType obj = NULL;
// 2. 再尝试从磁盘加载。
if (FileExisted(objectPath))
{
obj = LoadObjectFromFile(objectPath);
_memoryCache[objectPath] = obj;
return obj;
}
// 3. 最后才从网络下载
DownloadObjectFromNet(objectPath);
obj = LoadObjectFromFile(objectPath);
_memoryCache[objectPath] = obj;
return obj;
}
6.5.5 控制Log
游戏的Log通常会隔一段时间存档,如果逻辑处理不好,很可能引发卡顿。比如,每帧输出大量调试log,会引发频繁存档。游戏Z在早期,也曾发生卡顿现象,后来经Profiler分析发现是Log存档引发的。所以,有必要对Log做出一些优化。常见的优化方法:
- 避免帧更新输出Log。防止Log数据迅速膨胀引起频繁存档或增加存档时间。
- 改进Log存档机制。可以适当改进Log存档机制,比如每隔多少时间存档一次,或者Log数据到达一定量级触发。
- 建立Log等级。可以将Log分为Info,Warning,Error几个级别,不重要的log不存档。
- 异步存档。将存档Log的逻辑防止单独的线程,防止卡主线程。
- 避免无用的log。这就要在逻辑层控制log输出,避免无效的log。
6.5.6 JSON代替XML
游戏数据存储一般有两种:二进制和文本格式。二进制格式数据量最小,但可读性和扩展性差,适合存储模型/纹理/字体/音频等数据。文本格式的特点跟二进制刚好相反,适合存储配置信息。最常见的文本格式有JSON和XML两种,其中JSON对比XML有诸多优点:
- 数据量少。表达同样的数据,JSON格式可以比XML少40%(见下)。
<?xml version="1.0" encoding="utf-8" ?>
<country>
<name>中国</name>
<province>
<name>福建</name>
<citys>
<city>福州</city>
<city>南平</city>
</citys>
</province>
<province>
<name>广东</name>
<citys>
<city>广州</city>
<city>深圳</city>
<city>梅州</city>
</citys>
</province>
</country>
{
name: "中国",
provinces: [
{ name: "福建", citys: { city: ["福州", "南平"]} },
{ name: "广东", citys: { city: ["广州", "深圳", "梅州"]} }
]
}
- 可读性更佳。上面两段分别是XML和JSON表达相同的数据,谁可读性更佳一目了然。
- 更快的解析。JSON因为数据量更小,IO也会更快,解析速度当然也更快。
每个游戏都有大量逻辑数据需要存档,比如角色信息,技能信息,场景信息,配置信息等等。这些数据如果适合用文本格式存储,首选JSON无疑。
6.6 使用进度条
如果上面那些章节都无法解决卡顿现象,可以尝试使用进度条。思路是将卡顿逻辑抽离出来,分成若干阶段(step),每完成一个step,给一帧时间刷新UI进度条。当然也可以用异步方式实现。伪代码:
HandleStep1();
RefreshProgressBar(1 / n);
WaitForNextFrame();
HandleStep2();
RefreshProgressBar(2 / n);
WaitForNextFrame();
...
HandleStepN();
RefreshProgressBar(1);
WaitForNextFrame();