以 Unity3D 为例,整理了一些性能优化的主要内容和知识点。抛开 Unity 性能优化的方向是类似的,主要内容无非是CPU/GPU/内存/包体积等几个大方向的优化,道理是相通的,知识是可以迁移的,希望能对游戏领域的小伙伴有所帮助。文章部分正在整理和汇总中,格式和内容还有不完善,请见谅。

DrawCall

Drawcall(绘制调用)是指在图形渲染中,向图形API(如OpenGL或DirectX)发送绘制命令的操作。每次绘制调用都会告诉图形API如何绘制一个或多个图形对象(如三角形、矩形等),包括顶点数据、纹理信息、着色器等。 在游戏或图形应用中,绘制调用的次数对性能有较大影响。每个绘制调用都会引起CPU和GPU之间的通信开销,而通信开销是比较昂贵的。
比如你要买水果,一次袋子里只装一个拿回家,往返20次;还是一次装20个只往返一次呢。也许你觉得对于计算机巨大的算力来说这不算什么,但是实际情况是,这种类似的计算每秒要计算上千万次。尽量减少不必要的计算会导致累积的结果发生质的变化。因此,减少绘制调用的次数以提高性能是一个重要且常见的优化手段。

DrawCall的优化

在Unity中,有以下几种常见优化Drawcall的方法:

  1. 批量处理(Batching):将多个相同材质、纹理、渲染模式等属性的图形对象合并到一个Drawcall中,减少CPU-GPU通信消耗。Unity有两种批处理的方式:静态合批动态合批

    • 静态合批:适用于不会频繁移动或者修改的静态物体,如静态环境、地形等。 - 合并方式:在Build阶段,Unity会将静态物体合并成一个大Mesh,使用共享的材质,以减少Drawcall
    • 动态合批:适用于需要频繁移动或者修改的动态物体,如粒子效果、角色等。 - 合并方式:在运行时Unity自动把每一帧画面里符合条件*的多个模型网格合并为一个,再传递给GPU
  2. 合并纹理(Texture Atlasing):将多个小纹理合并为一个大纹理图集,减少绘制调用和纹理切换。可以使用TexturePacker等工具来自动合并纹理;可以使用Sprite Atlas将Sprites打包成同一图集。

  3. 减少不必要的渲染状态切换:避免在绘制调用之间频繁切换渲染状态,如材质、着色器等。可以通过合并材质、使用相同的着色器等方式来减少状态切换。

  4. GPU Instancing:利用硬件实例化技术,通过复制几何体和使用实例化数据来减少绘制调用,对于使用同一网格 和 同一材质的物体们,它使用少量的渲染调用(DrawCall),渲染同一网格的多个副本。有Unity自动和手动两种方式。

  5. 减少透明物体的数量:透明物体需要额外的渲染步骤,因此数量过多会导致 Draw Call 增加。可以通过使用不透明物体、使用 Alpha Test 等方式来减少透明物体的数量。

只有当动态批处理产生的CPU开销小于DrawCall的开销,动态批处理才具有优化性能的效果。而在如今的电子设备上,动态批处理产生的CPU开销反而有可能大于DrawCall的开销,影响性能

UGUI的优化

Unity中UI优化的主要方向是减少Drawcall,主要的优化思路与上文类似,只是实现手段的不同。 简要的说一下以上提到的一些UI减少Drawcall的实现方式:

  • 使用UI Image组件和Sprite Atlas
  • 避免使用大量的透明材质
  • 使用合适的排序层
  • 禁用不需要的Raycast Target
  • 使用合适的像素适应设置
  • 使用合适的UI Element组件,例如Image和rawImage
  • 避免使用过多的动态效果

除此之外还有进阶的优化思路:

  1. UI的动静分离
    UGUI构建后会合并网格。当UI元素移动或者Transform时,需要重新合批,导致同一批中的其他内容需要再次构建。因此将会动的UI元素放入专门的画板中,将静止的UI元素留在另一个画板上,这样在重构动态UI的时候就不会影响到静态UI,减少了CPU重绘和合并的消耗。
  2. 拆分过重的UI
    一个部件的UI元素过多,层级过深,会导致实例化或者是销毁的成本增加,也会造成搜索效率的骤减,并且难以管理。所以拆分与重组相关UI也是优化的方向。
  3. UI预加载
    UI实例化时,期间会有网格的合并、组件初始化、渲染初始化、图片加载、界面逻辑初始化等,会消耗大量的CPU。可以考虑负载均衡的预载时机。
  4. UI贴图设置的优化
    • 是否需要Alpha通道。需要则打开,不需要则关闭。
    • 是否需要进行2次方大小修正。对于UI贴图来头像这类Icon基本上都是2次方大小。
    • 去除读、写权限。这里通勾选的话,会使贴图在内存中存储两份方便脚本读写,导致比不勾选的时候内存增加一倍。
    • 去除Mipmap。Mipmap是对3D远近视觉的优化,通过生成不同大小的图,在摄像头远离物体时因为不需要高清的图片而使用Mipmap生成的小而模糊的贴图,从而减轻GPU的负担。但2D中没有远近之分,不需要这个功能,使用的话反而会导致内存和磁盘空间加大,UI看起来模糊。
    • 选择压缩方式。压缩主要时为了降低内存的消耗、加载时的消耗,降低CPU与GPU之间的带宽消耗,在保证清晰度的前提下,应尽可能的选择一个压缩方式来优化内存和包体

