Unity Optimizați-vă jocul folosind Profiler

Performanța este un aspect cheie al oricărui joc și nicio surpriză, indiferent cât de bun este jocul, dacă funcționează prost pe mașina utilizatorului, nu se va simți la fel de plăcut.

Deoarece nu toată lumea are un computer sau un dispozitiv de ultimă generație (dacă vizați dispozitivul mobil), este important să aveți în vedere performanța pe parcursul întregului curs de dezvoltare.

Există mai multe motive pentru care jocul ar putea rula lent:

  • Redare (Prea multe rețele cu polietilenă ridicată, umbritoare complexe sau efecte de imagine)
  • Audio (în mare parte cauzat de setări incorecte de import audio)
  • Cod neoptimizat (Scripturi care conțin funcții care necesită performanță în locuri greșite)

În acest tutorial, voi arăta cum să vă optimizați codul cu ajutorul Unity Profiler.

Profiler

Din punct de vedere istoric, performanța de depanare în Unity a fost o sarcină plictisitoare, dar de atunci, a fost adăugată o nouă caracteristică, numită Profiler.

Profiler este un instrument din Unity care vă permite să identificați rapid blocajele din jocul dvs. prin monitorizarea consumului de memorie, ceea ce simplifică foarte mult procesul de optimizare.

Fereastra Unity Profiler

Performanță proastă

Performanța proastă se poate întâmpla în orice moment: să presupunem că lucrați la instanța inamică și când o plasați în scenă, funcționează bine, fără probleme, dar pe măsură ce generați mai mulți inamici, este posibil să observați fps (cadre pe secundă ) încep să scadă.

Verificați exemplul de mai jos:

În scenă, am un cub cu un script atașat, care mișcă cubul dintr-o parte în alta și afișează numele obiectului:

SC_ShowName.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);
    }
}

Privind statisticile, putem vedea că jocul rulează la 800+ fps, așa că abia dacă are vreun impact asupra performanței.

Dar să vedem ce se va întâmpla când duplicam Cubul de 100 de ori:

Fps a scăzut cu peste 700 de puncte!

NOTĂ: Toate testele au fost efectuate cu Vsync dezactivat

În general, este o idee bună să începeți optimizarea atunci când jocul începe să prezinte bâlbâială, îngheț sau fps-ul scade sub 120.

Cum se utilizează Profiler?

Pentru a începe să utilizați Profiler, veți avea nevoie de:

  • Începeți jocul apăsând pe Play
  • Deschideți Profiler accesând Fereastra -> Analiză -> Profiler (sau apăsați Ctrl + 7)

  • Va apărea o fereastră nouă care arată cam așa:

Fereastra Unity 3D Profiler

  • Ar putea părea intimidant la început (mai ales cu toate acele diagrame etc.), dar nu este partea la care ne vom uita.
  • Faceți clic pe fila Cronologie și schimbați-o în Ierarhie:

  • Veți observa 3 secțiuni (EditorLoop, PlayerLoop și Profiler.CollectEditorStats):

  • Extindeți PlayerLoop pentru a vedea toate părțile în care este cheltuită puterea de calcul (NOTĂ: Dacă valorile PlayerLoop nu se actualizează, faceți clic pe butonul "Clear" din partea de sus a ferestrei Profiler).

Pentru cele mai bune rezultate, direcționați personajul dvs. de joc către situația (sau locul) în care jocul întârzie cel mai mult și așteptați câteva secunde.

  • După ce ați așteptat puțin, opriți jocul și observați lista PlayerLoop

Trebuie să vă uitați la valoarea GC Alloc, care înseamnă Garbage Collection Allocation. Acesta este un tip de memorie care a fost alocat de component dar nu mai este necesar și așteaptă să fie eliberat de Garbage Collection. În mod ideal, codul nu ar trebui să genereze gunoi (sau să fie cât mai aproape de 0).

Timpul ms este, de asemenea, o valoare importantă, arată cât timp a durat codul să ruleze în milisecunde, așa că, în mod ideal, ar trebui să urmăriți să reduceți și această valoare (prin memorarea în cache a valorilor, evitând apelarea funcțiilor care necesită performanță la fiecare actualizare etc..).

