前言:记录了总6w字的面经知识点,文章中的知识点若想深入了解,可以点击链接学习。由于文本太多,按类型分开。这一篇是 性能优化 常问问题总结,有帮助的可以收藏。
性能优化,主要聚焦在 内存、 CPU、GPU 三大方向上。
Unity内存占用组成
1.1.1 Mono方向
(1)编程语言:字符串拼接,减少频繁扩容,合理用new,减少内存碎片的产生(将大额内存进行预处理)等一系列降低GC的操作。尽量用结构体取代类(值类型不占用堆,不需要GC)。
(2)配置表优化:延迟加载,具体表现为:
1.protobuf主要用于网络传输,或者将一些数据(比如聊天数据)序列化在存本地,下次启动文件再反序列化过来就可以了。
2.一个字典存储了所有的配置表信息,key是配置表的文件id,value是一个字典存储的配置表中的配置信息,其中key是配置ID,value是行数据偏移信息(配置行的起止位置)。
3.序列化的时候先只需要储存索引,要用到的时候按照索引再去现场序列化即可,这样可以不在游戏开始就序列化所有数据,大大的降低内存占用。
C#侧优化方案-用MessagePack压缩
MessagePack for C#(MessagePack-CSharp)是用于C#的极速MessagePack序列化程序,比MsgPack-Cli快10倍,与其他所有C#序列化程序相比,具有最好的性能。 MessagePack for C#具有内置的LZ4压缩功能,可以实现超快速序列化和二进制占用空间小。
详细请看:MessagePack for C# - HackerVirus - 博客园MessagePack for C# 快速序列化组件MessagePack介绍 简介 MessagePack for C#(MessagePack-CSharp)是用于C#的极速MessagePackhttps://www.cnblogs.com/Leo_wl/p/8143259.html
1.1.2 Native方向
美术资源,模型面数,密度精度,贴图格式,限制等。
1.1.3 对象池
管理unity对象
1.1.4 AssetBuildle
1. 资源冗余(重复资源)
2.避免运行时资源泄露。
为什么会资源泄露
UI优化核心就是一点:减低DrawCall
2.1 DrawCall介绍
CPU准备好需要绘制的元素,对底层图形程序接口进行调用的过程。
也可以理解为:CUP向GUP发布一条渲染指令,就是一次DrawCall的过程。简称DC。
2.2 降低DrawCall
2.3 合批
一次Draw Call中批量处理多个物体。只要物体的变换和材质引用相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。
注意:简单来说在一个Canvas下,需要相同的材质,相同的纹理以及相同的Z值。例如Ul上的字体Texture使用的是字体的图集,往往和我们自己的UI图集不一样,因此无法合批。还有UI的动态更新会影响网格的重绘,因此需要动静分离。
2.3.1 静态合批
将static的静态物体(永远不会移动、旋转和缩放) ,如果相同材质球,面数在一定范围之内。unity会自动合并成一个batch送往GPU处理。
1.需要做的事情
把要进行静态批处理的GameObject在Inspector面板右上角的Static勾选(实际上只需要勾选Batching Static即可)
2.优点
因为只需要进行一次,所以性能会比动态批处理要好。
3.缺点
使用静态合批需要额外的内存开销来存储合并后的几何数据。
因为需要额外维护多一份数据,所以包体会变大,占用的内存也会变多(不能有超级大量的相同模型(如:森林里的树))
无法移动
进行了静态批处理之后的GameObject不能在游戏运行时改变位置或者是跟渲染有关的属性。并且因为把所有要静态批处理的GameObject都合并成一个大网格保存起来,所以这实际上相当于即使是同一个GameObject,也需要复制一份网格数据一起保存在这个大网格的顶点数据里面去,这样就导致了占用的内存变多了。
静态合批就是多渲染一套合并后的网格 ,提前存在内存里,内存当然就大了。
4.原理
在开始阶段把需要静态批处理的GameObject进行一次网格合并操作,然后把这个合并之后的大网格保存起来,后续都是用这个网格而不需要再进行合并。
在预处理阶段,把一些材质相同的模型的顶点统一变换到世界空间坐标下,并且新构建一个大的VB把数据保存下来,在绘制时,就会把这个大的VB提交上去,只需要设置一次渲染状态,再进行多次drawcall绘画出每个子模型。 所以Static Batching是不会减少drawcall的,但由于只修改了一次渲染状态依然可以减少CPU的消耗。而且在渲染前,也可以进行视锥体剔除,减少顶点着色器对不可见的顶点的处理次数,提交GPU的效率。
2.3.2 动态合批(OpenGL介绍)
如果动态物体共用着相同的材质,那么Unity会自动对这些物体进行批处理。动态批处理操作是自动完成的,并不需要你进行额外的操作。
1. 优点
不用自己做任何事情,Unity会在游戏中自动进行动态批处理,只要满足下述条件。
在Unity中,要进行动态批处理需要满足以下条件:
- 顶点属性要小于900。例如,如果shader中需要使用顶点位置、法线和纹理坐标这三个顶点属性,那么要想让模型能够被动态批处理,它的顶点数目不能超过300。因此,优化策略就是shader的优化,少使用顶点属性,或者模型顶点数要尽可能少。(这个是《UnityShader入门精要》这本书上说到的,同时书上也说了不一定是900,可能不同版本的Unity会有所区别,这个可以自己在Unity中去手动验证得出)
- 多Pass的shader会中断批处理。
- 使用LightingMap的物体需要小心处理。为了让这些物体可以被动态批处理,需要保证它们指向LightingMap中的同一位置。
2. 原理
Unity会检测哪些GameObject使用了同一个共享材质,然后去合并这些使用了同一个共享材质的网格顶点数据,形成一个新的大网格,然后传给显存,直接渲染这个大网格就相当于渲染了所有的被合并的小网格,而这只需要一次DrawCall。
在每一帧运行时,计算相同材质的模型,把他合并批次进行渲染。动态合批只需要设置一次渲染状态,且能减少drawcall次数。
为什么用了不同的材质,不同的贴图,不同的材质属性等会导致不能动态批处理呢?(OpenGL)
在Unity渲染其实也是调用的OpenGL或者是DirectX,因为我只了解过OpenGL,所以以OpenGL为例(DirectX也是同理)。OpenGL中要渲染一个东西出来的,需要顶点着色器和片元着色器,这个对应的是ShaderLab中的顶点着色器和片元着色器,然后还需要把要渲染的网格的顶点属性作为一个数组绑定到VBO(顶点缓存对象)中去,然后绑定VAO(顶点数组对象)并设置顶点数组的属性,然后一些需要外部设置的着色器属性也是在这个阶段进行设置,当做完这些之后,再调用glDrawElements(也就是一次DrawCall)去渲染这个物体。这就是OpenGL渲染一个东西的最简单的流程。所以回到问题,为什么贴图,材质属性等必须一样呢?因为如果不一样的话,他们就不能通过一次DrawCall去设置这些属性和贴图了,想想看,如果A物体使用了贴图A,B物体使用了贴图B,如果把他们合并成一个大网格,要直接在一个DrawCall里渲染出来的话,这个合并好的大网格到底该用贴图A还是贴图B呢?无论使用哪个,都是不对的。所以A物体和B物体不能进行批处理。
2.3.3 动静合批的区别
动态批处理一切都是自动的,不需要做任何操作,而且物体是可以移动的,但是限制很多。
静态批处理:自由度很高,限制很少,缺点可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了。
静态合批发生在加载场景的时候。
动态合批发生在游戏运行的时候。
2.3.4 合批了就一定就性能更加好嘛?
不一定,场景中比如有很多颗静态的树,你如果设置静态合批,内存直接炸裂。
2.3.5 静态合批是在什么时候完成的?
加载场景的时候
在unity上 启动项目就会加载场景
在手机上 要先 加载资源 解析资源 释放资源
所有在层级窗口中的物体,实际上已经记录数据了,加载场景是直接通过反射来创建,这个时候(加载场景之初)就会进行和批处理,所以运行中动态创建的物体 即使勾选了static 也不会进行和批处理(因为时间已经过了)。
2.3.6 静态合批有数量限制/动态合批的限制多少顶点
32k顶点 / 900
每个Batch最大300Mesh。
详细请看
浅谈Batch(批次合并)_0zien0的博客-CSDN博客浅谈Batch(批次合并)https://blog.csdn.net/a42626423/article/details/126765697
2.4 DrawCall影响
DrawCall越高对显卡的消耗就越大。(影响CPU,导致游戏性能下降)
影响CPU性能的原因:
当CPU发布一条指令后,传到GPU处理,GPU处理渲染速度很快,对于30条还是300条时间影响不是很大。基本都早早的完成“工作”而处于闲置状态。
而对于CPU而言,每一次 DrawCall 前,CPU 都需要做一系列准备工作,才能让 GPU 正确渲染出图像。而 CPU 的每一次内存显存读写、数据处理和渲染状态切换都会带来一定的性能和时间消耗。这些相对于 GPU 渲染来说非常慢。大量的 DrawCall 会让 CPU 忙到不可开交,而 GPU 大部分时间都在摸鱼,是导致游戏性能下降的主要原因。
3.1 Overdraw介绍
Overdraw是指屏幕上的某个像素在同一帧的时间内被绘制了多次。
3.2 Overdraw影响什么
过多Overdraw可能会引起GPU过载,影响动画的播放和界面响应速度。
3.3 降低Overdraw
1.将Mask(自带两层Overdraw)替换为RectMask2D(自带一层Overdraw)
2.全屏遮挡的情况,则为被遮挡的canvas添加CanvasGroup 组件,然后在被挡住时将其alpha值设为0,不参与绘制,没有 DrawCall也没有Overdraw,同时顶点也不会参与重绘。
3.unity的文字显示中自带阴影组件shadow/Outline减少使用,自带overcall。
4.UI摆放,减少UI的叠加效果。
5.将不想显示的物体直接禁用,而不是将其透明度改为0,仍会产生overcall。
详细请看:
Unity Ugui 优化 OverDraw_开着房车环游世界的博客-CSDN博客_overdraw unity1.如果你使用了九宫格 那么 Fill center 的效果是不一样的这是不勾选勾选后就变成这样了这个会影响overdrawMask组件,它自带两层OverDrawText组件的OutLine和Shadow,Shadow会增加一层OverDraw,而OutLine是复制了四份Shadow实现的4 这个就是偏移后的overdraw确实是4个5 对于弹出窗口,位于底层的窗口如果被上层遮挡,请将它从Camera渲染层级里移除并将不可见的Canvas设置enable = false,不https://blog.csdn.net/qq_38913715/article/details/122537287
详细请看:
【Unity优化篇】| Unity3D场景 常用优化策略,遮挡剔除、层消隐距离技术 和 LOD多层次细节_呆呆敲代码的小Y的博客-CSDN博客_unity场景优化本篇文章来简单介绍一下 Unity中几种常用3D场景优化策略 方法对于整个项目来说,本文中出现的方法也只是冰山一角,慢慢积累学习吧!内容包括遮挡剔除(Occlusion Culling)、层消隐距离技术 和 LOD多层次细节https://xiaoy.blog.csdn.net/article/details/123869619
4.1.1 介绍
当场景中包含大量模型时,DrawCall就会非常大,造成的性能消耗就会比较大,从而会造成渲染效率降低,使用遮挡剔除技术,可以使那些被阻挡的物体不被渲染,达到提高渲染效率的目的。
4.1.2原理
在场景空间中创建一个区域,该遮挡区域有单元格组成,每个单元格构成整个场景的一部分,这些单元格会把整个场景拆分成多个部分,当摄像机能够看到该单元格时,表示该单元格中的物体会被渲染出来。
如果场景中存在大量小"物件”,则可以使用"层消隐距离"来优化场景;"层消隐距离"就是在比较远的距离将小物体剔除以减少绘图调用的数量(比如:可以一个大型场景中,高大型的物体任然可见,但是一些小装饰内容(小狗、车子之类的则可以隐藏))
LOD(Level of detail)多层次细节,是最常用的游戏优化技术。
它按照模型的位置和重要程度决定物体渲染的资源分配,降低非重要物体的面数和细节度,从而获得高效率的渲染运算。
这就是说,根据摄像机与模型的距离,来决定显示哪一个模型,一般距离近的时候显示高精度多细节模型,距离远的时候显示低精度低细节模型,来加快整体场景的渲染速度。
作用 : 优化GPU
缺点 : 同一模型要准备多个模型,消耗内存。
特点 : 以内存做消耗来优化GPU
为了加快渲染速度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,这样的贴图被称为 MIP map 或者 mipmap。
5.2.1 优点
优化显存带宽,用来减少渲染。因为可以根据距离摄像机远近,选择适合的贴图来渲染。所以UI不适用MipMap。
5.2.2缺点
运行时占用更多内存,且增加包的容量。
【使用 Mip maps 需要使用 33%以上的内存,但不使用它会导致巨大的性能损失】
Mipmap纹理技术是目前解决纹理分辨率与视点距离关系的最有效途径,它会先将图片压缩成很多逐渐缩小的图片,例如一张64*64的图片,会产生 64*64 , 32*32 , 16*16 , 8*8 , 4*4 , 2*2 , 1*1的7张图片,(从2的0次幂 到2的n次幂,n+1即为图片大小)当屏幕上需要绘制像素点为20*20 时,程序只是利用 32*32 和 16*16 这两张图片来计算出即将显示为 20*20 大小的一个图片,这比单独利用 32*32 的那张原始片计算出来的图片效果要好得多,速度也更快。
Mask实现的具体原理是一个DC来创建Stencil mask(来做像素剔除),然后画所有子UI,再在最后一个DC移掉Stencil mask。
这头尾两个DC无法跟其他UI操作进行Batch,所以表面上看加个Mask就会多2个DC,但是,因为Mask这种类似“汉堡包式”的渲染顺序,所有Mask的子节点与其他UI其实已经处在两个世界了,上面提到的层级合并规则只能分别作用于这两个世界了,所以很多原本可以合并的UI就无法合并了。
原因是不同材质不能合批。具体来讲就是 Mask用的Stencil模板材质;而非Mask用的default Ul mat材质。
因此mask会比rectMask2D额外生成两个DC。
1.RectMask2D本身不产生drawcall.
2.不同RectMask2D的子对象不能合批.
RectMask2D控件的局限性包括:
仅在2D空间中有效
不能正确掩盖不共面的元素
RectMask2D的优势包括:
不使用模板缓冲区
无需额外的绘制调用
无需更改材质
高速性能
详细请看:
Unity 3D - Mask和RectMask2D区别_Yu______________的博客-CSDN博客_mask和rectmaskUnity 3D - Mask和RectMask2D区别 :主要区别:区别1:Mask主要处理不规则图形遮罩效果RectMask2D只能做矩形遮罩. 区别2:Mask需要一个Image来当作遮罩区域,子节点在Image[渲染区域]才会显示RectMask2D以自身RectTransform为裁剪区域,子节点在[RectTransform区域]内显示从效果上看 Mas...https://blog.csdn.net/yu__jiaoshou/article/details/102742784
图集就是碎图合成大图 降低内存,减少dc。
UI图集有合批没有的优点,就是热更新的时候因为小文件变少了,所以会快一些。
UI图集就是UI的动态合批。
UI图集完成合批的条件是什么?
深度 贴图 材质 => 排序好的列表当前这个依次和前面对比是否贴图和材质ID相同决定是否合批。
扩展:
Unity 将Sprite打包进图集_心之凌儿的博客-CSDN博客_unity精灵图集打包前言:打包图集是一个很简单的过程,但是如果没有这样的经验,可能不知道如何下手本博客大概说一下打包流程,以及一些基本原理1、为什么要打包图集首先要了解一点,在UGUI中即使你什么都不干,在项目打包时,Unity也会自动将你的一些小图片资源合在一张大图里面,也就是合成一张图集,但是这种默认的方式,会产生大量的Draw Call,对于性能消耗比较大而NGUI则是开发者必须要将图片资源打包成图集,然后才能使用这些图片资源因此无论是UGUI还是NGUI,掌握图集的打包都是非常重要的2、打包开始前在合成https://blog.csdn.net/xinzhilinger/article/details/116043662
Unity3D:打包图集与动态加载图集的两种方法_CalmerLoader的博客-CSDN博客_unity 加载图集Unity3D图集打包老打包方法新打包方法加载图集老图集加载处理新图集加载处理两种图集的区别https://blog.csdn.net/qq_34406755/article/details/103699215
UGUI性能优化——图集(上) - 知乎前言:上主要是了解一下图集和它的作用,下会讲解如何优化我们的图集,我们的DrawCall的具体算法和进一步优化思路。1.什么是图集?首先,你必须把你的美术资源TextureType改为Sprite(精灵类型),因为SpriteAltas只…https://zhuanlan.zhihu.com/p/391885850
LightMap:就是指在三维软件里实现打好光,然后渲染把场景各表面的光照输出到贴图上,最后又通过引擎贴到场景上,这样就使物体有了光照的感觉。
Mask使用模板缓冲来实现区域切除逻辑(Stencil),会占用两个DC。它实现最初设置模板缓存会给Mask添加一个特殊的材质,并且以像素为单位存储是否需要显示最后还原模板缓存,这两次操作各增加一次DC。它可以和其他Mask子物体进行合批,如果两个mask重叠了,那就不能进行合批,会产生额外的dc。
rectmask2d继承自IClipper接口,内部主要实现的就是一个方法来实现了区域的切除逻辑,本身是不占用DC的,完全遮住的情况下不会绘制顶点和面,不参与深度运算不占用DC(和mask的最大区别)。 缺点:它无法和RectMask的子物体进行合批,只能和自身的子物体进行合批(注:如果本身带了Image组件的话是可以进行合批的)
mask2d只能矩形,要不同形状的遮罩还是得mask,所以RectMask2D并不一定完全好,他在特定情况下无法合批。
你的背包设计了1w个道具,如何优化?
操作
1. 每个滚动条目都是同一个预设体的实例
2.做缓存,只要实例化视野范围内滚动条目,往上超出视野部分,自动填补到视野的下面
可以缩小我们为性能瓶颈的搜索范围提供很大的帮助。
SetPassCall: 通俗的讲解就是更换重新装在渲染管线里面的Shader代码和配置,就像换画笔一样的。SetPassCall开销非常的大,所以尽可能的要少用一些不同的Shader,再一个场景里面。尽可能的让同一个Shader 绘制最多的物体后再切换下一个Shader。尽量避免绘制物体的时候平凡交叉的来回切换Shader来节约SetPassCall的次数。
可以把Shader 设置为常驻内存缓存,这样节约SetPassCall所带来的开销。
例如当mmo类型游戏中 存在多个玩家时,一个玩家移动,将通知所有玩家随之跟新,发送与接收数据太多造成卡顿,利用AOI机制(兴趣范围)
以状态修改的角色为原点,设置半径,获取该角色的兴趣范围,当其他角色存在该兴趣范围的时候,进行接收通知并更新状态,减少cpu压力。
可以利用该思想去解决不同的方案
思想(这是圆形区域,或者可以格子布局(九宫格优化)或者两者组合,扩大地图等):
优点:易于实现,且不需要特殊的数据结构。
缺点:计算成本较高:需要该角色与所有角色的距离判断。(1+n)n/2。
优化:
优化设计方案:
减小 Max Size
使用能生成视觉上可接受的结果的最低设置。这种非破坏性方式,可以快速降低纹理内存。
使用 2 的幂 (POT)
Unity 要求移动端纹理压缩格式(PVRCT 或 ETC)采用 POT 纹理尺寸。
说明:当游戏中的图标满足边长为2的n次方的时候,满足POT格式,比非POT格式的图标占用更少的内存。可以被GPU直接解析。
例如 119*119的图标(非POT格式)大小为55.3k,可以利用图集解决。
128*128的图标(POT格式)大小为16k。
所以并非图标越小占用内存越少。
原因:
内存对齐:许多图形硬件和软件要求纹理的尺寸必须是2的幂次方。使用POT尺寸的图像可以满足这种要求,因此在加载和渲染时更高效。
压缩算法:许多POT格式采用了压缩算法,如PNG的无损压缩和JPEG的有损压缩。这些算法可以有效地减少图像文件大小,从而减少内存占用。
质量损失:某些POT格式可能会对图像进行压缩,减少像素信息的精度或丢弃一些细节。虽然这样可以减小文件大小和内存占用,但也可能导致图像质量的损失。
将工作推迟到必要时进行以避免不必要的工作。就是用一个标志位来标记内容是否发生变化,如果没有发生变化就直接使用缓存数据,不需要重新计算。
要点:
脏标识模式:当前有一组原始数据随着时间变化而改变。由这些原始数据计算出目标数据需要耗费一定的计算量。这个时候,可以用一个脏标识,来追踪目前的原始数据是否与之前的原始数据保持一致,而此脏标识会在被标记的原始数据改变时改变。那么,若这个标记没被改变,就可以使用之前缓存的目标数据,不用再重复计算。反之,若此标记已经改变,则需用新的原始数据计算目标数据。
使用场合:
1,原始数据转换到目标数据会消耗很多时间,都可以考虑使用脏标记模式来节省开销。
2,游戏中物体局部变换到世界变换的计算,当没有变化时不需要每帧重复计算。(从根节点沿着它的父链将变换组合起来,矩阵相乘=世界变换)。还有游戏场景图中每帧渲染的对象,对于没有发生变化的对象可以不必重新渲染。再有我们的文档存档也可以用到,内存中就是我们的原始数据,存盘到磁盘就是我们的目标数据,当然不需要实时存盘。
3,若原始数据的变化速度远高于目标数据的使用速度,此时数据会因为随后的修改而失效,此时就不适合使用脏标记模式。
详细请看:
优化模式--脏标记模式_夜色魅影的博客-CSDN博客_脏标记模式理论要点什么是脏标记模式:将工作推迟到必要时进行以避免不必要的工作。就是用一个标志位来标记内容是否发生变化,如果没有发生变化就直接使用缓存数据,不需要重新计算。要点 脏标识模式:当前有一组原始数据随着时间变化而改变。由这些原始数据计算出目标数据需要耗费一定的计算量。这个时候,可以用一个脏标识,来追踪目前的原始数据是否与之前的原始数据保持一致,而此脏标识会在被标记的原始数据改变时改变。那么,若这https://blog.csdn.net/u010223072/article/details/78237737
希望此篇文章可以帮助到更多的同学,此外对现在面临校招的大三大四的同学,以及热爱游戏或者即将面临找工作的朋友,可以点击下方链接,来解决游戏职业道路的种种困惑,并且还可以学习理论知识的同时,拓宽游戏制作的实践技能~
游戏行业大揭秘https://scrm.vipskill.com/CMS/prod/5726/54/home.html?mantisSiteId=175&track_id=__TRACKID__