Unity プロファイラーを使用してゲームを最適化する

パフォーマンスはあらゆるゲームの重要な側面であり、当然のことながら、ゲームがどれほど優れていても、ユーザーのマシンでの動作が悪ければ、ゲームはそれほど楽しく感じられません。

誰もがハイエンドの PC やデバイスを持っているわけではないため (モバイルを対象とする場合)、開発の全過程を通じてパフォーマンスを念頭に置くことが重要です。

ゲームの動作が遅い場合には、いくつかの理由が考えられます。

  • レンダリング (多すぎるハイポリメッシュ、複雑なシェーダ、または画像効果)
  • オーディオ (主に、間違った オーディオ インポート設定 が原因です)
  • 最適化されていないコード (パフォーマンスを要求する関数が間違った場所に含まれているスクリプト)

このチュートリアルでは、Unity プロファイラーを使用してコードを最適化する方法を説明します。

プロファイラー

これまで、Unity でのパフォーマンスのデバッグは退屈な作業でしたが、それ以来、Profiler と呼ばれる新しい機能が追加されました。

プロファイラーは Unity のツールで、メモリ消費を監視することでゲームのボトルネックを素早く特定できるため、最適化プロセスが大幅に簡素化されます。

Unityプロファイラーウィンドウ

悪いパフォーマンス

パフォーマンスの低下はいつでも発生する可能性があります。たとえば、敵のインスタンスで作業していて、それをシーンに配置すると、問題なく正常に動作しますが、より多くの敵を生成すると、fps (フレーム/秒) が低下することに気づくかもしれません。 ) 下がり始めます。

以下の例を確認してください。

シーンには、Cube を左右に移動してオブジェクト名を表示するスクリプトがアタッチされた Cube があります。

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 ポイント以上低下しました。

注: すべてのテストは、Vsync を無効にして実行されました。

一般に、ゲームが途切れたり、フリーズしたり、fps が 120 を下回ったりし始めたときに最適化を開始することをお勧めします。

プロファイラーの使い方

プロファイラーの使用を開始するには、次のものが必要です。

  • 「Play」を押してゲームを開始します
  • [ウィンドウ] -> [分析] -> [プロファイラー] に移動してプロファイラーを開きます (または Ctrl + 7 を押します)。

  • 次のような新しいウィンドウが表示されます。

Unity 3D プロファイラーウィンドウ

  • 最初は怖く見えるかもしれません (特にこれらすべてのグラフなど) が、これから検討する部分ではありません。
  • [タイムライン] タブをクリックし、[階層] に変更します。

  • 3 つのセクション (EditorLoop、PlayerLoop、Profiler.CollectEditorStats) があることがわかります。

  • PlayerLoop を展開すると、計算能力が費やされているすべての部分が表示されます (注: PlayerLoop の値が更新されない場合は、プロファイラー ウィンドウの上部にある "Clear" ボタンをクリックしてください)。

最良の結果を得るには、ゲームのキャラクターをゲームが最も遅れている状況 (または場所) に誘導し、数秒待ちます。

  • 少し待った後、ゲームを停止し、PlayerLoop リストを観察します。

GC Alloc 値を確認する必要があります。これはガベージ コレクション割り当てを表します。これは、component によって割り当てられたメモリのタイプですが、不要になり、ガベージ コレクションによって解放されるのを待っています。理想的には、コードはガベージを生成しない (または可能な限り 0 に近づける) 必要があります。

時間 ms も重要な値で、コードの実行にかかった時間をミリ秒単位で示します。そのため、理想的には、この値も減らすことを目指す必要があります (値をキャッシュする、更新ごとにパフォーマンスを要求する関数の呼び出しを避けるなど)。 。)。

問題のある部分をより早く見つけるには、[GC Alloc] 列をクリックして値を高い順に並べ替えます)。

  • CPU 使用率グラフの任意の場所をクリックして、そのフレームにスキップします。具体的には、fps が最も低かったピークを確認する必要があります。

Unity CPU 使用率チャート

プロファイラーが明らかにしたことは次のとおりです。

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 個あることを考えると、これは適切ではありません。それで、それに対して何ができるでしょうか?答えは、各キューブの GUI メソッドを呼び出す OnGUI() メソッドを含む単一のスクリプトを用意することです。

  • まず、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_GUIメソッド.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() メソッドがあった状態から 1 つだけになったので、再生ボタンを押して結果を見てみましょう。

  • GUIUtility.BeginGUI() は 36.7KB ではなく 368B のみを割り当てるようになり、大幅に削減されました。

ただし、OnGUI() メソッドはまだメモリを割り当てていますが、SC_ShowName スクリプトから GUIMethod() を呼び出しているだけであることがわかっているため、このメソッドのデバッグに直接進みます。

しかし、プロファイラーはグローバル情報のみを表示します。メソッド内で何が起こっているのかを正確に確認するにはどうすればよいでしょうか?

メソッド内でデバッグするために、Unity には Profiler.BeginSample という便利な API があります。

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 で各行を囲みました (ただし、メソッドの行数が多い場合は、囲む必要はありません)各行を均等なチャンクに分割し、そこから作業します)。

プロファイラー サンプルが実装された最終的なメソッドを次に示します。

    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() の文字列変数に割り当てることです。そうすれば、オブジェクトは 1 回だけ呼び出されます。

最適化されたコードは次のとおりです。

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();
    }
}
  • プロファイラーが何を表示しているかを見てみましょう。

すべてのサンプルは 0B を割り当てているため、それ以上のメモリは割り当てられません。