Pentru a localiza mai repede piesele cu probleme, faceți clic pe coloana GC Alloc pentru a sorta valorile de la mai mare la mai mic)

  • În diagrama de utilizare CPU, faceți clic oriunde pentru a trece la acel cadru. Mai exact, trebuie să ne uităm la vârfuri, unde fps-ul a fost cel mai scăzut:

Diagrama de utilizare a procesorului Unity

Iată ce a dezvăluit Profiler:

GUI.Repaint alocă 45.4KB, ceea ce este destul de mult, extinzându-l a dezvăluit mai multe informații:

  • Arată că majoritatea alocărilor provin din metoda GUIUtility.BeginGUI() și OnGUI() în scriptul SC_ShowName, știind că putem începe optimizarea.

GUIUtility.BeginGUI() reprezintă o metodă OnGUI() goală (Da, chiar și metoda OnGUI() goală alocă destul de multă memorie).

Utilizați Google (sau alt motor de căutare) pentru a găsi numele pe care nu le recunoașteți.

Iată partea OnGUI() care trebuie optimizată:

    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);
    }

Optimizare

Să începem optimizarea.

Fiecare script SC_ShowName își apelează propria metodă OnGUI(), ceea ce nu este bun având în vedere că avem 100 de instanțe. Deci, ce se poate face în privința asta? Răspunsul este: Pentru a avea un singur script cu metoda OnGUI() care apelează metoda GUI pentru fiecare Cub.

  • Mai întâi, am înlocuit OnGUI() implicit în scriptul SC_ShowName cu public void GUIMethod() care va fi apelat dintr-un alt script:
    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);
    }
  • Apoi am creat un nou script și l-am numit 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 va fi atașat la un obiect aleatoriu din scenă și va apela toate metodele GUI.

  • Am trecut de la 100 de metode individuale OnGUI() la doar una, să apăsăm pe play și să vedem rezultatul:

  • GUIUtility.BeginGUI() acum alocă doar 368B în loc de 36.7KB, o mare reducere!

Cu toate acestea, metoda OnGUI() încă alocă memorie, dar din moment ce știm că apelează doar GUIMethod() din scriptul SC_ShowName, mergem direct la depanarea acelei metode.

Dar Profiler arată doar informații globale, cum vedem exact ce se întâmplă în cadrul metodei?

Pentru a depana în interiorul metodei, Unity are un API la îndemână numit Profiler.BeginSample

Profiler.BeginSample vă permite să capturați o anumită secțiune a scriptului, arătând cât timp a durat până a fost finalizată și câtă memorie a fost alocată.

  • Înainte de a folosi clasa Profiler în cod, trebuie să importam spațiul de nume UnityEngine.Profiling la începutul scriptului:
using UnityEngine.Profiling;
  • Eșantionul Profiler este capturat adăugând Profiler.BeginSample("SOME_NAME"); la începutul capturii și adăugând Profiler.EndSample(); la sfârșitul capturii, cum ar fi acest:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Deoarece nu știu ce parte a GUIMethod() cauzează alocări de memorie, am inclus fiecare linie în Profiler.BeginSample și Profiler.EndSample (Dar dacă metoda dvs. are o mulțime de linii, cu siguranță nu trebuie să includeți fiecare linie, împărțiți-o în bucăți egale și apoi lucrați de acolo).

Iată o metodă finală cu Profiler Samples implementate:

    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();
    }
  • Acum apăs pe Play și văd ce arată în Profiler:
  • Pentru comoditate, am căutat "sc_show_" în Profiler, deoarece toate mostrele încep cu acest nume.

  • Interesant... O mulțime de memorie este alocată în sc_show_names partea 3, care corespunde acestei părți a codului:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

După ceva Google, am descoperit că obținerea numelui Object alocă destul de multă memorie. Soluția este să atribuiți un nume de obiect unei variabile șir în void Start(), astfel va fi apelat o singură dată.

Iată codul optimizat:

SC_ShowName.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();
    }
}
  • Să vedem ce arată Profiler:

Toate probele alocă 0B, deci nu mai este alocată memorie.