【Unity】常见性能优化

1 前言

        本文将介绍下常用的Unity自带的常用优化工具,并介绍部分常用优化方法。都是比较基础的内容。

2 界面

2.1 Statistics窗口

可以简单查看Unity运行时的统计数据,当前一帧的性能数据。

2.1.1 Audio

音频相关内容。

  • Level:音量大小,单位是分贝(dB)。
  • Clipping:表示音频的剪裁情况。当音频信号超过设备支出的最大范围时,音频信号会被裁剪。被裁剪的音频会出现一定程度的失真。
  • Dsp load:表示数字信号处理器负载。播放的声音数量越多、声音采样率越大,负载值将会升高。
  • Stream load:表示音频流的负载情况。音频加载方式不是一次性加载全部数据,而是以流的形式加载,流式加载是指以持续的方式从音频源获取音频数据。流式加载的优点是可以实时的处理和播放音频,而无需等待全部数据加载完成。应避免此数值过大。

2.1.2 Graphics

图像相关内容。

  • FPS:帧数。例如“300(3.6ms)”表示平均每秒播放300张画面,平均每3.6ms播放一张画面。
  • CPU:表示CPU处理一帧的时间。例如“main 6.1ms render thread 0.3ms”表示主线程处理这一帧花费了6.1ms,渲染线程处理这一帧花费了0.3ms。
  • Batches:表示处理的绘制调用(Draw Call,一个Draw Call就是CPU向GPU发送的一组绘制指令,可以绘制出一个或多个物体)批次总数。此值应小。
  • Save by batching:表示有多少个绘制调用(Draw Call)被合并到了批次。
  • Tris:表示当前摄像机视椎体范围内的三角面个数。
  • Verts:表示当前摄像机视椎体范围内的顶点个数。(在Unity中,一个Cube有24个顶点,每个面有4个顶点)
  • Screen:屏幕分辨率。如“1920x1080-23.7MB”表示尺寸与占用内存大小。
  • SetPass calls:表示在当前摄像机的渲染过程中,Unity切换着色器通道(Shader Pass)来渲染游戏对象的次数。一个着色器(Shader)可以包含多个着色器通道,每个着色器通道可以通过不同的方式来渲染游戏对象。但每次切换着色器通道都会消耗一定的性能。
  • Shadow casters:表示摄像机画面中有多少个游戏对象产生了阴影。同一个游戏对象产生较多的阴影,可能会被算作多个Shadow casters。
  • Visible skinned meshes:表示当前摄像机中有多少个可见的蒙皮网格。网格用来定义一个模型的形状、大小和表面细节等信息,模型的所有顶点、线、面共同构成了这个模型的网格。蒙皮网格是一个与骨骼绑定的网格,这个网格可以发生形变和做出各种动作。一个网格在没有蒙皮之前是不能发生形变的,也不能做出各种动作的。但是在成功蒙皮之后,这个网格就可以发生形变和做出各种动作。
  • Animation components playing:表示当前场景中有多少个Animator组件正在播放动画。播放动画会消耗性能。
  • Animator components playing:表示当前场景中有多少个Animation组件正在播放动画。播放动画会消耗性能。

PS:没用的Animator组件和Animation组件可以考虑删掉。因为即使只有空的动画,Animator组件和Animation组件也会根据自己的工作流程进行每帧的计算和更新,以检查当前动画状态和过渡条件,这样就会消耗不必要的性能。

2.1.3 代码中获取窗口信息

