Unity基础系列(五)——每秒帧率(测试性能)

2020-07-24 11:14:13 浏览数 (1)

目录

1 构造原子核2 使用Profiler3 测量FPS4 帧平均每秒5 给文本上色

本章重点: 1、用物理学来创造一个不断成长的原子核。 2、使用分析器排查性能。 3、统计并显示帧速率。 4、阻止创建临时字符串。 5、通过平均多个帧来稳定帧速率。 6、对不同帧率进行分色显示。

本教程将创建一个简单的测试场景,然后测试其性能。先用profiler排查,然后创建我们自己的帧率计数器。

本教程要求对Unity脚本有基本的理解。兼容Unity5.0.1及以上版本。如果你还不熟悉Unity脚本操作,可以先看一看前面几个章节。

(聚集球体,知道你的帧率开始承受不住)

1 构造原子核

要测试性能,就需要搭建一个测试场景。一个理想的测试场景应该涵盖高性能和低性能的情况。所以这里通过将越来越多的核子融合在一起来创造一个原子核。随着核变的越来越大,性能逐渐恶化。

核子只是简单的球体,它们会被吸引到场景的中心,在那里它们会聚集在一起形成一个球。这当然不是原子的正确表示,但这不是重点。

我们可以用一个默认的球和一个自定义的Nucleon 组件来模拟一个核。该组件要要同时添加刚体组件,然后简单地将其拉向原点。拉力的强度由可配置的吸引力和距中心的距离决定。

缺少一些访问修饰符?

是的,字段和方法声明中可以省略private 修饰符,因为在默认情况下它们就是私有的。

用球体制造两个核子预制体,一个用于质子,另一个用于中子。分别给不同的材质球,让它们看起来不同。其实性能演示只需要一种核子类型就够了,但这样感觉就没意思了。

什么是预制体?

预制体是一个不存在于场景中且未被激活的Unity对象或对象层次结构。你可以使用它作为模板,创建它的克隆并将它们添加到场景中。要创建一个对象,可以像往常一样在场景中构造一个对象,然后将其拖到项目窗口中。场景对象将成为一个预置实例,如果你不再需要它,可以直接删除。

(核子预制体)

为了产生这些核子,还需要制造另一个成分-- NucleonSpawner 。它需要知道不同副本之间的时间间隔,离中心有多远,要产生什么。

创建一个空的游戏对象,附加一个NucleonSpawner组件,并根据你的需要配置它。

(核子生成器)

为了定期生成,就需要跟踪从上次生成的时间。可以使用一个简单的FixedUpdate方法来完成这个任务。

为什么使用 FixedUpdate 而不是 Update?

使用FixedUpdate会让生成与帧速率无关。如果在子程序之间配置的时间比帧时间短,使用Update会导致产生延迟。因为这个场景的重点是阻碍帧率的,所以这必然会发生。

可以使用一个while循环取代if检查来加速追赶漏产生的核子,但是当timeSinceLastSpawn 意外的被设置为零时,这将导致无限生产循环。将生产限制在每一个固定的时间步骤是一个理智的限制。

实际产生由三个步骤组成。选择一个随机的预制件,实例化它,并在期望的距离上给它一个随机的位置。

(通过轰炸建立一个核)

播放这个场景应该会导致球体向中间聚集。它们会挣脱一段时间,直到相互碰撞,形成一个球。这个球将继续增长,物理计算将变得更加复杂,并且在某一时刻你会注意到帧率的下降。

如果你的机器硬件好,需要很长时间才能看到性能恶化的话,则可以增加产生的速率。通过增加时间尺度的配置来加快时间是比较有效的。可以通过Edit Project Settings Time 找到。当然也可以减 Fixed Timestep ,这也会导致每秒更多的物理计算。

(Unity的时间设置)

为什么在低时间尺度下运动不流畅的?

当time scale被设置为0.1这样的低值时,时间会移动得非常慢。由于固定的时间步长是恒定的,这意味着物理系统更新的频率会降低。因此,物理物体将保持静止,直到一个固定的更新发生,也就是每隔几个帧就更新一次。

随着时间的增加,你可以通过减少固定的time scale来对抗这种现象。或者,可以更改刚体部件的插值模式,以便它们在物理步骤之间进行插值,从而隐藏较低的更新频率。

2 使用Profiler

现在已经有了一个场景,并且最终能降低任何机器的帧率,现在是衡量实际性能的时候了。你能做的最快的事情就是启用游戏视图的统计数据。

(游戏视图统计显示)

然而,光是帧率显示根本不准确,它更像是一个粗略的猜测。可以通过打开Unity的Profiler分析器来看到更直观的数据统计,通过 Window / Profiler 可以打开分析器窗口。分析器给我们提供了很多有用的信息,特别是CPU使用情况和内存数据。