这里只列举了部分优化思路,还有很多其他思路,在这里先不一一列举了,有时间再添加别的。

内存优化

Unity的内存主要分为三大类:

  • 托管内存:主要是托管堆或GC管理的内存
  • 非托管内存:C#交互使用的内存,可以与Unity Collection命名空间和package结合使用,不包含GC内存
  • Native内存:Unity运行引擎使用的C++部分的内存

需要注意到的是,以上是Unity使用的内存,如果是Unity应用,应用程序还会占用GPU显存(移动端的内存),程序框架占用的内存,第三方库使用的内存和系统内存。

代码架构优化

ECS架构

ECS(Entity-Component-System)是一种用于游戏开发的架构模式。它的核心思想是将游戏中的实体(Entity)、组件(Component)和系统(System)分离,以提供更高效和可扩展的编程模型。个人认为ECS其实就是为了代码解耦

在 ECS 架构中,游戏对象(实体)被分解为包含不同功能的组件。每个组件只包含描述实体的某个特定方面的数据,而不包含任何行为。例如,一个游戏角色实体可以包含渲染组件、动画组件和碰撞组件等。实体和组件时一对多的关系,实体有怎样的功能取决于其所拥有的组件,再游戏中Runtime地进行组件的添加/修改/删除就可以Runtime地改变实体的行为。

组件和Unity的Component概念相似,但是这里的组件不包含任何行为,组件的行为是通过一些独立的系统来处理的,这又和Unity中继承式的组件结构又完全不同了。这些独立的系统负责对具有相似组件的实体进行操作和处理。系统通过访问组件的数据进行逻辑处理,例如更新位置、计算物理碰撞等。每个系统只关注和处理特定的组件,而与其他组件和系统无关。

ECS 在2017的 GDC 上由暴雪守望先锋的开发者分享出来并得到大规模关注(并非暴雪提出此架构),GDC永远是游戏开发者的知识盛会,无数游戏开发的创意、结构、设计都由世界各地的开发者由此分享出来。

ECS的优势

ECS组件的行为通过系统来处理。系统是一些独立的模块,负责对具有相似组件的实体进行操作和处理。系统通过访问组件的数据进行逻辑处理,例如更新位置、计算物理碰撞等。每个系统只关注和处理特定的组件,而与其他组件和系统无关

所以ECS结构有以下优势:

  1. 扩展性:ECS的行为逻辑是数据驱动的,因此实体的行为和属性数据是分离的,使得可以实现更为灵活的组合和代码扩展,降低代码耦合度。
  2. 性能优化:ECS的数据驱动逻辑可以实现相关对象实体执行行为逻辑而不需要遍历所有的对象。

那么有哪些模块适合使用ECS呢?

  1. 游戏对象管理:当游戏中有大量的实体,ECS易于处理实体的创建、销毁和更新。
  2. 物理模拟:ECS可以用于大规模的物理模拟,例如粒子物理,通过物理组件作为实体的一部分,针对这些组件的物理系统进行更新,可以获得更好的性能。
  3. 碰撞检测:和物理模拟类似,ECS可以实现通过独立的系统来处理碰撞检测,例如将每个实体的碰撞组件作为输入,根据需要执行碰撞检测逻辑。这种方式可以提高碰撞检测的效率,特别是当涉及大量实体和复杂碰撞逻辑时。
  4. AI寻路:对于需要处理大量AI代理和路径规划的游戏,ECS架构可以提供更高效的处理方式。通过将AI组件和路径组件与独立的系统结合使用,可以对大量AI代理进行并行化处理

Unity中的ECS

Unity中可以使用Unity.Entities命名空间来使用ECS的API,需要导入ECS的包:Entities

Loading… Please stand by…
文章正在加载中… …

作者博客:YMX’s Site
作者B站视频:CyberStreamer