using UnityEditor;//编辑器头文件
using UnityEngine;public class NewBehaviourScript : MonoBehaviour
{private void Update(){//使用UnityStats类访问相关数据Debug.Log(UnityStats.vertices);}
}

2.2 Profiler窗口

性能分析器的窗口,用来详细分析游戏性能。

2.2.1 打开窗口

打开Profiler窗口的方法:

  1. Window——Analysis——Profiler
  2. Ctrl+7

2.2.2 自定义模块

如图所示,显示的是性能模块,分别显示了各类性能条目。我们也可以自定义这块内容。

点击后如图:

可以勾选来选择哪些显示、哪些隐藏,右下角可以恢复默认状态,左下角小齿轮则是设置,点击它。

图上已经标明相关操作了,除了对已有模块的排序,其他操作都是针对我们创建的新的自定义模块的。

2.2.3 收集信息

Play Mode需要在运行状态下收集,Edit Mode在编辑器状态下收集。另外,开始收集后再次点击圆形按钮将会中值收集。

2.2.4 保存与读取信息

可以保存为二进制文件,后续可以读取这个二进制文件。

2.2.5 深度分析

选中后即可开启深度分析,在收集数据,并选择观察某一帧的数据时,里面会包含脚本函数的性能分析数据。一般是不开的,因为若脚本过多的话,深度分析会收集过多的数据,导致分析器过卡。

2.2.6 对一段代码进行性能分析

        深度分析是分析所有脚本的函数,不能仅仅只收集某一个函数的。这里还有两个方法可以针对性的收集某一段代码的性能,方便我们使用。

代码:

using UnityEngine;
using UnityEngine.Profiling;//引用头文件public class NewBehaviourScript : MonoBehaviour
{private void Update(){//使用BeginSample和EndSample包裹一段代码,这段代码便可被性能分析器收集到Profiler.BeginSample("MyProfilerTest HHHHHHH");//参数就是下面那段代码,在性能分析器中的名称。(建议不要写中文名)for (int i = 0; i < 10000; i++){int a = 149;a += 1;}Profiler.EndSample();}
}

然后在性能分析器中收集,再查询:

可以看到我们之前的代码段就可以被搜索到了。

2.2.7 调用栈

        调用栈(Call Stack)是计算机程序在执行过程中记录函数调用的一种数据结构。
        调用栈是一个栈结构,即先进后出。它用于记录程序执行过程中,每个函数被调用的情况。
        当一个函数被调用时,它的相关信息,如函数名、参数、返回地址等,会被添加进调用栈中。当该函数执行完成后,相应的信息会从调用栈中移除。通过不断添加和移除函数调用的信息,调用栈就记录了程序执行的顺序。
        调用栈对于程序调试和分析非常有用。当程序出现错误或异常时,可以通过查看调用栈来确定错误发生的位置和函数调用的顺序。调试器通常会显示当前调用栈的信息,以帮助开发人员查看函数的执行过程并找出发生异常的原因。

选中Deep Profile右侧的Call Stacks按钮,这样在收集性能数据的时候,每一帧都会记录方法的的调用栈信息,这里的方法指三个方法,点击右侧倒置小三角可以看到它们:GC.Alloc、UnsafeUtility.Malloc、JobHandle.Complete。其是Unity的方法,启用Call Stacks且勾选它们后,如果Unity有调用它们,则可以在Hierarchy或Raw Hierarchy右侧的搜索框中搜索到它们,这样就可以查看它们的性能了。

  • GC.Alloc表示GC的内存分配情况。
  • UnsafeUtility.Malloc(Persistent)用于在内存中分配指定大小的未初始化内存块。这个方法会直接在堆上分配内存,并可以绕过自动内存管理功能,需要手动管理内存的生命周期和释放。一般情况下,只有在处理非托管内存的特定场景下才会使用UnsafeUtility.Malloc方法。
  • JobHandle.Complete表示Job的完成情况。这里的Job是指Unity的Job System的一组特定的任务。

2.2.8 CPU Usage模块

        下方窗口可以选择Timeline、Hierarchy、Raw Hierarchy。
        选择Timeline,可以通过时间轴的方式查看这一帧中CPU依次干了什么。
        选择Hierarchy,可以查看CPU在这一帧中做的事情所消耗的性能和所花费的时间。Total表示一共占用了CPU使用情况的百分之几。Self表示自身的代码占用了CPU使用情况的百分之几,调用其它方法的代码不算在内的。Calls表示被调用了几次。GC Alloc表示GC分配的内存,当一个对象被释放后,它GC分配的内存不会马上被回收,所有GC分配的内存的总量达到一定程度,会触发GC,此时垃圾回收器才会把这些内存回收,不过同时也会造成游戏卡一下。Time ms表示一共耗时多少毫秒。Self ms表示表示自身的代码耗时多少毫秒,调用其它方法的代码不算在内的。
        选择Raw Hierarchy比起Hierarchy会单独列出更多信息,Hierarchy实际上是把这些信息合并了。

2.2.9 独立的Profiler窗口

其功能和Profiler基本一致,只是其是独立运行的性能分析器,不在Unity中运行,分析的内容中也不会包含自己(性能分析器)的性能数据。

2.3 Frame Debugger窗口

        Frame Debugger窗口也叫帧调试器窗口,用于查看每一帧的画面是如何渲染出来的,可以详细查看这一帧的绘制过程。

2.3.1 打开窗口

2.3.2 使用

        按下“Enable”(窗口左上角),则会启动帧调试,此时如果运行了游戏,则会自动暂停,然后当前这帧的渲染情况可以在这个窗口中查看。

上面拖动调中最大数值表示了绘制过程中有多少步,可以查看下一步或上一步。如果要禁用帧调试,可以按下“Disable”。用Frame Debugger窗口查看当前一帧的每一步时,可以配合Stats窗口使用,以此来确定哪一个物体造成的性能开销较大。绘制的步骤越少,性能越好。Frame Debugger窗口也能看到每一帧的Shader信息,但是需要有一定Shader基础才能看懂。

PS:

        大多数平台都支持帧调试器的使用,可以用手机的数据线成功连接到电脑,在手机上运行Unity的游戏,Frame Debugger窗口中会多出该手机设备供我们选择,我们就可以分析该手机设备上运行的Unity项目的性能。也可以让手机和电脑都连接同一个wifi,这样一来,Frame Debugger窗口中也会多出该手机设备供我们选择,我们就可以分析该手机设备上运行的Unity项目的性能。
        但是要注意,构建时必须在Project Settings窗口中勾选“Development Build”。而且有些平台可能不支持Frame Debugger的使用,例如WebGL平台。

2.4 Memory Profiler窗口

2.4.1 打开窗口

        在Profiler中打开:

安装后,安装按钮便变为了打开按钮,点击即可。

2.4.2 使用

        具体的使用方式是“内存快照”。点击“Captrue New Snapshot”,就会捕捉当前内存情况。非运行状态下捕捉的是编辑器状态下的内容,运行状态下捕捉的是运行下的内容。内存快照中将包含当前内存中的所有信息,所以存在泄漏重要内容的可能性,这点要注意。快照会被默认保存到与项目Assets文件夹同级的MemoryCaptures文件夹中,后续可再次打开旧快照。另外,保存路径在窗口右上角的设置中可以修改。

点击快照,即可看到详细数据。

窗口内提供了对比功能,可以拍多张快照进行对比,查询两个瞬间下内存的变化,通常用来查找内存泄露。后续自己可以研究下里面的内容,这里就不多说了。

2.5 其余窗口

        在Window→Analysis中还有一些其他窗口,使用上可能没有那么频繁,具体内容直接从官方文档中学习吧。

3 常用方法

3.1 静态合批

        静态合批也叫静态批处理,是Unity的一种优化技术。对于始终静止不动的物体使用静态合批后,CPU会把它们合并为一个批次发送给GPU处理,这样可以减少Draw Call带来的性能消耗,从而提升游戏性能。

3.1.1 使用

        要使用静态合批,必须确保Edit——Project Settings——Player——Other Settings——Static Batching是勾选的。

把一个物体设置为静态的方法:选中该物体,点击在Inspector窗口右上角的Static右方的下拉菜单,选择Batching Static。

使用静态合批虽然可以提升游戏性能,但是设置为静态的物体在整个游戏中就不能再运动了,强行使它们运动会出问题。

3.1.2 注意事项

        即使按照以上步骤进行了静态合批,也不一定保证会成功,必须满足以下全部条件,静态合批才会成功:

  1. 游戏对象处于激活状态。
  2. 游戏对象有一个Mesh Filter组件,并且该组件已启用。
  3. Mesh Filter组件具有对网格的引用。
  4. 网格已启用Read/Write功能。
  5. 网格的顶点计数大于0。
  6. 该网格尚未与另一个网格组合。
  7. 游戏对象有一个Mesh Renderer组件,并且该组件已启用。
  8. 网格渲染器组件不将任何材质与DisableBatching标记设置为true的着色器一起使用。
  9. 要批处理在一起的网格使用相同的顶点属性。例如,Unity可以将使用顶点位置、顶点法线和一个UV的网格与另一个UV进行批处理,但不能将使用顶点定位、顶点法线、UV0、UV1和顶点切线的网格进行批处理。

        即使静态合批成功,合出来的每个批次可以包含的网格顶点数是有限的,最多是64000个顶点。如果超过这个数,则会创建到另一个批次中。

3.1.3 查看静态合批效果

        在Profiler中查看,如下图所示:

选中Rendering后,下方窗口会显示详细数据,红色框住的地方可以查看合批的效果,主要看中间那个Static Batching即可,若后面有数值表示进行了合批。另外说下,Dynamic Batching是动态合批,Instancing是GPU Instancing,这些后面会讲,看合批效果也是在这看。还有,Batched Draw Calls是参于合批的Draw Calls数,Batches则是参与合批的Batches数,后面两个是三角面数与顶点数。

3.1.4在运行时静态合批

        如果要在游戏运行时进行静态合批,则可以使用StaticBatchingUtility类的Combine方法。

using UnityEngine;public class NewBehaviourScript : MonoBehaviour
{public GameObject m_obj;public GameObject[] m_objArray;private void Start(){//对指定的根物体的所有子孙物体进行静态合批。StaticBatchingUtility.Combine(m_obj);//对指定的游戏对象进行静态合批,并指定它们静态合批的根物体。这里数组是静态合批的对象,m_obj是根物体。StaticBatchingUtility.Combine(m_objArray, m_obj);//对于以上两个://只有当它们符合静态合批的所有条件,静态合批才会成功。//成功之后,这些物体就不能再运动了,强行运动会出问题。但是该根物体仍然允许运动。}
}

3.2 动态合批

        动态合批也叫动态批处理,是Unity的一种优化技术。对移动的物体使用动态合批后,则Unity不会一个个绘制它们,而是把它们合并为一个批次(Batch),再由CPU把它们一次性提交给GPU进行处理,这样可以减少Draw Call带来的性能消耗,从而提高性能。

3.2.1 使用

        动态合批默认是由Unity自动完成。可以在Edit——Project Settings——Player——Other Settings——Dynamic Batching查看。默认Dynamic Batching是勾选的,当条件满足时,Unity会自动对使用了相同材质(Material)的物体进行动态合批。如果取消勾选,则不会进行动态合批。

3.2.2 注意事项

        即使勾选了Dynamic Batching,也必须同时满足以下条件,动态合批才会成功:

  1. Unity不能对包含超过900个顶点属性和225个顶点的网格应用动态批处理。这是因为网格的动态批处理对每个顶点都有开销。例如,如果你的着色器使用顶点位置、顶点法线和单个UV,那么Unity最多可以批处理225个顶点。然而,如果你的着色器使用顶点位置、顶点法线、UV0、UV1和顶点切线,那么Unity只能批处理180个顶点。
  2. 如果GameObjects使用不同的材质实例,Unity就不能将它们批处理在一起,即使它们本质上是相同的。唯一的例外是阴影施法者的渲染。
  3. 带有光贴图的游戏对象有额外的渲染参数。这意味着,如果你想批处理光照贴图的游戏对象,它们必须指向相同的光照贴图位置。
  4. Unity不能完全将动态批处理应用于使用多通道着色器的GameObjects。

        几乎所有的Unity着色器都支持正向渲染中的多个光源。为了实现这一点,他们为每个光处理一个额外的渲染通道。Unity只批处理第一个渲染通道。它不能批处理额外的逐像素灯光的绘制调用。
        遗留延迟渲染路径不支持动态批处理,因为它在两个渲染通道中绘制GameObjects。第一个通道是灯光预通道,第二个通道渲染GameObjects。
        其中我们要注意的是,物体必须使用相同的材质,才有可能成功进行动态合批。
        使用动态合批往往能减少CPU和GPU的开销,提升游戏性能,但同时也会占用一定的内存。
        是否要开启动态合批,要根据自己的项目来定。可以尝试启用,在性能分析器中看看效果如果,如果效果好,再确定启用它。

3.2.3 查看动态合批效果

        请查看3.1.3。

3.3 GPU Instancing

        Instancing是Unity的一种优化技术。使用GPU Instancing可以在一个Draw Call中同时渲染多个相同或类似的物体,从而减少CPU和GPU的开销。

3.3.1 使用

        要启用GPU Instancing,我们可以选中一个材质,然后在Inspector窗口勾选Enable GPU Instancing,这样就可以了。

3.3.2 注意事项

        要成功使用GPU Instancing进行优化,游戏对象必须同时满足以下条件:

  1. 使用相同的材质和网格。
  2. 材质的着色器必须支持GPU Instancing。例如标准着色器和表面着色器就支持GPU Instancing。
  3. 网格的顶点布局和着色器必须相同。如果网格的顶点布局或着色器不同,那么它们就无法被合并成一个实例。
  4. 每个实例需要有不同的变换信息(例如位置、旋转、缩放)。虽然多个实例可以使用相同的材质和网格,但是它们必须拥有不同的变换信息才能被正确地实例化并渲染出来。

        另外需要注意的是,GPU Instancing与SRP Batcher不兼容。如果项目使用了SRP Batcher,并且配置为优先使用SRP Batcher而不是GPU实例化,启用GPU实例化可能不会生效。SRP Batcher是Unity提供的一种渲染优化技术,它可以将多个网格合并成单个批次进行渲染,从而提高性能。在这种情况下,GPU实例化将被忽略。

        使用GPU Instancing往往能减少CPU和GPU的开销,提升游戏性能,但同时也会占用一定的内存。
        是否要启用GPU Instancing,要根据自己的项目来定。可以尝试启用,在性能分析器中看看效果如果,如果效果好,再确定启用它。
        一般来说,当场景中有大量重复的网格实例时,可以尝试启用GPU Instancing。例如场景中有大量树木、草地、石块等,这些实例具有相同的网格和材质,只是位置、颜色等属性稍有差异,那么启用GPU Instancing或许能够显著提高性能。

3.3.3 查看效果

        请查看3.1.3。

3.4 遮挡剔除

        正常情况下,如果一个障碍物A挡住了后面的物体B,虽然我们看不见物体B,但是Unity仍然会消耗性能来渲染这个物体B。这样CPU和GPU就会有一部分性能白白浪费在渲染物体B身上。如果想在一个障碍物挡住了后面的物体后,不渲染被挡住的物体,则可以使用遮挡剔除。

3.4.1 使用

        以一堵墙挡住几个小球为例,选中这堵墙,在Inspector窗口右上角的Static右侧的下拉菜单处选择Occluder Static,则这堵墙就是遮挡物。分别选中这些小球,在Inspector窗口右上角的Static右侧的下拉菜单处选择Occludee Static,则这些小球就是被遮挡物。对于一个物体,两个标签都可以勾选,这样它既可以遮挡剔除别的物体,也可以被别的物体遮挡剔除。有时候被遮挡物只勾Occludee Static,烘焙之后可能看不出遮挡剔除的效果,建议把Occluder Static也勾上,再重新烘焙,或许就能看出效果了。注意,无论是勾选了Occluder Static还是Occludee Static,勾选后物体就无法运动了。

选中摄像机,要确保它启用了Occlusion Culling属性。

设置完之后,要创建一个遮挡区域,当摄像机处于这个遮挡区域中,遮挡剔除才会生效。创建遮挡区域的方法:

  1. 打开Occlusion Culling窗口。打开方法:Window——Rendering——Occlusion Culling——Bake。打开之后,选择Object选项卡,点击Occlusion Areas,点击Create New右侧的Occlusion Area。
  2. 创建一个空物体,在它身上添加Occlusion Area组件。

第一种:

新创建的区域对象如下:

Occlusion Area组件的Size决定了遮挡剔除区域的范围,它越大,烘焙之后生成的遮挡剔除区域就越大。Center控制遮挡区域中心点的世界坐标。Is View Volume表示是否定义视图体积,只有启用了这个选项,Occlusion Area组件才可能生效。调整区域大小:

之后,要让遮挡剔除生效,还要在Occlusion Culling窗口的Bake选项卡中点击右下方的Bake按钮,进行烘焙,遮挡剔除才可能生效。而且以后每次调整完场景的遮挡物、被遮挡物、Occlusion Area组件的范围,都要这样烘焙一次。如果点击Bake按钮旁边的Clear按钮,则会清除之前烘焙的数据。对场景进行烘焙:

黄色的框的范围才是最终的相机应该所在的范围。其大小跟前面的Occlusion Area组件的范围可能不一致。

烘焙完之后,当摄像机在黄框范围内,则被遮挡的物体不会被渲染。遮挡物实际上遮挡了摄像机视锥体的范围,只要物体完全没有出现在摄像机视锥体的范围内,则都不会被渲染。但是一旦物体的任意一小部分暴露在了摄像机视锥体的范围内,则这个物体整个会被渲染出来。 当摄像机移出了黄框范围,则遮挡剔除会失效。

3.4.2 注意事项

  1. 无论是勾选了Occluder Static还是Occludee Static,勾选后物体就无法运动了。
  2. 每次调整完场景的遮挡物、被遮挡物、Occlusion Area组件的范围,都要重新烘焙。
  3. 进行遮挡剔除的烘焙时,不会烘焙动态的游戏对象的信息到遮挡剔除的数据中。动态的游戏对象只能作为被遮挡物,而不能充当遮挡物。要让动态的游戏对象成为遮挡剔除中的被遮挡物,可以选中它,启用它身上Mesh Renderer组件身上的Dynamic Occlusion属性。
  4. 使用Occlusion Portal组件也可以实现遮挡剔除。

3.4.3 Occlusion Portal

        使用Occlusion Portal组件也可以实现遮挡剔除。
        例如可以在一堵墙上添加Occlusion Portal组件。而且这堵墙不要不勾选Inspector窗口右上角下拉菜单的Occluder Static和Occludee Static,但是被它遮挡的物体仍然要勾选Occluder Static和Occludee Static。
        设置好之后,打开Occlusion Culling窗口,在Bake选项卡进行烘焙。
        这样一来,Occlusion Portal组件就会生效。当取消勾选它的Open属性后,被这堵墙就会使用遮挡剔除。当勾选它的Open属性后,被这堵墙就不会使用遮挡剔除。我们可以使用代码来控制Open属性,控制这堵墙在什么时候使用遮挡剔除,什么时候不使用遮挡剔除。
        Occlusion Portal组件的Center属性控制了中心的位置,Size属性控制了遮挡的范围。点击Edit Bounds左侧的按钮后,可以在Scene窗口手动调节Occlusion Portal组件的遮挡范围。每次调整完,或者修改过场景,都要重新烘焙。

PS:挂载Occlusion Portal的墙是可以移动的,但是遮挡位置是不能移动的,即移动了墙后,原来的位置虽然空了,但起遮挡作用的“墙”依旧在原来的位置。

3.5 场景优化思路

  1. 对始终静止不动的游戏对象使用静态合批技术。
  2. 尽量使用同一个材质,以便使用动态合批技术。
  3. 使用GPU Instancing技术。
  4. 使用遮挡剔除。
  5. 进入游戏后的第一个场景要尽量简单,这样可以减少游戏的启动时间。可以先进一个简单的场景,再进行异步加载,之后再进入游戏的主要的场景。
  6. 尽量避免Hierarchy窗口的层级结构过深。例如一个物体有很多个子物体,这些子物体又有其它子物体,这些子物体又有其它子物体,继续这样下去就会导致层级结构过深,我们应尽量减少这种情况。
  7. Edit——Project Settings——Quality,可以对不同平台中游戏的品质进行设置。
  8. 如果使用了后期处理技术,例如Post Processing等插件,调整屏幕效果的属性,不要使用太绚丽的特效,可以优化性能。
  9. 要优化Terrain地形,可以使用Unity资源商店的插件,例如Terrain To Mesh插件可以把地形烘焙成网格。
  10. 场景要尽可能简单,尽量多使用预制体,用代码动态创建它们出来,并管理它们。

3.6 光照优化思路

  1. 减少光源。
  2. 调节好每个Light组件的属性,平衡视觉效果和游戏性能。
  3. 尽量不要用实时光照,而是考虑用烘焙光照或者混合光照,此时可以配合光照探针使用。Lighting窗口可以设置烘焙光照的参数。
  4. 减少启用的阴影投射。
  5. 根据摄像机距离光源的距离,用脚本来决定是否启用光源和阴影。但是这样就会花费一些性能来计算摄像机到光源的距离。
  6. 可以考虑设置光照的阴影。无阴影的性能最好,硬阴影的性能稍差,软阴影的视觉效果最好,但是性能是这三者中最差的。
  7. 注意MeshRenderer组件上的属性,默认情况下,Unity 会启用阴影投射和接收、光照探针采样、反射探针采样和运动矢量计算。如果项目不需要这些功能中的一个或多个,请确保关闭它们。2D游戏尤其要注意,往往都不需要它们。
  8. 远处的景物,如果确定玩家无法到达,则可以不用模型,而是把远处的景物做成一张贴图放到天空盒的材质中,给天空盒使用。也可以使用反射探针烘焙出一张贴图,然后放到天空盒的材质。

PS:光照和阴影最影响项目的性能,其次才是模型网格和贴图。把实时光照改成烘焙光照,可以使游戏性能大幅度增加。

3.7 图像优化思路

3.7.1 图像尺寸

        如果这张图片是应用在移动端的,则导入Unity前,可以对这张图的每条边进行调整,确保每条边的长度都是2的正整数次方个像素。例如2、4、8、16...256、512、1024、2048、4096...。这个做法只对移动端有效。

3.7.2 图像属性设置

  • 图片导入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可以参考官方文档:Unity - Manual: Recommended, default, and supported texture formats, by platform

3.7.3 精灵图集

        用Sprite Atlas用于制作图集,把多张图片打包成一整张图集。

3.7.3.1 优点
  1. 减少资源文件所占大小。
  2. 打包到同一个Sprite Atlas图集中的图片会共享相同的纹理数据,减少了重复的存储,往往能起到优化内存的作用。
  3. 可以减少DrawCall,提高游戏的渲染性能。
3.7.3.2 缺点
  1. 需要手动将图片添加到Sprite Atlas,增加额外的工作量。
  2. 当加载图集中的任意一张图,则会先加载整个图集。如果把一些不常用的图打包到同一个图集中,则会浪费内存。
3.7.3.3 安装插件

        从Window→PackageManger中安装2D Sprite插件,才能使用Sprite Atlas。

3.7.3.4 使用图集

        在安装完插件后,可以在Project窗口中创建图集:

创建好图集后,选中它,Inspector面板如图所示:

红框中为我们添加图片的地方,但在添加之前注意下面的叹号提示,我们需要提前做一些设置,才能正常使用图集。去Project Settings中进行设置,如图:

V1、V2是两种不同的版本,选V1就好。之后返回图集数据界面去添加图像,添加图像的方式也有多种,如下(添加图像需要为Sprite类型):

  1. 点击“+”号后,从弹出的窗口中选择图像。
  2. 从Project中拖拽一个图像放到“Objects for Packing”名字上松开。
  3. 从Project中拖拽一个文件夹放到“Objects for Packing”名字上松开,其中的所有图像都会被添加其中。

添加后,点击打包:

可以看到,两张图像被打包到了一起。

3.7.3.5 使用代码加载图集

代码:

using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;public class NewBehaviourScript : MonoBehaviour
{//精灵图集对象private SpriteAtlas m_mySpriteAtlas;//拖拽进来的图像组件,用于展示图像public Image m_image;private void Start(){//加载精灵图集m_mySpriteAtlas = Resources.Load<SpriteAtlas>("MySpriteAtlas");}private void Update(){//按下键盘Q键if (Input.GetKeyDown(KeyCode.Q)) {//从图集中获取名称为“1”的图像,并赋值给Image组件m_image.sprite =  m_mySpriteAtlas.GetSprite("1");//设置图像为原始尺寸m_image.SetNativeSize();}//按下键盘E键if (Input.GetKeyDown(KeyCode.E)){//从图集中获取名称为“testd”的图像,并赋值给Image组件m_image.sprite = m_mySpriteAtlas.GetSprite("testd");//设置图像为原始尺寸m_image.SetNativeSize();}}
}

演示:

如图,按下Q和E键,从图集中加载不同的图像进行展示。

3.8 UI优化思路

  1. 尽量避免使用IMGUI来做游戏时的UI,因为IMGUI的开销比较大。
  2. 如果一个UGUI的控件不需要进行射线检测,则可以取消勾选UI组件上的Raycast Target。
  3. 尽量避免使用完全透明的图片和UI控件。因为即使完全透明,我们看不见它,但它仍然会产生一定的性能开销。如果UI中一定要用到很多张完全透明的图片,则建议把这些完全透明的图片由单独的摄像机进行渲染,且这些UI不要叠加到场景摄像机的渲染范围内。
  4. 尽量避免UI控件的重叠。如果多个UI有重叠的部分,则会稍微增加一些额外的计算和渲染的开销。虽然这部分开销通常是非常小的,但我们最好也尽量避免这种情况。
  5. UI的文字使用TextMeshPro比使用Text的性能更好。但是TextMeshPro对中文的支持不太好。

3.9 模型优化思路

3.9.1 Model选项卡

  1. 模型导入Unity后,可以选中这个模型,在Inspector窗口设置它的属性。在Model选项卡,启用Mesh Compression可以压缩模型,压缩程度越高,模型精度越低,但是模型也会节省一些空间。
  2. 如果该模型不需要用代码来读写,则可以取消勾选Read/Write Enabled。
  3. 设置Optimize Game Objects可以优化模型。
  4. 如果该模型不需要使用法线,则可以把Normals设置为None。
  5. 如果该模型不需要用混合变形法线,则可以把Blend Shape Normals设置为None。
  6. 如果该模型不需要使用切线,则可以把Tangents设置为None。
  7. 如果该模型不需要用光照UV贴图,则可以取消勾选Swap UVs和Generate Lightmap UVs。

3.9.2 Rig选项卡

  1. Animation Type如果选择Generic Rig会比Humanoid Rig性能更好,但是一般使用Humanoid Rig是为了对人型的角色进行动画重定向,所以要根据自己的情况来选择。如果模型不需要使用动画,例如一些完全不会动的石头等物体,则可以将Animation Type选择为None。
  2. Skin Weights默认是4,对于一些不重要的动画对象,本变量可以设置为1,这样可以节省计算量。
  3. 建议勾选Strip Bones,这样会自动剔除没有蒙皮顶点的骨骼。
  4. 勾选Optimize Game Object可以提高角色动画的性能,但是在某些情况下可能会导致角色动画出现问题,是否勾选要看动画效果而定。如果角色模型是可以换装的,则在导入该模型后不要勾选这个选项,而可以在游戏运行时,该角色换装后,通过AnimatorUtility.OptimizeTransformHierarchy来勾选这个选项。

3.9.3 Animation

  1. 如果模型不需要使用动画,则可以取消勾选Import Animation。
  2. 设置Anim.Compression可以调整动画的压缩方式,Off表示不压缩动画,这样动画文件可能会占用较大的空间,但是在运行时不会有任何信息损失,Keyframe Reduction表示使用关键帧算法来压缩动画,这样会显著减小动画文件的大小,同时保持相对较高的动画质量,Optimal表示会尽可能高地压缩网格,但是这样也会导致压缩时间增加。

3.9.4 Materials

        如果使用Untiy的默认材质,则可以把Material Creation Mode设置为None。

3.9.5 项目设置

        Edit——Project Settings——Player——勾选Optimize Mesh Data,这样一来,Unity会在构建的时候中对网格数据进行优化处理,以达到提高游戏性能的效果。但是这样往往会修改网格,我们勾选之后应该要进行测试,确保没有问题,再确定启用它。

3.9.6 其他

  1. LOD技术。
  2. 合并网格。
  3. 减少模型的顶点、面、材质、骨骼、蒙皮网格(这一般由美术人员来完成)。

3.10 Lod

        LOD是Level of Detail的简称,意思是细节层次,它是一种优化技术。LOD的原理,就是我们可以为一个游戏对象设定多个模型,这些模型消耗的游戏性能由高到低排列。会根据摄像机距离模型的远近自动显示对应的模型。近的时候显示最精细的模型,距离中等的时候显示没那么精细的模型,远的时候显示粗糙的模型,最远的时候可以隐藏该模型。使用LOD技术能起到优化渲染性能的效果。但是使用LOD技术也会增加内存占用。

3.10.1 使用

        在Unity中可以使用LODGroup组件来实现LOD技术,当然资源商店也有LOD的插件,这里只讲Unity自带的LODGroup组件。把LODGroup组件添加在一个空物体身上。这个空物体身上不添加MeshRenderer组件,也不添加MeshFilter组件,但可以添加碰撞器。如果要添加刚体、脚本等。这个空物体要渲染的每一个模型都要作为它的子物体,它们的身上要添加MeshRenderer组件和MeshFilter组件,用于渲染这个模型,但是不要给它们添加碰撞器。

LOD级别LOD 0、LOD 1、LOD 2分别表示摄像机从近处看、从中等距离处看、从远处看时,所使用的模型的信息。Culled表示不渲染该模型。鼠标悬停在两个LOD级别之间,按住鼠标左键,左右拖动,可以调整LOD级别的范围。如果右击它们中的一个,点击Insert Before可以添加一个新的LOD级别到渲染条上,点击Delete,则可以删除该LOD级别。点击选中要设置的LOD级别,在Renderers下方点击Add可以选择要显示的游戏对象,点击-号可以移除该游戏对象,添加时也可以直接拖拽,另外,点开下方的LOD 0、LOD 1、LOD 2,点击+、-号也可以添加、删除模型。

演示:

3.10.2 参数设置

        Edit——Project Settings——Quality中有控制整个项目LOD的参数。

        LOD Bias值,可以理解为控制整个LOD条的距离,值越大LOD条表示的距离越长,值越小距离越短。比如,Bias为2,则相机从LOD条起始到末尾的移动距离为100,若Bias为20,则这个距离值将远大于100。
        Maximum LOD Level表示所有LOD Group组件最大能使用LOD级别,超过这个限制的LOD级别的模型不会显示。注意,LOD级别0大于LOD级别1,LOD级别1大于LOD级别2,依次类推。例如把这个值设置为1,则即使摄像机到物体的距离达到了LOD级别为0的范围,则也不会切换到LOD级别为0的模型来显示。例如把这个值设置为2,则即使摄像机到物体的距离达到了LOD级别为0或者LOD级别为1的范围,则也不会切换到这些模型来显示。

3.10.3 代码调用

        这里展示了一些LODGroup组件在代码中可调用内容。

LODGroup.crossFadeAnimationDuration
//float型。控制各个LOD级别之间进行切换的时候,用多少秒进行淡入淡出。
//如果赋值为0或者负数,则会报错。LODGroup对象.animateCrossFading
//bool型。控制各个LOD级别之间过渡的时候,是否有淡入淡出动画。
//值为true,则过渡时有淡入淡出动画。
//值为false,则过渡时没有淡入淡出动画。LODGroup对象.enabled
//bool型。是否启用这个LODGroup组件。
//值为true,则这个LODGroup组件才能正常工作。
//值为false,则这个LODGroup组件将不起作用,不会根据摄像机的距离切换要渲染的模型。LODGroup对象.fadeMode
//对应Inspector窗口的Fade Mode
//LODFadeMode型。控制各个LOD级别之间进行切换的时候,使用怎么样的动画进行过渡。
//LODFadeMode.None表示不使用任何过渡动画,切换LOD级别的时候,会瞬间切换。
//LODFadeMode.CrossFade表示切换LOD级别的时候,原来的模型仍然会存在一定秒数,之后才会消失。这样可能会同时看到两个LOD级别的模型,造成不好的视觉效果。
//LODFadeMode.SpeedTree表示切换LOD级别的时候,使用SpeedTree样式的过渡动画。LODGroup对象.localReferencePoint
//Vector3型。用于计算LOD级别与摄像机之间的距离的本地空间的参考点。
//Unity会自动计算这个参考点与摄像机之间的距离,并根据该距离切换到对应的LOD级别,从而渲染对应的模型。LODGroup对象.lodCount
//int型。这个LODGroup组件的LOD级别的数量。Culled是不算在内的。
//例如默认情况下,有LOD 0、LOD 1、LOD 2、Culled,则这个变量的值是3,因为Culled是不算在内的。LODGroup对象.size
//对应Inspector窗口的Object Size的值。
//float型。表示本地空间中这个LOD组件的包围区域的大小。
//在使用LODGroup组件进行LOD级别的切换时,Unity将自动根据摄像机到包围区域表面的距离来判断要切换到哪个LOD级别,而size属性是计算此距离的重要因素之一。LODGroup对象.ForceLOD(int 要切换到的LOD级别的索引)
//强制渲染指定LOD级别对应的模型。
//例如要渲染LOD 0对应的模型,则传入的参数就是0。如果要渲染LOD 1对应的模型,则传入的参数就是1。
//如果传入的参数大于这个LODGroup组件.lodCount-1,则不会渲染任何一个模型。
//调用这个方法且传入0或正整数作为参数后,则这个LODGroup组件就不会根据摄像机的距离自动切换到对应的LOD级别了,会一直渲染这个方法指定的LOD级别对应的模型。
//此时如果要让这个LODGroup组件重新根据摄像机的距离自动切换到对应的LOD级别,并自动渲染该LOD级别对应的模型,则可以再次调用这个方法,传入一个负整数作为参数,例如LODGroup型对象.ForceLOD(-1)。LODGroup对象.GetLODs()
//返回LOD[]型。表示这个LODGroup组件的LOD级别的数组。
//LOD级别的数组有哪些元素,可以在Inspecotor窗口的Object Size下方查看。LODGroup对象.RecalculateBounds()
//对应Inspector窗口的Recalculate Bounds按钮。
//调用之后,会重置这个LODGroup组件的包围区域。这个区域就是模型对象尺寸区域Object Size,在我们点击Reset Object Size后,就可以点击Recalculate Bounds按钮重新计算Bounds了。
//这个方法的调用相对较慢,不要经常调用。LODGroup对象.SetLODs(LOD[]型 LOD级别的数组)
//重新设置这个LODGroup组件的LOD级别的数组。
//一旦调用这个方法,则这个LODGroup组件原来的LOD数组就会被整个替换掉。
//LOD级别的数组有哪些元素,可以在Inspecotor窗口的Object Size下方查看。LOD lod=new LOD();
//创建一个LOD级别,它是一个结构体。LOD lod=new LOD(float 用于过渡的屏幕相对高度,Renderer[] 此LOD级别要使用的渲染器的数组)
//创建一个LOD级别,它是一个结构体。并且设置用于过渡的屏幕相对高度和渲染器的数组。
//用于过渡的屏幕相对高度的范围是[0,1]。LOD对象.fadeTransitionWidth
//float型。表示切换LOD界别时淡入淡出的过渡区域的宽度。
//这个宽度与当前LOD的整个长度成比例,范围是[0,1]。
//仅当未动画化时才使用。LOD对象.renderers
//Renderer[]型。表示这个LOD级别的渲染器的数组。LOD对象.screenRelativeTransitionHeight
//float型。表示这个LOD级别用于过渡的屏幕相对高度,范围是[0,1]。

3.11 合并网格

        合并网格就是把多个网格合并为一个整体,从而提升游戏性能。但是这样一来,我们就不能单独控制这些网格运动了,只能控制合并后的整个网格运动。而且合并的网格使用的材质必须是相同的,合并才可能成功,否则合并之后依然不能提升性能。
        可以自己写代码,使用Unity自带的CombineMeshes方法来合并网格,也可以使用资源商店的插件,在资源商店搜Mesh Combine可以搜索到相关的插件,例如Easy Mesh Combine Tool等插件。
        合并后面DrawCall可能降低,面数和顶点数可能增多,具体性能怎样要测试才知道。

3.12 动画优化思路

3.12.1 Animator组件

恰当地设置Animator组件的Culling Mode,可设置内容:

  1. Always Animate表示如果该动画不可见,也会播放它。
  2. Cull Update Transformations表示如果该动画不可见,则不会渲染该动画,但是依然会根据该动画的播放来改变游戏对象的位置、旋转、缩放,这样是常用的选项。
  3. Cull Completely表示完全不会播放该动画,不但不会渲染该动画,而且也不会改变游戏对象的位置、旋转、缩放。

        对于Animator组件,可以使用Animator.StringToHash方法获得指定字符串的哈希值,然后保存此哈希值,后续再把它作为参数传入Animator型对象.GetXXX方法和Animator型对象.SetXXX方法中进行使用。只是起个方便作用吧。

3.12.2 Skinned Mesh Renderer组件

        禁用SkinMesh Renderer组件的Update When Offscreen可以让角色在不可见的时候动画不更新,这样可以减少计算量,提升性能。

3.12.3 其他

        不用的Animation组件和Animator组件可以考虑删掉,因为只要它们存在,就会消耗性能来检测当前的状态和过渡条件。

        一些简单的动画可以使用DoTween、iTween等插件实现,而不需要每个动画都用Animator来实现。

3.13 音频优化思路

        Unity支持后缀为.wav、.ogg、.mp3的音频文件,但建议使用.wav,因为Unity对它的支持特别好。注意:Unity在构建项目时总是会自动重新压缩音频文件,因此无需刻意提前压缩一个音频文件再导入Unity,因为这样只会降低该音频文件最终的质量。

        音频属性设置(选中音频后的Inspector面板):

  1. 把音频文件导入Unity后,选中它,可以在Inspector窗口设置它的属性。勾选Force To Mono,这样就会把这个音频文件设置为单声道。可以节省该资源所占据的空间。因为很少有移动设备实际配备立体声扬声器。在移动平台项目中,将导入的音频剪辑强制设置为单声道会使其内存消耗减半。此设置也适用于没有立体声效果的任何音频,例如大多数UI声音效果。
  2. 对于Load Type选项,小文件(小于200kb)选择Decompress on Load,中等大小的文件(大于等于200kb)选择Compressed In Memory,比较大的文件(如背景音乐)选择Streaming。
  3. 对于Compression Format的选项,PCM表示不压缩,Vorbis表示压缩,但也会尽量保证音频的质量,ADPCM表示压缩,且压缩的程度比Vobis更高。由于PCM不会压缩音频,所以占用的空间大,应尽量少用,长时间的音频文件可以使用Vorbis,短时间的音频文件可以使用ADPCM。
  4. Sample Rate Setting用于控制音频文件的采样率,对于移动平台,采样率不需要太高,建议选择Override Sample Rate,然后在下方的Sample Rate选择22050Hz,一般这样就够用了。

3.14 物理优化思路

  1. 使用简单的碰撞器进行碰撞检测,如球体碰撞器、盒子碰撞器、胶囊体碰撞器,少用网格碰撞器等复杂的碰撞器。即使用多个简单的碰撞器组合在一起,也往往比使用网格碰撞器的性能要好。
  2. 如果同一个功能既可以用碰撞器来做,也可以用触发器来做,则往往使用触发器来做,性能更好。
  3. 尽量减少刚体组件,因为刚体组件的物理计算较多。
  4. 如果勾选刚体组件的Is Kinematic,则性能会有所提高。但这样一来,这个刚体只会给别的刚体施加力,自己不会受到别的刚体施加的力的作用。
  5. Edit——Project Settings——Player——Other Settings——勾选Optimization下方的Prebake Collision Meshes,可以提高碰撞的效率,但是构建游戏的时间会增长。
  6. Edit——Project Settings——Physics或者Physics 2D——设置Layer Collision Matrix。它规定了哪些Layer层的游戏对象可以彼此碰撞,哪些Layer层的游戏对象会忽略碰撞。如果有些Layer层的游戏对象之前不需要进行碰撞,则可以在这里设置,取消勾选则表示不会碰撞。
  7. Edit——Project Settings——Time——稍微调大Fixed Timestep,这样可以稍微提升游戏性能,但是物体的运动可能会出现问题。(增大此值就减少了Fixed Update的执行次数,所以性能提升,但随之就是可能出问题)

3.15 其他优化思路

  1. 使用AssetBundle作为资源加载方案。而且经常一起使用的资源可以打在同一个AssetBundle包中。尽量避免同一个资源被打包进多个AB包中。压缩方式尽量使用LZ4,少用或不要用LZMA的压缩方式。如果确定后续开发不会升级Unity版本,则可以尝试启用打包选项BuildAssetBundleOption.DisableWriteType,这样TypeTree信息不会被打到AB包中,可以极大减小包体大小以及运行加载时的内存开销。
  2. 使用AssetBundle或者Addressables加载的资源,如果不使用,要记得卸载它们,否则会造成内存泄漏。
  3. 不用的资源要释放掉,不用的引用类型的变量也要赋值为null,不要让它们一直占着内存中。
  4. 加载资源时尽量使用异步加载。
  5. 频繁创建和销毁对象,可以使用对象池。
  6. 切换场景时,旧的场景要释放掉,不用的资源也可以考虑释放掉,也可以考虑用System.GC.Collect来进行一次垃圾回收。
  7. 画质控制,Edit→Project Settings→Quality→在这里进行画质设置,还可以分画质层级。
  8. 锁定游戏的帧率 。帧率为30,游戏会明显卡顿,但是对于手游来说,消耗手机的电量比较少。帧率为45,游戏有一点点卡,但还凑合,消耗电量中等。帧率为60,游戏很流畅,但消耗手机的电量会比较多。可以用Application.targetFrameRate来锁定帧率,也可以用UnityEngine.Rendering命名空间中的OnDemandRendering.renderFrameInterval来锁定帧率。
  9. 尽量少用foreach语句,可以改为for语句。因为每次使用foreach语句会造成微量的内存垃圾。
  10. 要判断GameObject型对象.tag是不是某个标签,使用GameObject型对象.CompareTag方法会更高效。
  11. 尽量少用GameObject.Find方法和Object.FindObjectOfType方法来查找游戏对象,可以提前把要查找的游戏对象存储在变量、列表、字典等容器中,方便查找。也可以用GameObject.FindGameObjectWithTag方法来查找游戏对象。
  12. 在UI显示字符串的时候,如果一些内容是固定的,我们可以把它拆分开来,这样可以减少使用+号来拼接的次数,减少内存垃圾的产生。例如“杀敌数:999”,其中“杀敌数:”是固定的,冒号后面的数字才是会变的,那么我们可以用两个Text组件分别记录它们,改变的时候只改变冒号后面的数字。
  13. 频繁对字符串赋新的值,或者频繁拼接字符串的时候,可以使用StringBuilder代替string。
  14. 如果要频繁操作某脚本,不要每次都用GetComponent方法来获取这些脚本。可以用一个变量存储起获得的这个脚本,之后要访问它,就直接访问这个变量即可。也可以考虑在生命周期方法Awake或者Start中声明变量来存储,之后访问这个变量即可。
  15. 尽量少用正则表达式。虽然正则表达式的形式看上去比较简便,但是使用它会造成一定的性能消耗,且会产生内存垃圾。
  16. 尽量少用LINQ语法,因为每次使用LINQ都会产生一定量的内存垃圾。
  17. 尽量少用Camera.main来访问主摄像机,因为每次访问它,实际上Unity都是从场景中查找它的。可以声明一个变量存储它,在生命周期方法Awake或Start中获取主摄像机的应用。
  18. 在Animator、Shader中使用Get方法和Set方法时,不传入字符串作为参数,而是传入哈希值。例如Animator组件可以使用Animator.StringToHash方法获得指定字符串的哈希值,再把它作为参数传入Animator组件的Get方法或Set方法中进行使用。例如Shader,则可以用Shader.PropertyToID方法来获取指定属性的ID。
  19. 使用非分配物理API。例如使用Physics.RaycastNonAlloc方法代替Physics.RaycastAll方法,使用Physics.SphereCastNonAlloc方法代替Physics.SphereCastAll方法,以此类推。Physics2D类也有类似的方法。
  20. 一般情况下,整数的数学运算比浮点数的数学运算效率高,浮点数的数学运算比矢量的数学运算效率高。可以灵活运用数学的加法交换律、加法结合律、乘法交换律、乘法结合律,在保证结果不变的前提下,调整运算顺序,减少浮点数的数学运算和矢量的数学运算。
  21. 使用高效的算法进行计算。
  22. 每次执行Debug.Log来打印信息会消耗极少量的性能,如果要在游戏正式发布之后不执行某些Debug.Log的语句,但又不想把这些代码删掉,则可以使用宏来禁止在游戏正式发布之后执行Deubg.Log的语句。例如使用#if语句或者Conditional特性。
  23. 尽量减少在生命周期方法Update、FixedUpdate、LateUpdate中的逻辑。其中有些不需要频繁执行的逻辑,可以使用协程或者Invoke方法,每隔指定的秒数执行一次或每隔指定的帧数执行一次。
  24. 尽量避免频繁的装箱拆箱操作。也可以使用泛型,这样就能避免装箱拆箱。但是要注意,Lua热更新对泛型的支持不太好。
  25. 如果物体身上添加了CharacterController组件,则尽量用CharacterController组件的方法来移动它,而不是用Transform类的方法来移动它。同理,如果物体身上添加了刚体组件,则应尽量用刚体组件的方法来移动它,而不是用Transform类的方法来移动它。
  26. 应尽量避免DontDestroyOnLoad中加载的资源过多,因为它在切换场景的时候不会被释放,声明的变量以及加载的资源会一直占用着内存。我们可以考虑把一些资源不用的资源释放掉,需要的时候再加载它。
  27. 不使用组件可以删掉,这样可以节省一些内存。常见的有AudioSource组件、Animator组件、Animation组件等,如果它们不需要使用,则可以删掉。
  28. 写一个类继承AssetPostProcessor,然后定义里面特定的方法,以此来自动设置资源导入Unity之后的属性。
  29. 尽量避免闭包。因为闭包会产生额外的内存开销。

4 工具Unity UPR

        可以使用Unity UPR对整个项目进行性能分析,找出问题后,再手动优化它们。其中Unity UPR中的Asset Checker能对本地的整个Unity项目进行性能分析,帮助我们找出问题。
        Unity UPR网址:UPR - Unity专业性能优化工具

5 后记

没啦。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/813481.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图机器学习导论

图&#xff1a;描述关系数据的通用语言&#xff0c;起源于哥尼斯堡七桥问题 传统的机器学习&#xff1a;数据样本之间独立同分布&#xff0c;简单拟合数据边界&#xff0c;在传统的机器学习中&#xff0c;每个数据样本彼此无关。传统的神经网络&#xff0c;只能处理简单的表格、…

实体行业的品牌传播与网络运营,迅腾文化解决完善品牌定位运营

实体行业的品牌传播与网络运营&#xff0c;迅腾文化解决完善品牌定位运营 在今日的商业环境中&#xff0c;如何借助网络的力量&#xff0c;将品牌有效地传播出去&#xff0c;让渠道商、加盟商和消费者感受到安全感&#xff0c;成为了深思的问题。品牌是一个企业的灵魂&#xf…

动态规划先导片

大家知道动规是由前一个状态推导出来的&#xff0c;而贪心是局部直接选最优的&#xff0c;对于刷题来说就够用了。 对于动态规划问题&#xff0c;我将拆解为如下五步曲&#xff0c;这五步都搞清楚了&#xff0c;才能说把动态规划真的掌握了&#xff01; 确定dp数组&#xff0…

开源!工厂数字化项目会用到的地理信息系统

软件介绍 QGIS&#xff08;Quantum GIS&#xff09;是一款免费、开源、跨平台的地理信息系统&#xff08;GIS&#xff09;软件&#xff0c;适用于Unix平台、Windows和MacOS。提供了强大且用户友好的功能&#xff0c;使其成为地理信息处理领域的热门选择。 功能特点 1.空间数据管…

react使用npm i @reduxjs/toolkit react-redux

npm i reduxjs/toolkit react-redux 创建一个 store文件夹&#xff0c;里面创建index.js文件和子模块文件夹 index,js文件写入以下代码 import {configureStore} from reduxjs/toolkit // 导入子模块 import counterReducer from ./modules/one import two from ./modules/tw…

鸿蒙TypeScript学习第14天:【联合类型】

1、TypeScript 联合类型 联合类型&#xff08;Union Types&#xff09;可以通过管道(|)将变量设置多种类型&#xff0c;赋值时可以根据设置的类型来赋值。 注意&#xff1a;只能赋值指定的类型&#xff0c;如果赋值其它类型就会报错。 创建联合类型的语法格式如下&#xff1…

RTSP/Onvif安防视频EasyNVR平台 vs.多协议接入视频汇聚EasyCVR平台:设备分组的区别

EasyNVR安防视频云平台是旭帆科技TSINGSEE青犀旗下支持RTSP/Onvif协议接入的安防监控流媒体视频云平台。平台具备视频实时监控直播、云端录像、云存储、录像检索与回看、告警等视频能力&#xff0c;能对接入的视频流进行处理与多端分发&#xff0c;包括RTSP、RTMP、HTTP-FLV、W…

大厂Java笔试题之判断字母大小写

/*** 题目&#xff1a;如果一个由字母组成的字符串&#xff0c;首字母是大写&#xff0c;那么就统计该字符串中大写字母的数量&#xff0c;并输出该字符串中所有的大写字母。否则&#xff0c;就输出* 该字符串不是首字母大写*/ public class Demo2 {public static void main(St…

Redis的双写一致性问题

双写一致性问题 1.先删除缓存或者先删除数据库都可能出现脏数据。 2.删除两次缓存&#xff0c;可以在一定程度上降低脏数据的出现。 3.延时是因为数据库一般采用主从分离&#xff0c;读写分离。延迟一会是让主节点把数据同步到从节点。 1.读写锁保证数据的强一致性 因为一般放…

在视频号开店,新手一定要注意了,这几个细节很多人都不知道

大家好&#xff0c;我是电商笨笨熊 视频号小店作为今年黑马项目&#xff0c;自是吸引力不少的电商玩家&#xff1b; 但是在这些玩家中不免有一些新手玩家&#xff0c;从未做过电商&#xff0c;或者做过其他平台的电商但是没有接触过视频号&#xff1b; 而视频号小店在某些地…

thinkphp6入门(23)-- 如何导入excel

1. 安装phpexcel composer require phpoffice/phpexcel composer update 2. 前端 <form class"forms-sample" action"../../xxxx/xxxx/do_import_users" method"post" enctype"multipart/form-data"><div class"cont…

【InternLM 实战营第二期-笔记4】XTuner 微调个人小助手认知

书生浦语是上海人工智能实验室和商汤科技联合研发的一款大模型,很高兴能参与本次第二期训练营&#xff0c;我也将会通过笔记博客的方式记录学习的过程与遇到的问题&#xff0c;并为代码添加注释&#xff0c;希望可以帮助到你们。 记得点赞哟(๑ゝω╹๑) XTuner 微调个人小助手…

基于 FPGA 的 DE1-SoC 功率估算器

Introduction 功耗是当今许多技术都要考虑的重要因素。例如&#xff0c;手机生产商总是谈论他们在电源管理方面的改进&#xff0c;以及如何延长电池的使用寿命。功能与功耗之间的平衡是许多人都在研究的有趣课题。然而&#xff0c;当我们做实验时&#xff0c;我们很少会考虑我…

SpringBoot3整合Mybatis plus

Java版本&#xff1a;17 Spring Boot版本&#xff1a;3.1.10 Mybatis plus版本&#xff1a;3.5.5 源码地址&#xff1a;Gitee仓库 01 创建我们的项目工程 首先&#xff0c;我们创建一个maven工程spring-boot3-demo&#xff0c;pom文件配置如下。 这里我们将spring-boot-start…

视频号小店好做吗?普通人没有货源,也可以做吗?

大家好&#xff0c;我是电商糖果 视频号小店作为2022年才出来的电商黑马项目&#xff0c;吸引了不少正在找创业项目的朋友。 这里面也有很多没有接触过电商&#xff0c;没有货源的普通人。 于是不少朋友就问糖果&#xff0c;如果普通人没有货源可以做吗&#xff1f;小店好做…

简单好用的SaaS知识库工具都在这了,看完赶紧收藏!

在信息飞速发展的今天&#xff0c;企业如何有效地管理海量的信息和知识成为了提高工作效率的关键。SaaS知识库工具正成为企业寻求的解决方案&#xff0c;它们不仅能够帮助团队组织文档&#xff0c;而且优化知识分享流程。现在就让我们来看看市场上几款简单又好用的SaaS知识库工…

佛山分公司迎来重要指导蒋书记一行及杭州区域分公司领导共襄盛举

近日&#xff0c;佛山分公司迎来了一场重要的指导活动。蒋书记携夫人&#xff0c;以及助理黄显文和公司工作人员施晓燕等一行领导莅临佛山分公司&#xff0c;为公司的未来发展提供了宝贵的指导意见。同时&#xff0c;江浙福地区的杭州区域分公司负责人白棋元总和朱建江总也亲临…

宝藏免费音乐软件LX music

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 宝藏免费音乐软件LX music 前言LX Music的特色功能&#xff1a;音乐播放的新境界安装与配置&#xff1a;在不同平台上使用LX Music下载页面 主题定制 本文将深入研究LX Music&#xff0c;一款备受欢迎…

socat神器解密:网络数据传输的利器

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 socat神器解密&#xff1a;网络数据传输的利器 前言socat简介基本用法常见功能常见功能&#xff1a;1. 端口转发和数据重定向&#xff1a;2. 加密和解密数据流&#xff1a; 高级功能1. 代理服务器和隧…

力扣 | 160. 相交链表

import ListNodeInfo.ListNode;import java.util.HashSet; import java.util.Set;public class Problem_160_IntersectionOfTwoLinkedList {//双指针方法 public ListNode getIntersectionListNode(ListNode headA, ListNode headB){if(headA null || headB null) return nul…