乌啦呀哈呀哈乌啦!

欢迎光临,这里是喵pass的个人博客,希望有能帮到你的地方

0%

客户端性能优化

在面试中回答这个问题,建议从宏观的系统层面和微观的渲染层面两个维度来展开,并重点围绕“CPU瓶颈”与“GPU瓶颈”这两个核心概念来阐述。

系统层面

CPU与逻辑优化(解决主线程阻塞)

减少DrawCall(绘制调用)

  • 这是最常见的优化点。通过静态合批(合并静态场景)、动态合批(合并同材质小模型)以及GPU Instancing(大量重复物体如草丛、子弹的实例化渲染)来大幅降低CPU向GPU发送指令的次数。

预加载

  • 预加载常用的资源,比如场景中的静态资源、UI 资源、音效资源等等。这些UI可以在游戏开始前加载UI资源但不实例化,只是将其加载到内存中。
  • 有些UI的实例化和初始化也可以提前到游戏开始前。实例化和初始化完成后,将其“隐藏”起来,待到需要的时候再将其展示出来。
  • “隐藏”既可以将UI放在某一特定不被渲染或者未激活的层级,也可以将其放到摄像机视角之外藏起来。或者可以单独设置一个层级为不可见的layout,需要显示时再将UI元素放在可见层级中去。
  • 比如在场景加载阶段(Loading 界面)预先实例化核心 UI 并隐藏(SetActive(false)),这属于将提前初始化,而非延迟初始化,适合高频使用的核心界面。

异步处理与分帧加载 (Time-Slicing)

  • 避免在每一帧(Update)中执行大量计算。将路径寻路、AI决策等耗时逻辑拆分到多帧执行,或者放入异步线程/WebWorker中,防止阻塞主线程导致卡顿。
  • 对于包含大量子元素的列表(如背包系统),不要在同一帧初始化所有项。可以使用协程(Coroutine)每帧只初始化 2-3 个元素,避免单帧计算过载导致的掉帧。

物理与AI精简

  • 关闭远处或视野外物体的物理碰撞检测和AI逻辑,只计算玩家周边的关键交互。

内存优化(杜绝泄漏与峰值溢出)

对象池

  • 对象池可以提高对象创建和销毁的效率,因为对象池中的对象可以重复使用,从而避免了创建和销毁对象的开销。
  • 延迟初始化后,不使用的 UI 不要直接 Destroy,而是放入对象池中。下次需要时,直接从池中取出并重新初始化,这比反复 Instantiate 性能更高。

延迟初始化

  • 采用按需加载/Lazy Initialization的思路,在运行时加载需要的资源,而不是在编译时加载所有的资源。
  • 仅在 UI 界面真正需要显示时,才调用 Instantiate 创建对象或从对象池中取出。

资源管理与按需加载

  • 将资源采用高压缩率格式压缩,例如LZ4HC (用打包时间换空间)
  • 大世界采用流式加载,只加载玩家周围一定范围内的资源;切换场景或退出副本时,强制卸载无用资源并释放内存。
  • 在背包或一些数据项比较多的应用中,scroll view常常会有大量的元素存在,他们在窗口中的不断滚动变化导致前面说的网格重构,消耗大量的CPU资源。
  • 可以只实例化加载稍微比可视化区域大点的数据项UI,当滚动达到一定范围后再销毁一部分不可见的元素,同时加载一部分后续可能显示的元素(结合对象池)

字体拆分

  • 字体一定程度上站了很大的空间(汉字😣),而且游戏打多需要的文字固定,没必要仅为少数的文字使用而加入整个字体库。
  • 数字字母等固定的字符可以单独打包,汉字常用字符3000足够游戏使用,真遇到特殊字符可以单独添加进去,在通过unity下的字体制作工具打包处理。

UI贴图

  • 倒入贴图资源时,贴图的某些属性或数据其实并不是必要的,但要占用很大的空间,不合理的贴图大小也会增大资源占用。
  • 导入贴图资源时注意事项:
    1. alpha通道是否需要/
    2. 2次方贴图大小 (宽、高分别是 2 的整数次幂,也叫 Power of Two 纹理,例如:256 × 256,512 × 1024)
      以前很多 GPU 对非 POT 纹理有限制,现在通常支持。但部分压缩格式仍对尺寸有要求,例如 PVRTC 常有 2 次幂限制,具体取决于平台和格式。不必把所有图片都强制改成 POT,应结合目标平台、压缩格式和图集设置判断。
    3. 读写权限是否需要
    4. MipMap是否需要(多级渐远纹理)。它会为原图生成一组逐级缩小的版本,例如:1024 × 1024 → 512 × 512 → 256 × 256 → … → 1 × 1,物体远离镜头、贴图只占少量屏幕像素时,GPU 可以使用较小的版本
      开启 Mipmap 后,在普通纹理采样中,GPU 通常会自动选择合适的层级,前提是采样设置允许使用 Mipmap。它主要根据纹理坐标在相邻屏幕像素之间的变化率判断,相当于估算:一个屏幕像素覆盖了原贴图多少像素。
    5. 合适的压缩方式
  • 上述过程可以通过unity editor扩展asset importer pipeline的方式自动化管理,可以通过Texture Importer类控制指定导入路径的图片具体设置。

