本帖最后由 冰玥 于 2023-11-7 18:14 编辑
1. Why.为什么觉得卡?- 首先,感知上我肯定是觉得这个游戏很“卡”,才会萌生要搞清楚它 “为什么卡” 的问题,这个 “卡” 的感觉,可以用 游戏中里最常用的命令之一来描述:stat fps
- 其次,之所以需要花精力去“定位”,是由于造成卡顿的原因有多种。造成卡顿的因素大致分为三类,隐含在另一个最常用的命令之中:stat unit
- Frame: 即一帧所耗费的总时间,这个值越大,fps 就越小,二者相乘恒等于 1000。由于Game线程和Draw线程在完成一帧之前保持同步,帧时往往接近其中一个线程中显示的时间。
- Game: 处理游戏逻辑所耗费的时间.
- 这一步完全不考虑渲染问题,表现的是整个游戏世界在一帧之内,只在逻辑层面处理所有的变化需要花多长时间——Compute Game Context
- Draw: 准备好所有必要的渲染所需的信息,并把它从 CPU 发送给 GPU 所耗费的时间承接上一步,在游戏世界在逻辑层完成所有的计算和模拟后,收集渲染所需的信息,并剔除非必要信息,通知 GPU 进行画面渲染—— What to Render
- GPU: 接收到渲染所需信息之后,将像素最终的表现画在屏幕上的耗时
在游戏中按下 ~ 键,输入stat unit
2. What.瓶颈定位- 瓶颈定位,就是要找到造成性能开销的最大元凶,也就是确定优化的基本方向,才能深入和落实到细节层面,进行后续的分析和优化工作。而要搞清楚开销主要发生在哪个阶段,不可避免地还是要对 Game, Draw, GPU 对应的三类线程以及它们之间的关系有更详细的认识
- Game Thread 首先会对整个游戏世界进行逻辑层面的计算与模拟(e.g.Spawn 多少个新的 actor、每个 actor 在这一帧位于何处、角色移动、动画状态等等),所有这些信息会被输送到 Draw Thread,主要耗时点Tick函数
- Draw Thread(也叫 Rendering Thread) 会根据这些信息,剔除(Culling)掉不需要显示的部分(e.g. 处于屏幕外的物体),接着创建一个列表,其中包含了渲染每个物体必备的关键信息(e.g. 如何被着色、映射哪些纹理等等),再将这个列表输送给 GPU Thread,主要开销来源于 Visibility Culling 和 Draw Call
- GPU Thread 在获取了这个列表之后,会计算出每个像素最终需要如何被渲染在屏幕上,形成这一帧的画面,要开销来源于 顶点 ,作色,光照等
- 综上,对于每一帧来说,这三者的执行顺序依次为:Game Thread → Draw Thread → GPU Thread
Notes- 一帧的总耗时,取决于三者中开销最严重、即耗时最长的线程
- 逻辑在cpu计算,渲染在GPU
- 如果 GPU Thread 率先完成了它的工作,而其他二者仍在工作中(e.g. 已经绘制好了当前帧,但下一帧的数据还没拿到),那么 GPU 就会等待 CPU 的指令而导致下一帧的画面姗姗来迟;反之如果 GPU 耗时更严重,导致 CPU 输送的数据没有被及时处理,使得画面没能被及时渲染,同样会导致卡顿
3.HOW.性能优化点(使用层)
3.1 场景制作部分
a.减少材质种类,尽量使用同种材质
b.只是客户端使用的物体,同步方式改成C,如武器外观资源,子弹特效等
c.减少粒子特效的使用,控制单个特效的粒子数量,减少重叠播放粒子特效的使用,
3.2 TS代码优化点
1.对于用到大量查找的数据用map来储存数据,比如背包数据,属性资源等
export class BagItemBase {
/** 道具唯一Id */
id: string;
/** 配置Id */
cfgId: number;
/** 数量 */
count: number = 0;
}
export class BagInfo<T extends BagItemBase> extends Subdata {
items: Map<number,BagItemBase>;
}
2.清理没必要的预加载id,如大部分UI的guid,可加快进游戏的速度
3.避免在同一帧大量创建GameObject或者其他类似的逻辑,可采用分帧处理
//同一帧执行值执行的任务量过大
Event.addClientListener("second", (player) => {
for (let index = 0; index < 100; index++) {
let go = GameObject.spawn("20D499E241E742213CDECD9396ECB975", { replicates: false });
}
});
//分帧处理
Event.addClientListener("frame", async (player) => {
let index = 0;
await new Promise<void>((resolve) => {
let id = setInterval(() => {
let go = GameObject.spawn("20D499E241E742213CDECD9396ECB975", { replicates: false });
index++;
if (index == 100) {
clearInterval(id);
resolve();
}
}, 50)
})
4.合理使用对象池,循环滚动列表等现成优化组件 对象池类 GoPool ,循环滚动列表类
5.使用客户端能够完成的功能,就不要麻烦服务端,减少服务器压力
案例1.比如说背包的分解某些条件的物品,可能涉及到大量的计算。常规做法:发一条rpc 通知服务器筛选删除优化做法:客户端筛选,然后发一条 rpc包含要删除的id列表,通知服务器直接删除
案例2.大部分子弹的移动,命中检测可以在客户端做,服务器做关键性的验证
6.合理使用属性同步
1.对于所有玩家公共的,需要广播的使用属性同步能够简化逻辑,如玩家外观,武器挂件,世界boss血量等
2.对于针对某个玩家私有的,使用属性同步反而会造成不必要的通信浪费,如单人副本内的怪物血量,单人玩家的金币数量等
7.注意关闭没必要的Update调用,降低Update的调用频率
4.使用DevTools 分析TS的 内存及CPU
1.打开Chrome浏览器,地址栏输入chrome://inspect回车
2.点击Configure,配置Server的ip和port
4.1 cpu profile 点击“分析器”分页->“录制”,就可以录制一段时间的cpu性能分析报告
4.2 内存快照点击“内存”分页->“录制”,就可以拍下时刻的内存快照,对比两个快照可以找出直接的差异,从而查看内存泄露等问题
|