(分析器 显示大量的Vsync时间)

如果启用了vsync,它很可能在一开始就主导CPU图形。为了更好地了解场景需要多少CPU资源,请关闭vsync。您可以通过Edit Project Settings Quality 来关闭。在页面靠下的部分,Other的标头下面。

(关闭垂直同步)

现在我们有超高的帧率了!

如果没有vsync,简单场景就可以获得非常高的帧率,远远超过100。这会给硬件带来不必要的压力。你可以通过设置Application.targetFrameRate属性通过代码强制执行最大帧速率来防止这种情况。请注意,即使在退出播放模式之后,此设置在编辑器中仍然存在。将其设置为?1会消除限制。

现在,你可以更好地了解CPU的使用情况。在这个例子中,物理花费看大部分时间,其次是渲染,然后是脚本。这在很长一段时间内都是正确的,尽管随着球体计数的增加,所有都会变慢。

(没有垂直同步的结果)

这里还可以观察到两个意想不到现象。首先,偶尔会出现CPU使用率的高峰。其次,内存图显示频繁的GC分配峰值,这表明内存被分配并随后就被释放。但示例只是在创建新的对象,并没有丢弃任何东西,这就很奇怪了。

这两种现象都是由Unity编辑器造成的。每当你在编辑器中选择某些内容时,CPU峰值就会发生。内存分配是由对GameView.GetMainGameViewRenderRect的调用引起的,而编辑器会调用GameView.GetMainGameViewRenderRect。除此之外,还有额外的开销,特别是当你同时看到游戏和场景的时候。简而言之,编辑器本身会干扰我们的观测。

但即便如此你仍然可以从编辑器内的概要中获得大量有用的信息,但是如果想要从度量中消除编辑器本身的影响,则必须进行独立构建。如果你进行development 构建,甚至在运行应用程序时自动连接到它,你仍然可以使用分析器。您可以通过“File / Build Settings ”来配置.

(profiler绑定在standalone的构建上 )

分析独立构建的时候,数据看起来差别很大。内存分配现在只由生成核子引发,不再发生垃圾回收。在本示例中,渲染需要更多的时间,因为我运行的应用程序是全屏的。而脚本是如此的微不足道,以至于它们在图形中都是不可见的。

3 测量FPS

profiles 给我们提供了很多有用的信息,但它仍然不能很好地测量帧率。FPS数字显示的应该是1除以CPU时间,我们需要自己来实现。

要要一个简单的组件,告诉我们当前应用程序每秒运行的帧数即可。它只需要一个公共变量就足够了用整数来表示,一般帧率都比较大,所以并不在乎末尾的小数。

这个属性代表什么意思?

属性其是假装为字段的方法。我们将FPS作为公共信息提供,但只有组件本身需要更新该值。所使用的语法是自动生成属性的简写符号,类似于这样。

int fps;

