您好,登录后才能下订单哦!
这篇文章主要讲解了“Unity游戏开发的性能衡量方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Unity游戏开发的性能衡量方法”吧!
建立原子核
我们需要一个测试场景。理想地涵盖高性能和低性能情况的一种。我建议我们通过将越来越多的核子融合在一起来创建原子核。随着细胞核变大,性能会变差。
核子将是简单的球体,将被吸引到场景的中心,在那里它们会聚成一个球。这当然不是原子的正确表示,但这不是重点。
我们可以使用默认球体和自定义Nucleon
组件对核子建模。该组件可确保将刚体附着到其对象,然后将其简单地拉向原点。拉力的强度取决于可配置的吸引力和距中心的距离。
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
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); spawn.transform.localPosition = Random.onUnitSphere * spawnDistance; }
播放此场景应导致球体朝中心射击。它们会过一会儿,直到彼此碰撞到形成一个球为止。这个球将继续增长,物理计算将变得更加复杂,并且在某些时候您会注意到帧速率下降。
如果花费太长时间才能看到性能下降,则可以提高生成速度。通过增加时间比例来加快时间也可以。您可以通过“ 编辑” /“项目设置” /“时间”找到它。您还可以减少固定时间步长,这将导致每秒更多的物理计算。
使用探查器
现在我们有了一个最终会降低任何机器的帧速率的场景,是时候测量实际性能了。您最快可以做的就是启用游戏视图的统计信息叠加。
但是,那里显示的帧速率根本不准确,更像是一个粗略的猜测。通过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;
[RequireComponent(typeof(FPSCounter))]
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)]; }
通过使用可能需要的每个数字的固定字符串表示形式数组,我们消除了所有临时字符串分配!
每秒平均帧数
每帧更新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放在中间。
给标签上色
作为FPS标签的最后修饰,我们可以为它们着色。这可以通过将颜色与FPS值关联来完成。这样的关联可以用自定义结构表示。
[System.Serializable] private struct FPSColor { public Color color; public int minimumFPS; }
作为FPSDisplay
唯一将使用此结构的东西,我们将struct定义直接放在该类内,并将其私有化,这样它就不会显示在全局名称空间中。使它可序列化,以便可以由Unity编辑器公开。
现在添加这些结构的数组,以便我们可以配置FPS标签的颜色。我们通常会为此添加一个公共字段,但是由于结构本身是私有的,因此我们不能这样做。因此,也将数组设为私有并为其赋予SerializeField
属性,以便Unity在编辑器中公开并保存它。
[SerializeField] 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通道,则将获得完全透明的标签。
感谢各位的阅读,以上就是“Unity游戏开发的性能衡量方法”的内容了,经过本文的学习后,相信大家对Unity游戏开发的性能衡量方法这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。