Unity笔记
http://www.cnblogs.com/alan777/p/6204759.html
Unity函数调用
- 另外一个出乎人们预料的引起堆内存分配的函数是GameObject.name 或 GameObject.tag。他们都是返回新字符串的存取器。这意味着调用他们会产生垃圾。缓存可能会有效果,在这个例子中,我们可以用相关的Unity方法替代他们。当检查游戏对象的Tag是否相等时,使用GameObject.CompareTag()不会产生垃圾。
- Unity还有很多的函数,有类似的不会引起堆内存分配的版本,例如我们可以使用Input.GetTouch() 和 Input.touchCount代替Input.touches,或者使用Physics.SphereCastNonAlloc()代替Physics.SphereCastAll().**
协程
- 调用StartCoroutine()会产生少量的垃圾,因为Unity必须创建管理协程的类实例。
- 协程中的yield语句自身不会产生堆内存分配;尽管如此,我们通过yield传递的值可能会产生不必要的堆内存分配。例如下面的代码就会产生垃圾。
yield return 0;
这会产生垃圾是因为发生了装箱,如果我们只是想要等待一帧,而不产生垃圾,最好是使用下面的代码:
yield return null;
- 另一个使用协程的常见错误是在yield返回相同的值得时候,多次使用new。例如下面代码中,每次循环都会创建和释放WaitForSeconds对象:
while (!isComplete)
{
yield return new WaitForSeconds(1f);
}
如果缓存了WaitForSeconds,可以减少很多垃圾:
WaitForSeconds delay = new WaitForSeconds(1f);
while (!isComplete)
{
yield return delay;
}
组织我们的代码以便最小化垃圾回收的影响
- 我们不必要的增加垃圾回收器的负载的一种情况是,我们要求他去检查原本不必要检查的东西。结构体是值类型,但是如果我们在结构体中包含了引用类型的变量,那么垃圾回收器就需要检查整个结构体。如果我们有一个由大量这种结构体组成的数组,那么将使得垃圾回收器做了很多额外的工作。
下面的例子中,结构体包含了字符串,垃圾回收器必须要检查整个数组。
public struct ItemData
{
public string name;
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
- 另个增加没必要的垃圾回收器负载的情况是没必要的持有对象引用。当垃圾回收器在堆上检查对象的引用时,他必须在代码中检查每一个持有该对象的引用。我们持有堆中对象的引用越少,垃圾回收器的工作就会越少,哪怕我们并没有减少堆上的对象。
在下面的例子中,我们有一个用于填充对话框的类。当用户查看过后对话框时,另一个对话框会显示出来。我们的代码持有了一个用于填充下一个对话库的数据类DialogData,这意味着垃圾回收器必须检查这个引用。
public class DialogData
{
private DialogData nextDialog;
public DialogData GetNextDialog(){
return nextDialog;
}
}
这里,我们重构代码,返回下一个DialogData的实例id,替代返回实例本身。这就不需要引用对象,也就不会增加垃圾回收器的负载了。
public class DialogData
{
private int nextDialogID;
public int GetNextDialogID() {
return nextDialogID;
}
}
降低要渲染的对象数量
-
简单的降低我们场景的可见对象数量,是一个有效的解决方案。例如,我们要渲染有很多人物的人群,我们可以尝试减少人物的数量,如果看起来人群的效果仍然不错,那么这就是一个比其他复杂方法快捷的多的优化方法。
-
我们可以通过设置摄像机的剪裁平面的远端来降低摄像机的绘制范围。这个属性表示距离摄像机多远的物体将不再被渲染。如果我们想隐藏远处的物体不被渲染的事实,我们可以尝试使用雾来掩盖远处。
-
如果需要基于距离的更细粒度的隐藏物体,我们可以使用摄像机的Layer Cull Distances属性,它可以给不同的Layer设置单独的剪裁距离。如果我们有很多前景装饰细节时,这个方法很有用。我们可以使用很小的距离隐藏细节。
-
我们可以使用遮挡剔除功能去关闭被其他物体遮挡的物体的渲染。例如,我们场景中有一个很大的建筑,我们可以使用遮挡剔除功能,关闭它后面的物体的渲染。Unity的遮挡剔除功能不适用于所有的场景,它会导致额外的cpu消耗,并且相关设置很复杂,但是在一些场景中,它可能会极大的改善性能。使用遮挡剔除的最佳实践,This Unity blog post on occlusion culling best practices。
-
另外,我们可以通过手工的关闭物体渲染来实现我们自己的遮挡剔除,我们可以手工的关闭我们知道玩家无法看到的物体的渲染。例如,如果我们的场景包含一些过场的物体,那么在他们出现之前或者移出以后,我们应该手工的关闭他们的渲染。对于我们游戏来说,使用我们的知识,手工的剔除,往往比Unity动态的遮挡剔除有效的多。
蒙皮
-
我们应该考虑当前正在使用SkinnedMeshRenderers组件的物体是否有必要使用。可能是这种情况,我们导入的模型包含了SkinnedMeshRenderers组件,但是我们其实并不需要它运动。这种情况下,我们使用MeshRenderer组件替换它,会有助于性能提升。当我们在Unity中导入模型时,如果我们在模型的导入设置中选择不导出动画,这个模型就会包含一个MeshRenderer组件替换SkinnedMeshRenderers组件。
-
如果我们只在一些时刻运动物体(例如,只在游戏开始时,或者只有距离摄像机一定距离内时),我们应该交换为一个细节较少的网格,或者使用MeshRenderer替换SkinnedMeshRenderers。SkinnedMeshRenderers组件有一个函数BakeMesh,可以用匹配的动作创建一个网格,这个十分有用,在不同的网格或渲染器中切换时物体不会有可见的变化。
-
在一些平台上,和CPU相比,蒙皮可以被GPU更高效的处理。如果我们的GPU比较强,这个值得尝试。我们可以为当前平台开启GPU蒙皮,在Player Settings中。
如果游戏是GPU限制,那么第一件事就是找到GPU瓶颈的原因。GPU性能最常被填充率限制,尤其在移动平台,但是显存带宽和顶点处理也可能影响。让我们检查这些问题,并且学习引起问题的原因,怎么诊断和怎么修复问题。
填充率
填充率是指GPU在屏幕上每秒可以渲染的像素数。如果我们的游戏收到填充率的限制,意味着我们的游戏每帧尝试绘制的像素数量超过了GPU的处理能力。
检查是否填充率引起了游戏GPU限制很简单:
-
使用Profiler分析,注意GPU时间
-
在Player settings中降低显示分辨率
-
再次分析游戏,如果性能改善了,很可能是填充率的问题
如果确认了填充率引起了问题,有几个方法可以解决问题:
-
片元shader是告诉GPU怎么样去绘制一个像素的一段shader代码。这段代码GPU需要为每一个需要绘制的像素执行,所以如果这段代码效率低,那么很容易发生性能问题。复杂的片元shader是很常见的引起填充率问题的原因。
-
如果我们的游戏使用Unity内置shader,我们应该使用最简单和最优化的shader,为了达到我们想要的视觉效率。例如,the mobile shaders是Unity针对移动平台高度优化的shader,我们应该实验使用它们是否可以在不影响视觉效果的前提下改善性能。这些shader是为了移动平台设计的,但是它们也适用于任何项目。如果使用它们可以达到项目视觉效果的要求,那么在非移动平台上使用它们也是能够很好的改善性能的。
-
如果游戏中的物体使用的是Unity的Standard Shader,那么理解Unity编译这些shader是基于当前的材质设置的是很重要的。只有那些当前使用的功能会被编译。这意味着,移除例如detail maps可以减少片元shader的复杂度,这对性能提升有很大益处。如果我们游戏中是这种情况,我们应该实践,是否能够在不影响视觉质量的前提是提升性能。
-
如果我们游戏使用的是定制的shader,我们应该尽可能的优化它。优化shader是一个很复杂的主题,请参考this page of the Unity Manual和 this page of the Unity Manual。
-
Overdraw是指相同的像素绘制了多次。这是在物体绘制在其他物体之上的时候发生的,也在很大程度上引起了填充率问题。为了理解Overdraw,我们必须先理解Unity在场景中绘制物体的顺序。物体的shader决定了物体的绘制顺序,通常由render queue属性决定。Unity使用这些信息按照严格的顺序绘制物体,具体细节请参考page of the Unity Manual 另外在不同render queue的物体在被绘制之前会按不同的顺序排序。例如,Unity在Geometry queue中为最小化Overdraw会从前到后排序物体,但是在Transparent queue中,为了达到视觉效果的要求,则是从后到前排序物体。在Transparent queue中,从后向前排序物体其实最大化了Overdraw。Overdraw是一个很复杂的主题,并且没有一刀切的解决方案,但是降低重叠物体的数量使得Unity不能自动排序是关键。调查Overdraw问题最好的起点是Unity的场景视图中,DrawMode允许我们看到场景中的Overdraw,我们可以从这开始降低Overdraw的工作。最常见的引起Overdraw的因素是透明材质,未优化的粒子,和重叠的UI元素。所以我们应该尝试优化这些。请参考This article on the Unity Learn site 这篇文章聚焦于UI,但是也包含了Overdraw的很好的指导。
-
使用屏幕后处理技术也会极大的影响填充率,尤其是我们使用了不止一种的屏幕后处理的时候。如果我们在使用屏幕后处理是遇到了填充率问题,我们应该尝试不同的设置或者使用更加优化的屏幕后处理版本。例如使用Bloom (Optimized)替换Bloom。如果我们在同一个摄像机下使用了多个屏幕后处理,这将造成成倍的shader pass。这种情况下,我们应该合并shader到一个单独的pass,例如Unity’s PostProcessing Stack。如果我们优化屏幕后处理效果后,仍然有填充率问题,那么我们也许要考虑关闭屏幕后处理,尤其是在低端的设备上。
显存带宽
显存带宽是指GPU读写它的专用内存的速度。如果我们的游戏受限于显存带宽,通常意味着我们使用的纹理太大了,以至于GPU无法快速处理。
我们可以按如下方法检查是否显存带宽的问题:
-
用Profiler分析游戏,并关注GPU时间
-
在质量设置中降低当前平台的纹理质量
-
继续分析游戏,如果性能改善了,那么通常是显存带宽的问题。
如果是显存带宽的问题,我们需要降低纹理的内存占用。针对不同的游戏通常有不同的最佳解决方案,这里我们提供几个优化纹理的方法:
-
纹理压缩技术可以同时极大的降低纹理在磁盘和内存中的大小。如果是显存带宽的问题,那么使用纹理压缩减小纹理在内存的大小可以帮助改善性能。Unity中有很多可用的纹理压缩的格式和设置。通常来说,一些纹理压缩格式只要可用就应该尽可能的使用,尽管如此,通过实践找到针对每个纹理最合适的设置是最好的。请参考This page in the Unity Manual 讲述了纹理压缩的格式和各种设置的详细信息。
-
多级渐远纹理,是Unity对远处的物体使用的低分辨率版本的纹理。如果我们的场景包含距离摄像机很远的物体,我们可以通过使用多级渐远纹理来缓解显存带宽的问题。Unity场景视图中的The Mipmaps Draw Mode允许我们查看哪些物体受益于多级渐远纹理,请参考this page of the Unity Manual 包含了使用多级渐远纹理的详细信息。
顶点处理
顶点处理的消耗受两件事情影响:必须渲染的顶点数量,以及在每个顶点上要进行的操作数量。
如果我们的游戏是GPU限制,并且已经确认了不是填充率和显存带宽引起的问题,那么就很可能是顶点处理引起的。如果是这种情况,那么尝试减少GPU顶点处理的数量很可能会获得性能提升。
有一些方法可以减少顶点数量或者在每个顶点上执行的操作数量:
- 首先,我们应该降低不必要的网格复杂的。如果我们使用的网格包含在游戏中无法被看见的LOD,或者低效的网格在错误的创建时包含了太多的顶点,这些都会浪费GPU的工作量。最直接的降低顶点处理的消耗的方法,就是在3d建模软件中创建模型时使用更少数量的顶点。
- 我们可以尝试使用法线贴图技术,我们使用它来模拟更高几何复杂度的网格。尽管使用这种技术有一些GPU负载,但是在多数情况下,它会获得性能提升。请参考This page of the Unity Manual 介绍了使用法线贴图技术去模拟更复杂的网格。
- 如果我们的游戏没有使用法线贴图技术,在网格的导入设置中,我们可以关闭顶点的切线。这会降低GPU处理顶点的数据量。
- LOD(Level of detail),这是当物体远离摄像机时,降低物体网格的复杂度的技术。这可以有效的降低GPU需要渲染的顶点数量,并且不影响视觉表现。具体使用细节请参考. The LOD Group page of the Unity Manual。
- 顶点shader,是一段shader代码,告诉GPU怎么绘制每个顶点。如果我们的游戏受限制于顶点处理的影响,那么降低顶点shader的复杂度可能会有助于性能提升。