Unity 使用 Profiler 优化您的游戏

性能是任何游戏的一个关键方面,毫不奇怪,无论游戏有多好,如果它在用户的机器上运行不佳,它就会感觉不那么愉快。

由于并非每个人都拥有高端 PC 或设备(如果您的目标是移动设备),因此在整个开发过程中牢记性能非常重要。

游戏运行缓慢的原因有多种:

  • 渲染(过多的高多边形网格、复杂的着色器或图像效果)
  • 音频(主要是由不正确的音频导入设置引起的)
  • 未优化的代码(在错误位置包含性能要求函数的脚本)

在本教程中,我将展示如何在 Unity Profiler 的帮助下优化代码。

分析器

从历史上看,在 Unity 中调试性能是一项繁琐的任务,但从那时起,添加了一个新功能,称为 Profiler

Profiler 是 Unity 中的一个工具,可让您通过监控内存消耗来快速查明游戏中的瓶颈,从而大大简化优化过程。

Unity 分析器窗口

表现不佳

性能不佳随时可能发生:假设您正在处理敌人实例,当您将其放置在场景中时,它可以正常工作,没有任何问题,但当您生成更多敌人时,您可能会注意到 fps(每秒帧数) )开始下降。

检查下面的示例:

在场景中,我有一个附有脚本的立方体,该脚本将立方体从一侧移动到另一侧并显示对象名称:

SC_显示名称.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
}

从统计数据来看,我们可以看到游戏运行在 800+ fps 的水平上,因此对性能几乎没有任何影响。

但是让我们看看当我们复制 Cube 100 次时会发生什么:

FPS下降700多点!

注意:所有测试都是在禁用垂直同步的情况下完成的

一般来说,当游戏开始出现卡顿、冻结或 fps 降至 120 以下时,开始优化是个好主意。

如何使用分析器?

要开始使用 Profiler,您需要:

  • 按 Play 开始游戏
  • 通过转到“窗口”->“分析”->“探查器”(或按 Ctrl + 7)打开探查器

  • 将出现新窗口,如下所示:

Unity 3D 分析器窗口

  • 一开始它可能看起来很吓人(尤其是所有这些图表等),但这不是我们要查看的部分。
  • 单击“时间轴”选项卡并将其更改为“层次结构”:

  • 您会注意到 3 个部分(EditorLoop、PlayerLoop 和 Profiler.CollectEditorStats):

  • 展开 PlayerLoop 以查看消耗计算能力的所有部分(注意:如果 PlayerLoop 值未更新,请单击 Profiler 窗口顶部的 "Clear" 按钮)。

为了获得最佳结果,请将您的游戏角色引导至游戏最滞后的情况(或地点)并等待几秒钟。

  • 稍等片刻后,停止游戏并观察 PlayerLoop 列表

您需要查看 GC Alloc 值,它代表垃圾收集分配。这是一种已由 component 分配但不再需要并等待垃圾收集释放的内存类型。理想情况下,代码不应生成任何垃圾(或尽可能接近 0)。

时间 ms 也是一个重要的值,它显示了代码运行所花费的时间(以毫秒为单位),因此理想情况下,您也应该致力于减少该值(通过缓存值、避免每次更新调用对性能要求较高的函数等).)。

为了更快地找到麻烦的部分,点击GC Alloc栏,将值从高到低排序)

  • 在 CPU 使用率图表中,单击任意位置即可跳到该帧。具体来说,我们需要查看 fps 最低的峰值:

Unity CPU 使用率图表

下面是 Profiler 揭示的内容:

GUI.Repaint 正在分配 45.4KB,这相当多,扩展它会显示更多信息:

  • 它表明大部分分配来自 SC_ShowName 脚本中的 GUIUtility.BeginGUI() 和 OnGUI() 方法,知道我们可以开始优化。

GUIUtility.BeginGUI() 表示一个空的 OnGUI() 方法(是的,即使是空的 OnGUI() 方法也会分配相当多的内存)。

使用 Google(或其他搜索引擎)查找您不认识的名字。

下面是需要优化的OnGUI()部分:

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }

优化

让我们开始优化。

每个 SC_ShowName 脚本都调用自己的 OnGUI() 方法,考虑到我们有 100 个实例,这不太好。那么对此可以做什么呢?答案是:使用带有 OnGUI() 方法的单个脚本来调用每个多维数据集的 GUI 方法。

  • 首先,我将 SC_ShowName 脚本中的默认 OnGUI() 替换为 public void GUIMethod(),它将从另一个脚本中调用:
    public void GUIMethod()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
  • 然后我创建了一个新脚本并将其命名为 SC_GUIMethod:

SC_GUIMethod.cs

using UnityEngine;

public class SC_GUIMethod : MonoBehaviour
{
    SC_ShowName[] instances; //All instances where GUI method will be called

    void Start()
    {
        //Find all instances
        instances = FindObjectsOfType<SC_ShowName>();
    }

    void OnGUI()
    {
        for(int i = 0; i < instances.Length; i++)
        {
            instances[i].GUIMethod();
        }
    }
}

SC_GUIMethod 将附加到场景中的随机对象并调用所有 GUI 方法。

  • 我们从拥有 100 个单独的 OnGUI() 方法变成只有一个,让我们按播放键看看结果:

  • GUIUtility.BeginGUI() 现在只分配 368B 而不是 36.7KB,大大减少了!

但是,OnGUI() 方法仍在分配内存,但由于我们知道它仅从 SC_ShowName 脚本调用 GUIMethod(),因此我们将直接调试该方法。

但是Profiler只显示全局信息,我们如何查看方法内部究竟发生了什么?

为了在方法内部进行调试,Unity 有一个方便的 API,名为 Profiler.BeginSample

Profiler.BeginSample 允许您捕获脚本的特定部分,显示完成所需的时间以及分配了多少内存。

  • 在代码中使用Profiler类之前,我们需要在脚本开头导入UnityEngine.Profiling命名空间:
using UnityEngine.Profiling;
  • 通过在捕获开始处添加 Profiler.BeginSample("SOME_NAME"); 并在捕获结束处添加 Profiler.EndSample(); 来捕获分析器示例,例如这:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

由于我不知道 GUIMethod() 的哪一部分导致内存分配,因此我将每一行包含在 Profiler.BeginSample 和 Profiler.EndSample 中(但是如果您的方法有很多行,则绝对不需要包含每行,只需将其分成均匀的块,然后从那里开始工作)。

这是实现了 Profiler Samples 的最终方法:

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
        Profiler.EndSample();
    }
  • 现在我按“播放”并查看分析器中显示的内容:
  • 为了方便起见,我在探查器中搜索 "sc_show_",因为所有示例都以该名称开头。

  • 有趣... sc_show_names 第 3 部分分配了大量内存,对应于这部分代码:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

经过一番谷歌搜索后,我发现获取对象的名称会分配相当多的内存。解决方案是在 void Start() 中将对象的名称分配给字符串变量,这样它只会被调用一次。

这是优化后的代码:

SC_显示名称.cs

using UnityEngine;
using UnityEngine.Profiling;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    string objectName = "";

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
        objectName = gameObject.name; //Store Object name to a variable
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
        Profiler.EndSample();
    }
}
  • 让我们看看 Profiler 显示了什么:

所有样本都分配 0B,因此不再分配内存。

推荐文章
Unity 的优化技巧
如何在Unity中使用更新
Unity 的广告牌生成器
提高 Unity 中移动游戏的性能
Unity 音频剪辑导入设置以获得最佳性能
如何成为一名更好的 Unity 程序员
游戏设计的基本概念