public int fps{get{back fps;}

private {fps=value;}

此简写不能用于Unity的序列化,但在这里没问题,因为并不需要持久化保存FPS值。

我们通过将1除以当前帧的时间增量来测量每秒的帧数,然后将结果转换为整数,进行适当的舍入。

然而,这种方法存在一个问题。时间增量不是处理最后一个帧所需的实际时间,它会受当前time scale的影响。这意味着我们的FPS可能是错误的,除非time scale设置为1。但我们可以使用另外一个字段unscaledDeltaTime来得到没有经过缩放的时间增量。

现在需要UI来显示FPS,就用UGUI吧。创建一个Canvas,其中包含一个Panel,然后包含一个Text对象。可以通过GameObject/UI 子菜单添加这些内容。在添加Canvas时,还将自动添加一个EventSystem对象来处理用户输入,但我们不需要它,因此可以删除它。

(UI对象的层次)

使用默认画布设置,设置为pixel-perfect。

该面板用于创建FPS标签的半透明黑色背景。这样,它将永远是可读的。把它放在窗户的左上角。将它的锚设置在左上角,这样无论窗口的大小如何,它都保持在原地。将其枢轴设置为(0,1)以便于放置。

以类似的方式将Label放置在面板内。将其改为白色粗体文本,以水平和垂直两种方式居中。微调大小,使它适合两位数字显示。

(构建UI)

现在我们需要将FPS值绑定到Label上。为此需要创建一个组件。它需要一个FPSCounter组件从其中检索值,并需要从UnityEngine.UI命名空间中对文本标签的引用来将该值分配给它。

将此组件添加到面板上并将其关联起来,直接附加到Panel上,因为这是一个整体FPS显示器,而不仅仅是Label。后面我们会包括更多的Label。

(设置显示规则)

显示组件只需每帧更新Label的text。所以先缓存一下对计数器的引用,这样我们就不需要每次调用GetComponent了。

FPS标签正在更新!但是当时我们设计它的时候只想展示2位数,所以一旦帧率超过每秒99的时候,显示上就会有问题。所以逻辑上收紧显示值,任何超过99的表现无论如何都足够好了。

(可以看到帧率了)

看起来已经完成了预期的表现,但是有一个很小的问题。现在每帧都在创建一个新的String对象,该对象将在下一个更新中被丢弃。这会污染托管内存,从而触发垃圾收集器。虽然这对桌面应用来说不是什么大问题,但对于内存不足的设备来说,这就更麻烦了。它还污染了我们的分析器数据,这是比较烦人的,需要想办法解决。

(临时的string造成的性能开销)

有办法能摆脱这些临时的string吗?回想一下,FPS的显示值可以是0到99之间的任意整数。那其实就是100个不同的字符串。为什么不创建一次性创建所有这些字符并重复利用它们呢?

通过一个固定数组缓存可能需要的每个数字的字符串,现在已经能够消除所有临时字符串分配!

4 帧平均每秒

更新每个帧的FPS值有一个不好的副作用。当帧率不稳定时,label显示会不断波动,因此很难得到有用的读数。所以需要减少标签浮动更新的频率,但是这样的话,又不是及时反馈帧的浮动变化。

一个可能的解决方案是平均帧速率,平滑突然变化造成的影响,产生较少的抖动值。现在来调整下FPSCounter,使其在可配置的帧范围内完成此操作。将此值设置为1,但它与不平均的那个值完全不相同,因此它实际上是可选的。

(配置帧率)

将属性名从FPS更改为AverageFPS,因为这是对它现在表示的值的更好的定义和描述。你可以使用IDE重构名称,也可以手动更新显示组件以使用新名称。

除此之外,还需要一个缓冲区来存储多个帧的FPS值,再加上一个索引,这样我们就知道将下一个帧的数据放在哪里了。

初始化此缓冲区时,请确保FrameRange至少为1,并将索引设置为0。

Update方法变得有些复杂。它首先初始化缓冲区(如果需要的话),或者是因为我们刚刚开始,或者是因为FraRange已经改变了。不管如何,它都需要先初始化,再更新缓冲区,然后才能计算平均FPS。

更新缓冲区是通过在当前索引中存储当前FPS来完成的,该索引会递增。

如果这样的话,很快就会填满整个缓冲区。所以在增加新值之前,可以放弃最旧的值。所以,可以将所有的值都转换成一个位置,平均值并不关心值所处的顺序。所以我们可以将索引包装回数组的开头。这样,一旦缓冲区被填慢,我们总是用最新的值去覆盖最老的值。

计算平均值比较简单,就是将缓冲区中的所有值相加,再除以值的数量。

现在平均帧率可以正常显示了,在合理的帧范围内,这个表现会减少抖动,让展示变的平滑。但其实还可以做得更好。

由于现在有来自多个帧的数据,我们还可以在这个范围内公开最高和最低的FPS。这会给出更多的信息,而不仅仅是平均水平。

我们可以一边计算,一边找到这些值。

FPSDisplay组件现在可以绑定另外两个Label。

将两个Label添加到UI中,并将它们全部关联起来。把最高的FPS放在顶部,最低的FPS在底部,平均FPS在中间。

(更多的信息展示,更少的抖动)

5 给文本上色

作为FPS标签的最后一步,可以给它们上上色。这可以通过将颜色与FPS值相关联来实现。这样的关联可以用自定义结构表示。

由于FPSDisplay是使用此结构的唯一工具,因此我们将struct定义直接放在该类中,并将其设置为私有,这样它就不会出现在全局命名空间中。使其可序列化,以便由Unity编辑器编辑。

添加这些结构的数组,以便配置FPS标签的着色。我们通常会为它添加一个public字段,但是现在不能加,因为结构本身是私有的。所以,也要将数组设置为私有,并赋予它SerializeField属性,以便Unity在编辑器中公开并保存它。

继续,添一些颜色!确保至少有一个条目,按从最高到最低的FPS顺序,最后一个条目为0 FPS。

(颜色配置)

在将颜色应用到Label之前,通过引入一个单独的显示方法来重构Update方法,该方法负责调整单个Label。

通过遍历数组找到正确的颜色,直到满足颜色的最小FPS为止。然后设置颜色并跳出循环。

为什么我的Label消失了?

因为该条目的颜色将其所有四个通道设置为零。这包括控制不透明度的alpha通道。如果你没修改改alpha通道,得到就是完全透明的Label。

(带颜色的FPS展示)

完成!现在享受一下你的帧率变慢吧!

0 人点赞