喵的Unity游戏开发之路 - 帧每秒:性能衡量
使用物理学来创建不断增长的原子核。
使用探查器调查性能。
测量并显示帧频。
防止创建临时字符串。
通过平均多个帧来稳定帧速率。
对帧频显示着色。
在本教程中,我们将创建一个简单的测试场景,然后测量其性能。我们将首先检查分析器,然后创建自己的帧速率计数器。
本教程需要基本了解Unity中的脚本。它适用于Unity 5.0.1及更高版本。如果您是新手,请先看看构建全彩随机3D分形。
建立原子核
我们需要一个测试场景。理想地涵盖高性能和低性能情况的一种。我建议我们通过将越来越多的核子融合在一起来创建原子核。随着细胞核变大,性能会变差。
核子将是简单的球体,将被吸引到场景的中心,在那里它们会聚成一个球。这当然不是原子的正确表示,但这不是重点。
我们可以使用默认球体和自定义Nucleon
组件对核子建模。该组件可确保将刚体附着到其对象,然后将其简单地拉向原点。拉力的强度取决于可配置的吸引力和距中心的距离。
using UnityEngine;
[ ]
public class Nucleon : MonoBehaviour {
public float attractionForce;
Rigidbody body;
void Awake () {
body = GetComponent<Rigidbody>();
}
void FixedUpdate () {
body.AddForce(transform.localPosition * -attractionForce);
}
}
是的,我现在省略了字段和方法声明中的private修饰符,因为默认情况下它们是私有的。让我们看看它如何进行,无论它是否令人困惑。
使用球体创建两个核子预制体,一个用于质子,另一个用于中子。为每种材料提供不同的材料,以使它们看起来不同。我们只满足一种核子类型就足够了,但是这很无聊。
预制件是场景中不存在且尚未激活的Unity对象(或对象层次)。您将其用作模板,创建它的克隆并将其添加到场景中。要创建一个对象,请照常在场景中构造一个对象,然后将其拖到项目窗口中。场景对象将成为预制实例,如果不再需要它可以将其删除。
要生成这些核子,我们需要创建另一个组件NucleonSpawner
。它需要知道生成之间的时间间隔,离生成中心有多远以及生成什么。
using UnityEngine;
public class NucleonSpawner : MonoBehaviour {
public float timeBetweenSpawns;
public float spawnDistance;
public Nucleon[] nucleonPrefabs;
}
创建一个空的游戏对象,附加一个NucleonSpawner
组件,然后根据需要对其进行配置。
要定期生成,我们需要跟踪自上次生成以来的时间。我们可以用一种简单的FixedUpdate
方法来做到这一点。
float timeSinceLastSpawn;
void FixedUpdate () {
timeSinceLastSpawn += Time.deltaTime;
if (timeSinceLastSpawn >= timeBetweenSpawns) {
timeSinceLastSpawn -= timeBetweenSpawns;
SpawnNucleon();
}
}
使用FixedUpdate使产生的帧与帧速率无关。如果配置的生成之间的时间短于帧时间,则使用Update会导致生成延迟。并且由于此场景的重点是降低我们的帧速率,因此将发生这种情况。
您可以使用while循环而不是if检查来追赶错过的生成,但是当timeSinceLastSpawn意外将其设置为零时,这将导致无限的产卵循环。将产卵限制为每个固定时间步一次是明智的限制。
实际的生成包括三个步骤。挑选一个随机的预制件,实例化它,并在所需的距离上给它一个随机的位置。
void SpawnNucleon () {
Nucleon prefab = nucleonPrefabs[Random.Range(0, nucleonPrefabs.Length)];
Nucleon spawn = Instantiate<Nucleon>(prefab);
Random.onUnitSphere * spawnDistance; =
}
播放此场景应导致球体朝中心射击。它们会过一会儿,直到彼此碰撞到形成一个球为止。这个球将继续增长,物理计算将变得更加复杂,并且在某些时候您会注意到帧速率下降。
如果花费太长时间才能看到性能下降,则可以提高生成速度。通过增加时间比例来加快时间也可以。您可以通过“ 编辑” /“项目设置” /“时间”找到它。您还可以减少固定时间步长,这将导致每秒更多的物理计算。
当时间刻度设置为较低的值(例如0.1)时,时间将非常缓慢地移动。由于固定时间步长是恒定的,因此这意味着物理系统的更新频率将降低。因此,物理对象将保持不动,直到发生固定的更新(每隔几帧仅更新一次)。
您可以通过增加时间刻度来减小固定时间步长来解决此问题。或者,您可以更改刚体部件的插值模式,以便它们在物理步骤之间插值,从而隐藏了较低的更新频率。
https://github.com/zhouwensi/catlikecoding/raw/master/fps-01-building-an-atomic-nucleus.unitypackage
使用探查器
现在我们有了一个最终会降低任何机器的帧速率的场景,是时候测量实际性能了。您最快可以做的就是启用游戏视图的统计信息叠加。
但是,那里显示的帧速率根本不准确,更像是一个粗略的猜测。通过Window / Profiler打开Unity的探查器,我们可以做得更好。探查器为我们提供了许多有用的信息,尤其是CPU使用率和内存数据。
如果启用了vsync,则一开始它可能会主导CPU图形。为了更好地了解场景需要多少CPU资源,请关闭vsync。您可以通过“ 编辑” /“项目设置” /“质量”执行此操作。它位于“ 其他”标题下的底部。
如果没有vsync,则对于简单的场景,可能会获得很高的帧速率,甚至超过100。这会给硬件造成不必要的压力。您可以通过设置Application.targetFrameRate属性通过代码强制使用最大帧速率来防止这种情况。请注意,即使退出播放模式,此设置仍会保留在编辑器中。将其设置为-1将消除限制。
现在,您可以更好地了解CPU使用情况。在我的情况下,物理需要最多的时间,接着是渲染,然后是我的脚本。即使一切随着球体数量的增加而变慢,这种情况也将在很长一段时间内保持不变。
我们还有两个意想不到的发现。首先,偶尔会有CPU使用率飙升。其次,内存图显示了频繁的GC分配峰值,这表明存在正在分配并随后释放的内存。由于我们只是在创建新对象而从不丢弃任何东西,所以这很奇怪。
这两种现象都是由Unity编辑器引起的。每当在编辑器中选择某些内容时,就会发生CPU峰值。内存分配是由编辑器调用GameView.GetMainGameViewRenderRect引起的。还有额外的开销,特别是如果同时显示游戏视图和场景视图。简而言之,编辑器本身会干扰我们的测量。
您仍然可以从编辑器内分析中获得大量有用的信息,但是如果您想从测量中消除编辑器本身,则必须进行独立构建。如果您进行开发,甚至在运行应用程序时自动连接到探查器,您仍然可以使用探查器。您可以通过“ 文件/构建设置”进行配置...
对独立构建进行概要分析时,数据看起来完全不同。现在,内存分配仅由产生的核子引起,并且不再发生垃圾回收。就我而言,渲染需要花费更多时间,因为我是在全屏模式下运行应用程序,而不是在小游戏视图中运行。此外,脚本是如此微不足道,以至于它们甚至在图形中都不可见。
每秒测量帧
探查器为我们提供了有用的信息,但仍然不能很好地衡量帧速率。显示的FPS数仅是1除以CPU时间,这不是我们得到的实际帧速率。因此,让我们自己衡量一下。
我们需要一个简单的组件来告诉我们应用程序正在运行的当前每秒帧数。一个公共财产就足够了。我们将其设为整数,因为我们实际上不需要小数精度。
using UnityEngine;
public class FPSCounter : MonoBehaviour {
public int FPS { get; private set; }
}
请记住,属性是伪装成字段的方法。我们提供FPS作为公共信息,但只有组件本身需要更新值。使用的语法是自动生成的属性的简写形式,看起来像这样。
int fps;
public int FPS {
get { return fps; }
private set { fps = value; }
}
这个简写不适用于Unity的序列化,但这很好,因为我们仍然不需要保存FPS值。
我们通过将1除以当前帧的时间增量来测量每次更新的每秒帧数。我们将结果转换为整数,有效地四舍五入。
void Update () {
FPS = (int)(1f / Time.deltaTime);
}
但是,这种方法存在问题。时间增量不是处理最后一帧所花费的实际时间,它受当前时间比例的影响。这意味着除非将时间标度设置为1,否则我们的FPS将是错误的。幸运的是,我们还可以向Unity请求未标度的时间增量。
void Update () {
FPS = (int)(1f / Time.unscaledDeltaTime);
}
需要某种UI来显示FPS。让我们使用Unity的UI。创建一个内部带有面板的画布,该面板又包含一个文本对象。这些可以通过GameObject / UI子菜单添加。添加画布时,您还将获得一个EventSystem对象来处理用户输入,但是我们不需要它,因此可以将其删除。
我使用了默认的画布设置,除了我将其设置为像素完美。
该面板用于为FPS标签创建半透明的黑色背景。这样,它将始终可读。我把它放在窗口的左上角。将其锚点设置为左上角,以便无论窗口的大小如何都将其保留在适当的位置。将其枢轴设置为(0,1),以方便放置。
用类似的方法将标签放置在面板内。将其设为水平和垂直居中的白色粗体文本。设计整个内容,使其恰好适合两位数。
现在我们需要将FPS值绑定到标签。为此,我们创建一个组件。它需要一个FPSCounter
组件来从中检索值,并需要引用UnityEngine.UI命名空间中的Text
标签以将值分配给它。
using UnityEngine;
using UnityEngine.UI;
[ ]
public class FPSDisplay : MonoBehaviour {
public Text fpsLabel;
}
将此组件添加到面板中并进行连接。我们将其附加到面板上,因为这是整个FPS显示屏,而不是标签。稍后我们将包含更多标签。
显示组件只需在每一帧更新标签的文本。让我们缓存对计数器的引用,这样就不必每次都调用GetComponent。
FPSCounter fpsCounter;
void Awake () {
fpsCounter = GetComponent<FPSCounter>();
}
void Update () {
fpsLabel.text = fpsCounter.FPS.ToString();
}
FPS标签现在正在更新!但是,由于我们将其设计为两位数,因此只要我们的帧速率超过99每秒,它就会显示无用的值。因此,让我们限制显示的值。无论如何,99以上的表现都足够好。
void Update () {
fpsLabel.text =Mathf.Clamp(fpsCounter.FPS, 0, 99).ToString();
}
现在一切似乎都可以正常工作,但是存在一个细微的问题。现在,我们在每次更新时都创建一个新的字符串对象,在下次更新时将其丢弃。这会污染托管内存,这将触发垃圾回收器。尽管这对于台式机应用程序来说并不是什么大问题,但对于几乎没有可用内存的设备而言,这更为麻烦。它还会污染我们的探查器数据,这在您寻找分配时很烦人。
我们可以摆脱这些临时字符串吗?我们显示的值可以是0到99之间的任何整数。这是100个不同的字符串。为什么不一次创建所有这些字符串并重用它们,而不是始终重新创建相同的内容?
static string[] stringsFrom00To99 = {
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99"
};
void Update () {
fpsLabel.text =stringsFrom00To99[Mathf.Clamp(fpsCounter.FPS, 0, 99)];
}
通过使用可能需要的每个数字的固定字符串表示形式数组,我们消除了所有临时字符串分配!
https://github.com/zhouwensi/catlikecoding/raw/master/fps-03-measuring-frames-per-second.unitypackage
每秒平均帧数
每帧更新FPS值会有不利的副作用。当帧频不稳定时,标签将不断波动,从而难以获得有用的读数。我们只能偶尔更新一次标签,但是这样一来,我们就不会再对帧频的表现有任何印象。
一种可能的解决方案是平均帧速率,以平滑突然变化的影响,产生较小的抖动值。让我们进行调整FPSCounter
,使其在可配置的帧范围内执行此操作。将此值设置为1等于根本不求平均值,因此实际上是可选的。
public int frameRange = 60;
让我们将属性名称从更改FPS
为AverageFPS
,因为这是对其现在表示的值的更好描述。您可以使用IDE重构名称,也可以手动更新显示组件以使用新名称。
public intAverageFPS{ get; private set; }
现在,我们需要一个缓冲区来存储多个帧的FPS值,以及一个索引,以便我们知道将下一帧的数据放在何处。
int[] fpsBuffer;
int fpsBufferIndex;
初始化此缓冲区时,请确保该frameRange
值至少为1,并将索引设置为0。
void InitializeBuffer () {
if (frameRange <= 0) {
frameRange = 1;
}
fpsBuffer = new int[frameRange];
fpsBufferIndex = 0;
}
该Update
方法变得有点复杂。它是从初始化缓冲区(如果需要)开始的,这可能是因为我们刚刚启动,还是因为frameRange
已更改。然后必须更新缓冲区,然后才能计算平均FPS。
void Update () {
if (fpsBuffer == null || fpsBuffer.Length != frameRange) {
InitializeBuffer();
}
UpdateBuffer();
CalculateFPS();
}
通过将当前FPS存储在当前索引处来完成对缓冲区的更新,然后将其递增。
void UpdateBuffer () {
fpsBuffer[fpsBufferIndex++] = (int)(1f / Time.unscaledDeltaTime);
}
但是我们很快就会填满整个缓冲区,然后呢?在添加新值之前,我们必须丢弃最旧的值。我们可以将所有值移动一个位置,但是平均值并不关心值的顺序。因此,我们可以将索引回绕到数组的开头。这样,一旦缓冲区被填满,我们总是用最新的值覆盖最旧的值。
void UpdateBuffer () {
fpsBuffer[fpsBufferIndex++] = (int)(1f / Time.unscaledDeltaTime);
if (fpsBufferIndex >= frameRange) {
fpsBufferIndex = 0;
}
}
计算平均值是将缓冲区中的所有值相加并除以值量的简单问题。
void CalculateFPS () {
int sum = 0;
for (int i = 0; i < frameRange; i++) {
sum += fpsBuffer[i];
}
AverageFPS = sum / frameRange;
}
现在,我们的平均帧速率有效,并且在合理的帧范围内,轻松获得良好的阅读效果非常容易。但是我们可以做得更好。由于我们现在具有来自多个帧的数据,因此我们也可以公开此范围内的最高和最低FPS。这给了我们比平均值更多的信息。
public int HighestFPS { get; private set; }
public int LowestFPS { get; private set; }
我们可以在计算总和的同时找到这些值。
void CalculateFPS () {
int sum = 0;
int highest = 0;
int lowest = int.MaxValue;
for (int i = 0; i < frameRange; i++) {
int fps =fpsBuffer[i];
sum +=fps;
if (fps > highest) {
highest = fps;
}
if (fps < lowest) {
lowest = fps;
}
}
AverageFPS = sum / frameRange;
HighestFPS = highest;
LowestFPS = lowest;
}
现在,我们的FPSDisplay组件可以绑定两个附加标签。
public Text highestFPSLabel, averageFPSLabel, lowestFPSLabel;
void Update () {
highestFPSLabel.text =
stringsFrom00To99[Mathf.Clamp(fpsCounter.HighestFPS, 0, 99)];
averageFPSLabel.text =
stringsFrom00To99[Mathf.Clamp(fpsCounter.AverageFPS, 0, 99)];
lowestFPSLabel.text =
stringsFrom00To99[Mathf.Clamp(fpsCounter.LowestFPS, 0, 99)];
}
在用户界面中再添加两个标签,然后将它们全部连接起来。我将最高FPS放在顶部,将最低FPS放在底部,将平均FPS放在中间。
https://github.com/zhouwensi/catlikecoding/raw/master/fps-04-averaging-frames-per-second.unitypackage
给标签上色
作为FPS标签的最后修饰,我们可以为它们着色。这可以通过将颜色与FPS值关联来完成。这样的关联可以用自定义结构表示。
[ ]
private struct FPSColor {
public Color color;
public int minimumFPS;
}
作为FPSDisplay
唯一将使用此结构的东西,我们将struct定义直接放在该类内,并将其私有化,这样它就不会显示在全局名称空间中。使它可序列化,以便可以由Unity编辑器公开。
现在添加这些结构的数组,以便我们可以配置FPS标签的颜色。我们通常会为此添加一个公共字段,但是由于结构本身是私有的,因此我们不能这样做。因此,也将数组设为私有并为其赋予SerializeField
属性,以便Unity在编辑器中公开并保存它。
[ ]
private FPSColor[] coloring;
继续添加一些颜色!确保至少有一个条目,从最高FPS到最低FPS进行排序,最后一个条目为0 FPS。
在将颜色应用于标签之前,Update
通过引入一个单独的Display
方法来调整方法,以调整单个标签。
void Update () {
Display(highestFPSLabel,fpsCounter.HighestFPS);
Display(averageFPSLabel,fpsCounter.AverageFPS);
Display(lowestFPSLabel,fpsCounter.LowestFPS);
}
void Display (Text label, int fps) {
label.text = stringsFrom00To99[Mathf.Clamp(fps, 0, 99)];
}
可以通过遍历阵列直到找到某种颜色的最低FPS来找到正确的颜色。然后设置颜色并跳出循环。
void Display (Text label, int fps) {
label.text = stringsFrom00To99[Mathf.Clamp(fps, 0, 99)];
for (int i = 0; i < coloring.Length; i++) {
if (fps >= coloring[i].minimumFPS) {
label.color = coloring[i].color;
break;
}
}
}
默认颜色的所有四个通道都设置为零。这包括控制不透明度的Alpha通道。如果您尚未更改Alpha通道,则将获得完全透明的标签。
做完了!欣赏彩色的FPS坦克!
https://github.com/zhouwensi/catlikecoding/raw/master/fps-05-coloring-the-labels.unitypackage
恭喜你,喵的Unity游戏开发之路系列教程的基础部分完结啦!
下一个教程开始我们的全新章节 - 移动,第一节:滑动球体 - 玩家控制的移动!
往期精选
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/frames-per-second/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes
本文分享自微信公众号 - Unity3D游戏开发精华教程干货(u3dnotes)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
什么?Python Celery 也能调度Go worker?!
我们曾经研究过如何让Python和Go互相调度,当时发现,将Go语言写的模块打包成动态链接库,就能在Python中进行调度: 优劣互补! Python+Go结合开发的探讨 Go的优势很明显,从1亿减到1,在我的设备上测试,用Go运行只需要50ms,Python可能需要接近100倍的时间。 但是,这种写法也有缺点:实在太麻烦了,大大增加了整个项目的耦合性。 那Python中有没有办法不通过打包成动态链接库的方法,用Python调度Go的任务呢?答案是Go celery. https://github.com/gocelery/gocelery 我们可以用Go写一个计算密集型任务的Worker,然后用Python的Celery beat来调度这个Worker,下面给大家演示一下: 1.编写Go Worker 最好是将计算密集型的任务改造成Go语言版的,这样收益才能最大化。 比如这里,我使用的是上回从1亿减到1的老梗。 PS,别被下面这段代码吓到了,其实大部分是可以去掉的配置项,核心代码就几行。 输入命令: go run main.go 即可运行该worker 2.编写Python客户端 每...
- 下一篇
图解!24张图彻底弄懂九大常见数据结构!
数据结构想必大家都不会陌生,对于一个成熟的程序员而言,熟悉和掌握数据结构和算法也是基本功之一。数据结构本身其实不过是数据按照特点关系进行存储或者组织的集合,特殊的结构在不同的应用场景中往往会带来不一样的处理效率。 常用的数据结构可根据数据访问的特点分为线性结构和非线性结构。线性结构包括常见的链表、栈、队列等,非线性结构包括树、图等。数据结构种类繁多,本文将通过图解的方式对常用的数据结构进行理论上的介绍和讲解,以方便大家掌握常用数据结构的基本知识。 本文提纲 1 数组 数组可以说是最基本最常见的数据结构。数组一般用来存储相同类型的数据,可通过数组名和下标进行数据的访问和更新。数组中元素的存储是按照先后顺序进行的,同时在内存中也是按照这个顺序进行连续存放。数组相邻元素之间的内存地址的间隔一般就是数组数据类型的大小。 2链表 链表相较于数组,除了数据域,还增加了指针域用于构建链式的存储数据。链表中每一个节点都包含此节点的数据和指向下一节点地址的指针。由于是通过指针进行下一个数据元素的查找和访问,使得链表的自由度更高。 这表现在对节点进行增加和删除时,只需要对上一节点的指针地址进行修改,而无需...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS关闭SELinux安全模块
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Windows10,CentOS7,CentOS8安装Nodejs环境
- 设置Eclipse缩进为4个空格,增强代码规范