可以使用Unity UPR对整个项目进行性能分析,找出问题后,再手动优化它们。
Unity UPR网址:https://upr.unity.cn/instructions/desktop
其中Unity UPR中的Asset Checker能对本地的整个Unity项目进行性能分析,帮助我们找出问题。
场景优化:
对始终静止不动的游戏对象使用静态合批技术。
尽量使用同一个材质,以便使用动态合批技术。
使用GPU Instancing技术。
使用遮挡剔除。
进入游戏后的第一个场景要尽量简单,这样可以减少游戏的启动时间。可以先进一个简单的场景,再进行异步加载,之后再进入游戏的主要的场景。
尽量避免Hierarchy窗口的层级结构过深。例如一个物体有很多个子物体,这些子物体又有其它子物体,这些子物体又有其它子物体,继续这样下去就会导致层级结构过深,我们应尽量减少这种情况。
Edit——Project Settings——Quality,可以对不同平台中游戏的品质进行设置。
如果使用了后期处理技术,例如Post Processing等插件,调整屏幕效果的属性,不要使用太绚丽的特效,可以优化性能。
要优化Terrain地形,可以使用Unity资源商店的插件,例如Terrain To Mesh插件可以把地形烘焙成网格。
场景要尽可能简单,尽量多使用预制体,用代码动态创建它们出来,并管理它们。
光照优化
减少光源。
调节好每个Light组件的属性,平衡视觉效果和游戏性能。
尽量不要用实时光照,而是考虑用烘焙光照或者混合光照,此时可以配合光照探针使用。Lighting窗口可以设置烘焙光照的参数。
减少启用的阴影投射。
根据摄像机距离光源的距离,用脚本来决定是否启用光源和阴影。但是这样就会花费一些性能来计算摄像机到光源的距离。
可以考虑设置光照的阴影。无阴影的性能最好,硬阴影的性能稍差,软阴影的视觉效果最好,但是性能是这三者中最差的。
(备注:光照和阴影最影响项目的性能,其次才是模型网格和贴图。把实时光照改成烘焙光照,可以使游戏性能大幅度增加。)
注意MeshRenderer组件上的属性,默认情况下,Unity 会启用阴影投射和接收、光照探针采样、反射探针采样和运动矢量计算。如果项目不需要这些功能中的一个或多个,请确保关闭它们。2D游戏尤其要注意,往往都不需要它们。
远处的景物,如果确定玩家无法到达,则可以不用模型,而是把远处的景物做成一张贴图放到天空盒的材质中,给天空盒使用。也可以使用反射探针烘焙出一张贴图,然后放到天空盒的材质。
图片优化:
如果这张图片是应用在移动端的,则导入Unity前,可以对这张图的每条边进行调整,确保每条边的长度都是2的正整数次方个像素。例如2、4、8、16...256、512、1024、2048、4096...。这个做法只对移动端有效。
图片导入Unity后,可以选中这张图片,在Inspector窗口设置它的属性。设置这些属性,可以在发布不同的平台,分别对该图片进行相应的压缩。可以在合理的范围内减小Max Size,对于许多移动端的游戏,2048x2048 或 1024x1024 足以满足纹理图集的要求,而 512x512 足以满足应用于3D模型的纹理的要求。如果图片不需要读写,则可以取消勾选Read/Write Enabled,如果勾选可能导致双倍的内存占用。Filter Mode一般选择Bilinear即可平衡性能和视觉效果,如果选择Point(no filter),则视觉效果不太行,但性能开销也小,如果选择Trilinera,则视觉效果最好,但性能开销最大。Aniso Level一般选择1,只有个别比较重要的图片才需要设置为大于等于2的值。
图片导入Unity后,会默认生成Mip Maps格式。当摄像机到这幅贴图距离近,则显示最原始的图片,当摄像机距离这幅贴图的距离远,则这幅贴图会变模糊,以此降低渲染的性能消耗。但由于之前显示的一幅图,现在变成了有多幅,所以这样会略微增加内存消耗。如果确定本游戏的摄像机到图片的距离几乎不怎么变化,则可以禁用这个功能。点击该贴图,在Inspector面板的Advanced中取消勾选Generate Mip Maps,这样就不会生成Mip Maps,增加游戏性能。如果是2D游戏则可以禁用这个功能。如果是UI贴图,也可以禁用这个功能。
图片导入Unity后,可以选中这张图片,在Inspector窗口设置它在各个平台的Format和Compressor Quality。Format可以参考官方文档:https://docs.unity3d.com/2021.3/Documentation/Manual/class-TextureImporterOverride.html
用Sprite Atlas把图片打包成图集。但是这样一来,要使用图集中的任意一张图片,都会先加载这整个图集,这样占用的内存会增加。
UI优化:
尽量避免使用IMGUI来做游戏时的UI,因为IMGUI的开销比较大。
如果一个UGUI的控件不需要进行射线检测,则可以取消勾选Raycast Target
尽量避免使用完全透明的图片和UI控件。因为即使完全透明,我们看不见它,但它仍然会产生一定的性能开销。如果UI中一定要用到很多张完全透明的图片,则建议把这些完全透明的图片由单独的摄像机进行渲染,且这些UI不要叠加到场景摄像机的渲染范围内。
尽量避免UI控件的重叠。如果多个UI有重叠的部分,则会稍微增加一些额外的计算和渲染的开销。虽然这部分开销通常是非常小的,但我们最好也尽量避免这种情况。
UI的文字使用TextMeshPro比使用Text的性能更好。但是TextMeshPro对中文的支持不太好。
模型优化:
模型导入Unity后,可以选中这个模型,在Inspector窗口设置它的属性。在Model选项卡,启用Mesh Compression可以压缩模型,压缩程度越高,模型精度越低,但是模型也会节省一些空间。如果该模型不需要用代码来读写,则可以取消勾选Read/Write Enabled。设置Optimize Game Objects可以优化模型。如果该模型不需要使用法线,则可以把Normals设置为None。如果该模型不需要用混合变形法线,则可以把Blend Shape Normals设置为None。如果该模型不需要使用切线,则可以把Tangents设置为None。如果该模型不需要用光照UV贴图,则可以取消勾选Swap UVs和Generate Lightmap UVs。对于Rig选项卡,Animation Type如果选择Generic Rig会比Humanoid Rig性能更好,但是一般使用Humanoid Rig是为了对人型的角色进行动画重定向,所以要根据自己的情况来选择。如果模型不需要使用动画,例如一些完全不会动的石头等物体,则可以将Animation Type选择为None。Skin Weights默认是4,对于一些不重要的动画对象,本变量可以设置为1,这样可以节省计算量。建议勾选Optimize Bones,这样会自动剔除没有蒙皮顶点的骨骼。勾选Optimize Game Object可以提高角色动画的性能,但是在某些情况下可能会导致角色动画出现问题,是否勾选要看动画效果而定。如果角色模型是可以换装的,则在导入该模型后不要勾选这个选项,而可以在游戏运行时,该角色换装后,通过AnimatorUtility.OptimizeTransformHierarchy来勾选这个选项。对于Animation选项卡,如果模型不需要使用动画,则可以取消勾选Import Animation。对于Animation选项卡,设置Anim.Compression可以调整动画的压缩方式,Off表示不压缩动画,这样动画文件可能会占用较大的空间,但是在运行时不会有任何信息损失,Keyframe Reduction表示使用关键帧算法来压缩动画,这样会显著减小动画文件的大小,同时保持相对较高的动画质量,Optimal表示会尽可能高地压缩网格,但是这样也会导致压缩时间增加。对于Materials选项卡,如果使用Untiy的默认材质,则可以把Material Creation Mode设置为None。
Edit——Project Settings——Player——勾选Optimize Mesh Data,这样一来,Unity会在构建的时候中对网格数据进行优化处理,以达到提高游戏性能的效果。但是这样往往会修改网格,我们勾选之后应该要进行测试,确保没有问题,再确定启用它。
用LOD技术,使用Unity自带的LOD Group组件,并根据项目的情况来调整该组件的属性。Untiy资源商店也有一些其它的LOD插件。
把多个模型的网格合并为一个网格。可以使用自己写代码,使用Unity自带的CombineMeshes方法,也可以使用资源商店的插件,在资源商店搜Mesh Combine可以搜索到相关的插件,例如Easy Mesh Combine Tool等插件。
减少模型的顶点、面、材质、骨骼、蒙皮网格。这一般由美术人员来完成。
动画优化:
恰当地设置Animator组件的Culling Mode。Always Animate表示如果该动画不可见,也会播放它。Cull Update Transformations表示如果该动画不可见,则不会渲染该动画,但是依然会根据该动画的播放来改变游戏对象的位置、旋转、缩放,这样是常用的选项。Cull Completely表示完全不会播放该动画,不但不会渲染该动画,而且也不会改变游戏对象的位置、旋转、缩放。
禁用SkinMesh Renderer组件的Update When Offscreen可以让角色在不可见的时候动画不更新,这样可以减少计算量,提升性能。
对于Animator组件,可以使用Animator.StringToHash方法获得指定字符串的哈希值,再把它作为参数传入Animator型对象.GetXXX方法和Animator型对象.SetXXX方法中进行使用。
不用的Animation组件和Animator组件可以考虑删掉,因为只要它们存在,就会消耗性能来检测当前的状态和过渡条件。
一些简单的动画可以使用DoTween、iTween等插件实现,而不需要每个动画都用Animator来实现。
音频优化
Unity支持后缀为.wav、.ogg、.mp3的音频文件,但建议使用.wav,因为Unity对它的支持特别好。注意:Unity在构建项目时总是会自动重新压缩音频文件,因此无需刻意提前压缩一个音频文件再导入Unity,因为这样只会降低该音频文件最终的质量。
把音频文件导入Unity后,选中它,可以在Inspector窗口设置它的属性。勾选Force To Mono,这样就会把这个音频文件设置为单声道。可以节省该资源所占据的空间。因为很少有移动设备实际配备立体声扬声器。在移动平台项目中,将导入的音频剪辑强制设置为单声道会使其内存消耗减半。此设置也适用于没有立体声效果的任何音频,例如大多数UI声音效果。对于Load Type选项,小文件(小于200kb)选择Decompress on Load,中等大小的文件(大于等于200kb)选择Compressed In Memory,比较大的文件(如背景音乐)选择Streaming。对于Compression Format的选项,PCM表示不压缩,Vorbis表示压缩,但也会尽量保证音频的质量,ADPCM表示压缩,且压缩的程度比Vobis更高。由于PCM不会压缩音频,所以占用的空间大,应尽量少用,长时间的音频文件可以使用Vorbis,短时间的音频文件可以使用ADPCM。Sample Rate Setting用于控制音频文件的采样率,对于移动平台,采样率不需要太高,建议选择Override Sample Rate,然后在下方的Sample Rate选择22050Hz,一般这样就够用了。
物理优化:
使用简单的碰撞器进行碰撞检测,如球体碰撞器、盒子碰撞器、胶囊体碰撞器,少用网格碰撞器等复杂的碰撞器。即使用多个简单的碰撞器组合在一起,也往往比使用网格碰撞器的性能要好。
如果要把多个碰撞器组合成一个碰撞器,可以用复合碰撞器。
如果同一个功能既可以用碰撞器来做,也可以用触发器来做,则往往使用触发器来做,性能更好。
尽量减少刚体组件,因为刚体组件的物理计算较多。
如果勾选刚体组件的Is Kinematic,则性能会有所提高。但这样一来,这个刚体只会给别的刚体施加力,自己不会受到别的刚体施加的力的作用。
Edit——Project Settings——Player——勾选Optimization下方的Prebake Collision Meshes,可以提高碰撞的效率,但是构建游戏的时间会增长。
Edit——Project Settings——Physics或者Physics 2D——设置Layer Collision Matrix。它规定了哪些Layer层的游戏对象可以彼此碰撞,哪些Layer层的游戏对象会忽略碰撞。如果有些Layer层的游戏对象之前不需要进行碰撞,则可以在这里设置,取消勾选则表示不会碰撞。
Edit——Project Settings——Time——稍微调大Fixed Timestep,这样可以稍微提升游戏性能,但是物体的运动可能会出现问题。
代码优化:
使用AssetBundle作为资源加载方案。而且经常一起使用的资源可以打在同一个AssetBundle包中。尽量避免同一个资源被打包进多个AB包中。压缩方式尽量使用LZ4,少用或不要用LZMA的压缩方式。如果确定后续开发不会升级Unity版本,则可以尝试启用打包选项BuildAssetBundleOption.DisableWriteType,这样TypeTree信息不会被打到AB包中,可以极大减小包体大小以及运行加载时的内存开销。
使用AssetBundle或者Addressables加载的资源,如果不使用,要记得卸载它们,否则会造成内存泄漏。
不用的资源要释放掉,不用的引用类型的变量也要赋值为null,不要让它们一直占着内存中。
加载资源时尽量使用异步加载。
频繁创建和销毁对象,可以使用对象池。
切换场景时,旧的场景要释放掉,不用的资源也可以考虑释放掉,也可以考虑用System.GC.Collect来进行一次垃圾回收。
锁定游戏的帧率 。帧率为30,游戏会明显卡顿,但是对于手游来说,消耗手机的电量比较少。帧率为45,游戏有一点点卡,但还凑合,消耗电量中等。帧率为60,游戏很流畅,但消耗手机的电量会比较多。可以用Application.targetFrameRate来锁定帧率,也可以用UnityEngine.Rendering命名空间中的OnDemandRendering.renderFrameInterval来锁定帧率。
尽量少用foreach语句,可以改为for语句。因为每次使用foreach语句会造成微量的内存垃圾。
要判断GameObject型对象.tag是不是某个标签,使用GameObject型对象.CompareTag方法会更高效。
尽量少用GameObject.Find方法和Object.FindObjectOfType方法来查找游戏对象,可以提前把要查找的游戏对象存储在变量、列表、字典等容器中,方便查找。也可以用GameObject.FindGameObjectWithTag方法来查找游戏对象。
在UI显示字符串的时候,如果一些内容是固定的,我们可以把它拆分开来,这样可以减少使用+号来拼接的次数,减少内存垃圾的产生。例如“杀敌数:999”,其中“杀敌数:”是固定的,冒号后面的数字才是会变的,那么我们可以用两个Text组件分别记录它们,改变的时候只改变冒号后面的数字。
频繁对字符串赋新的值,或者频繁拼接字符串的时候,可以使用StringBuilder代替string
如果要频繁操作某脚本,不要每次都用GetComponent方法来获取这些脚本。可以用一个变量存储起获得的这个脚本,之后要访问它,就直接访问这个变量即可。也可以考虑在生命周期方法Awake或者Start中声明变量来存储,之后访问这个变量即可。
尽量少用正则表达式。虽然正则表达式的形式看上去比较简便,但是使用它会造成一定的性能消耗,且会产生内存垃圾。
尽量少用LINQ语法,因为每次使用LINQ都会产生一定量的内存垃圾。
尽量少用Camera.main来访问主摄像机,因为每次访问它,实际上Unity都是从场景中查找它的。可以声明一个变量存储它,在生命周期方法Awake或Start中获取主摄像机的应用。
在Animator、Shader中使用Get方法和Set方法时,不传入字符串作为参数,而是传入哈希值。例如Animator组件可以使用Animator.StringToHash方法获得指定字符串的哈希值,再把它作为参数传入Animator组件的Get方法或Set方法中进行使用。例如Shader,则可以用Shader.PropertyToID方法来获取指定属性的ID
使用非分配物理API。例如使用Physics.RaycastNonAlloc方法代替Physics.RaycastAll方法,使用Physics.SphereCastNonAlloc方法代替Physics.SphereCastAll方法,以此类推。Physics2D类也有类似的方法。
一般情况下,整数的数学运算比浮点数的数学运算效率高,浮点数的数学运算比矢量的数学运算效率高。可以灵活运用数学的加法交换律、加法结合律、乘法交换律、乘法结合律,在保证结果不变的前提下,调整运算顺序,减少浮点数的数学运算和矢量的数学运算。
使用高效的算法进行计算
每次执行Debug.Log来打印信息会消耗极少量的性能,如果要在游戏正式发布之后不执行某些Debug.Log的语句,但又不想把这些代码删掉,则可以使用宏来禁止在游戏正式发布之后执行Deubg.Log的语句。例如使用#if语句或者Conditional特性。
尽量减少在生命周期方法Update、FixedUpdate、LateUpdate中的逻辑。其中有些不需要频繁执行的逻辑,可以使用协程或者Invoke方法,每隔指定的秒数执行一次或每隔指定的帧数执行一次。
尽量避免频繁的装箱拆箱操作。也可以使用泛型,这样就能避免装箱拆箱。但是要注意,Lua热更新对泛型的支持不太好。
如果物体身上添加了CharacterController组件,则尽量用CharacterController组件的方法来移动它,而不是用Transform类的方法来移动它。同理,如果物体身上添加了刚体组件,则应尽量用刚体组件的方法来移动它,而不是用Transform类的方法来移动它。
应尽量避免DontDestroyOnLoad中加载的资源过多,因为它在切换场景的时候不会被释放,声明的变量以及加载的资源会一直占用着内存。我们可以考虑把一些不用的资源释放掉,需要的时候再加载它。
不使用的组件可以删掉,这样可以节省一些内存。常见的有AudioSource组件、Animator组件、Animation组件等。
写一个类继承AssetPostProcessor,然后定义里面特定的方法,以此来自动设置资源导入Unity之后的属性。
尽量避免闭包。因为闭包会产生额外的内存开销。
Shader优化:
修改Shader的代码,或者自定义一个Shader
修改渲染管线的源码,改成符合自己项目的渲染管线,或者自定义渲染管线。