[开发者心得] 工程中常见的优化分享

[复制链接]
2014 |1
冰玥 发表于 2023-7-9 11:16:24 | 显示全部楼层 |阅读模式
本帖最后由 冰玥 于 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: 接收到渲染所需信息之后,将像素最终的表现画在屏幕上的耗时
image.png
在游戏中按下 ~ 键,输入stat unit

2. What.瓶颈定位
  • 瓶颈定位,就是要找到造成性能开销的最大元凶,也就是确定优化的基本方向,才能深入和落实到细节层面,进行后续的分析和优化工作。而要搞清楚开销主要发生在哪个阶段,不可避免地还是要对 Game, Draw, GPU 对应的三类线程以及它们之间的关系有更详细的认识 1280X1280.PNG
  • 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
3a57f514-10df-458c-8c41-882c73bb9c24.png

  4.1 cpu profile  点击“分析器”分页->“录制”,就可以录制一段时间的cpu性能分析报告
0d749324-3b9a-4861-b76b-c0d1c02a923e.png

  4.2 内存快照点击“内存”分页->“录制”,就可以拍下时刻的内存快照,对比两个快照可以找出直接的差异,从而查看内存泄露等问题





回复

使用道具 举报

喵喵哭唧唧 发表于 2023-7-9 11:19:00 | 显示全部楼层
感谢分享~这里分享的内容和性能教室系列帖子的一样好哎
回复

使用道具 举报

热门版块
快速回复 返回顶部 返回列表