网络与IO优化

协议压缩

  • 使用 Protobuf 等二进制协议替代 JSON/XML,大幅减小数据包体积,解析速度更快。
  • Protobuf 是基于预定义协议(契约),在“对象”和“二进制字节数组”之间进行高效、双向转换的通信标准
  • Protobuf 按照提前写好的 .proto 协议文件(也就是双方约定好的“契约”),通过字段编号、数据类型等规则,进行极其精简的二进制编码。

数据差分同步

  • 状态同步时只发送变化的数据(如位置偏移量、血量变化),而不是发送全量数据。
    1. 只发送变化的字段:血量从 100 变成 90,只发送 { hp: 90 },省略未变化的位置、装备等字段。
    2. 只发送变化量:发送 { hpDelta: -10 },客户端在原有血量上减去 10。
  • 前者更容易纠正状态,后者依赖双方基准一致;丢包、重复处理或乱序可能导致偏差,因此通常需要版本号、顺序控制,以及定期全量同步来校准。

渲染层面

渲染优化的核心是减轻GPU的压力,核心指标是降低 Overdraw(过度绘制)和 FillRate(填充率)的消耗。

UI画布

动静分离

  • unity中UGUI系统都是使用网格模型来构建,UI元素只要“动”,就会发生网格的重构合并。正所谓牵一发而动全身,少的UI元素的动导致整个UI的重构,由此带来重绘导致了cpu的开销。
  • UGUI系统以Canvas作为自己的重绘节点,所以需要以canvas作为节点拆分动和静的UI元素。
  • 将频繁更新的动态 UI 与不常变动的静态 UI 拆分到不同的 Canvas 中,因为每次UI更新都会触发 Canvas 的重建。
  • 这样当动态 UI 延迟加载并触发重建(Rebuild)时,不会影响到静态 Canvas 的性能。

降低 Overdraw(过度绘制)

  • 控制透明层叠加:透明物体(如特效、粒子、UI流光)是 Overdraw 的重灾区。严格限制透明层的叠加数量(通常不超过2层),远距离的特效直接降低分辨率或关闭。
  • 遮挡剔除(Occlusion Culling):开启引擎的遮挡剔除功能,被墙体等物体完全挡住的物体直接不参与渲染。
  • 分层渲染:将UI、场景、角色、特效划分不同的渲染层级,透明与不透明物体严格分离。

避免 Layout Group 重建

  • 瞬间加载大量元素到含有 VerticalLayoutGroup 的父物体时,会导致剧烈的 Layout 重建。
  • 可以先禁用 Layout 组件,等所有元素加载完毕后,使用 LayoutRebuilder.ForceRebuildLayoutImmediate 一次性刷新。

网格重构

  • UGUI系统网格合并机制是,只有将相同材质球的网格合并在一起,才能达到最佳效果。一个材质球对应一个图集,只有相同图集内的图片才需要合并在一起。
  • 当元素需要改变颜色时,是通过改变顶点颜色实现,然后将他们重新合批到网格中去。在UI动画里修改UI颜色时,UGUI都会对网格进行重构。
  • 此时我们需要自定义UI元素的材质球,通过自定义材质球改变相应的颜色变化。
  • 此时UGUI不再重构网格,因为把渲染工作交给了新的材质球,而非通过UGUI设置顶点颜色达到效果。

模型与纹理优化

LOD(多细节层次)

  • 根据物体距离摄像机的远近,自动切换不同精度的模型。远处的物体使用低模(面数少),近处使用高模。

纹理压缩与格式选择

  • 使用 GPU 友好的压缩格式(如 ASTC、ETC2、BC7),在保证画质的前提下大幅降低显存占用和带宽压力。

Shader(着色器)轻量化

简化光照计算

  • 移动端尽量避免复杂的光照模型、实时阴影和高光反射。

减少分支指令

  • Fragment Shader(片元着色器)中的 if/else 分支会严重影响GPU并行计算效率,尽量用数学运算替代逻辑分支。

性能分析工具

  • 优化不能靠猜,必须依赖工具。
  • 常用的有 Unity 的 Profiler / Frame Debugger,或者第三方的 RenderDoc、Snapdragon Profiler。
  • 通过这些工具可以精准定位到当前帧的瓶颈是在 CPU(如 DrawCall 过高)还是 GPU(如 Overdraw 严重或 Shader 过于复杂),从而进行针对性的优化。