千家信息网

Unity游戏开发的性能衡量方法

发表于:2026-11-03 作者:千家信息网编辑
千家信息网最后更新 2026年11月03日,这篇文章主要讲解了"Unity游戏开发的性能衡量方法",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Unity游戏开发的性能衡量方法"吧!建立原子核我们
千家信息网最后更新 2026年11月03日Unity游戏开发的性能衡量方法

这篇文章主要讲解了"Unity游戏开发的性能衡量方法",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"Unity游戏开发的性能衡量方法"吧!

建立原子核

我们需要一个测试场景。理想地涵盖高性能和低性能情况的一种。我建议我们通过将越来越多的核子融合在一起来创建原子核。随着细胞核变大,性能会变差。

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

我们可以使用默认球体和自定义Nucleon组件对核子建模。该组件可确保将刚体附着到其对象,然后将其简单地拉向原点。拉力的强度取决于可配置的吸引力和距中心的距离。

using UnityEngine;
[RequireComponent(typeof(Rigidbody))]public class Nucleon : MonoBehaviour {
public float attractionForce;
Rigidbody body;
void Awake () { body = GetComponent(); } 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?

使用FixedUpdate使产生的帧与帧速率无关。如果配置的生成之间的时间短于帧时间,则使用Update会导致生成延迟。并且由于此场景的重点是降低我们的帧速率,因此将发生这种情况。

您可以使用while循环而不是if检查来追赶错过的生成,但是当timeSinceLastSpawn意外将其设置为零时,这将导致无限的产卵循环。将产卵限制为每个固定时间步一次是明智的限制。

实际的生成包括三个步骤。挑选一个随机的预制件,实例化它,并在所需的距离上给它一个随机的位置。

void SpawnNucleon () {    Nucleon prefab = nucleonPrefabs[Random.Range(0, nucleonPrefabs.Length)];    Nucleon spawn = Instantiate(prefab);    spawn.transform.localPosition = Random.onUnitSphere * spawnDistance;  }
通过轰击建立核。

播放此场景应导致球体朝中心射击。它们会过一会儿,直到彼此碰撞到形成一个球为止。这个球将继续增长,物理计算将变得更加复杂,并且在某些时候您会注意到帧速率下降。

如果花费太长时间才能看到性能下降,则可以提高生成速度。通过增加时间比例来加快时间也可以。您可以通过" 编辑" /"项目设置" /"时间"找到它。您还可以减少固定时间步长,这将导致每秒更多的物理计算。

统一时间设置。
为什么在低时标下运动不流畅?

当时间刻度设置为较低的值(例如0.1)时,时间将非常缓慢地移动。由于固定时间步长是恒定的,因此这意味着物理系统的更新频率将降低。因此,物理对象将保持不动,直到发生固定的更新(每隔几帧仅更新一次)。

您可以通过增加时间刻度来减小固定时间步长来解决此问题。或者,您可以更改刚体部件的插值模式,以便它们在物理步骤之间插值,从而隐藏了较低的更新频率。

使用探查器

现在我们有了一个最终会降低任何机器的帧速率的场景,是时候测量实际性能了。您最快可以做的就是启用游戏视图的统计信息叠加。

游戏视图统计信息叠加层。

但是,那里显示的帧速率根本不准确,更像是一个粗略的猜测。通过Window / Profiler打开Unity的探查器,我们可以做得更好。探查器为我们提供了许多有用的信息,尤其是CPU使用率和内存数据。

分析器,显示大量的vsync时间。

如果启用了vsync,则一开始它可能会主导CPU图形。为了更好地了解场景需要多少CPU资源,请关闭vsync。您可以通过" 编辑" /"项目设置" /"质量"执行此操作。它位于" 其他"标题下的底部。

没有更多的vsync。

我现在的帧率非常高!

如果没有vsync,则对于简单的场景,可能会获得很高的帧速率,甚至超过100。这会给硬件造成不必要的压力。您可以通过设置Application.targetFrameRate属性通过代码强制使用最大帧速率来防止这种情况。请注意,即使退出播放模式,此设置仍会保留在编辑器中。将其设置为-1将消除限制。

现在,您可以更好地了解CPU使用情况。在我的情况下,物理需要最多的时间,接着是渲染,然后是我的脚本。即使一切随着球体数量的增加而变慢,这种情况也将在很长一段时间内保持不变。

没有vsync的探查器。

我们还有两个意想不到的发现。首先,偶尔会有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对象来处理用户输入,但是我们不需要它,因此可以将其删除。

UI对象层次结构。

我使用了默认的画布设置,除了我将其设置为像素完美。

像素完美的画布。

该面板用于为FPS标签创建半透明的黑色背景。这样,它将始终可读。我把它放在窗口的左上角。将其锚点设置为左上角,以便无论窗口的大小如何都将其保留在适当的位置。将其枢轴设置为(0,1),以方便放置。

用类似的方法将标签放置在面板内。将其设为水平和垂直居中的白色粗体文本。设计整个内容,使其恰好适合两位数。


构建UI。

现在我们需要将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(); }
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通道,则将获得完全透明的标签。

彩色的fps标签。

感谢各位的阅读,以上就是"Unity游戏开发的性能衡量方法"的内容了,经过本文的学习后,相信大家对Unity游戏开发的性能衡量方法这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

时间 标签 更新 对象 速率 生成 方法 组件 场景 核子 编辑器 性能 信息 可以通过 情况 缓冲区 颜色 缓冲 内存 字符 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 广州百单互联网科技有限公司 没上市的科技互联网公司 壹账通互联网科技 互联网科技logo设计 互联网金融和金融科技 滨湖区互联网智能科技产品供应 山东吉时兔互联网科技有限公司 互联网实体化科技化 团飞科技与伯堂互联网服务平台 金融科技与互联网金融相同和不同 互联网时代科技改变生活 南博望互联网科技有限公司 山东子淼互联网科技有限公司 互联网营销推广黑科技 张召忠谈中国互联网科技 新氧科技和美国互联网有关系吗 互联网科技接地气 华人运通互联网科技公司 宁波允成互联网科技有限公司 如何强化互联网金融科技 互联网科技金融公司 和信科技工业互联网 长沙茂晟互联网科技有限公司 众安科技工业互联网合作 互联网安防科技感 陕西云淼互联网科技公司 下一代互联网重大应用科技园 数字科技和互联网金融哪个好 黑龙江互联网科技公司 每日互联网科技新闻
0