Unity Optimera ditt spel med Profiler

Prestanda är en nyckelaspekt i alla spel och ingen överraskning, oavsett hur bra spelet är, om det fungerar dåligt på användarens maskin kommer det inte att kännas lika roligt.

Eftersom inte alla har en avancerad PC eller enhet (om du är inriktad på mobil), är det viktigt att ha prestanda i åtanke under hela utvecklingsförloppet.

Det finns flera anledningar till att spelet kan köras långsamt:

  • Återgivning (för många högpolymaskor, komplexa skuggningar eller bildeffekter)
  • Ljud (mest orsakat av felaktiga ljudimportinställningar)
  • Ooptimerad kod (skript som innehåller prestandakrävande funktioner på fel ställen)

I den här handledningen kommer jag att visa hur du optimerar din kod med hjälp av Unity Profiler.

Profiler

Historiskt sett var felsökningsprestanda i Unity en tråkig uppgift, men sedan dess har en ny funktion lagts till, kallad Profiler.

Profiler är ett verktyg i Unity som låter dig snabbt lokalisera flaskhalsarna i ditt spel genom att övervaka minnesförbrukningen, vilket avsevärt förenklar optimeringsprocessen.

Unity Profiler-fönster

Dålig prestanda

Dålig prestanda kan hända när som helst: Låt oss säga att du arbetar med fiendens instans och när du placerar den i scenen fungerar den bra utan några problem, men när du skapar fler fiender kan du märka fps (frames-per-second) ) börjar sjunka.

Kontrollera exemplet nedan:

I scenen har jag en kub med ett skript kopplat till den, som flyttar kuben från sida till sida och visar objektets namn:

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

När vi tittar på statistiken kan vi se att spelet körs med bra 800+ fps, så det har knappt någon inverkan på prestandan.

Men låt oss se vad som händer när vi duplicerar kuben 100 gånger:

Fps sjönk med mer än 700 poäng!

OBS: Alla tester gjordes med Vsync inaktiverat

Generellt sett är det en bra idé att börja optimera när spelet börjar uppvisa stamning, frysning eller fps sjunker under 120.

Hur använder man Profiler?

För att börja använda Profiler behöver du:

  • Starta ditt spel genom att trycka på Play
  • Öppna Profiler genom att gå till Fönster -> Analys -> Profiler (eller tryck på Ctrl + 7)

  • Ett nytt fönster kommer att dyka upp som ser ut ungefär så här:

Unity 3D-profilfönster

  • Det kan se skrämmande ut till en början (särskilt med alla dessa diagram etc.), men det är inte den del vi kommer att titta på.
  • Klicka på fliken Tidslinje och ändra den till Hierarki:

  • Du kommer att märka tre sektioner (EditorLoop, PlayerLoop och Profiler.CollectEditorStats):

  • Expandera PlayerLoop för att se alla delar där beräkningskraften förbrukas (OBS: Om PlayerLoop-värdena inte uppdateras, klicka på knappen "Clear" högst upp i Profiler-fönstret).

För bästa resultat, rikta din spelkaraktär till den situation (eller plats) där spelet släpar efter mest och vänta i några sekunder.

  • Efter att ha väntat lite, stoppa spelet och observera PlayerLoop-listan

Du måste titta på värdet GC Alloc, som står för Garbage Collection Allocation. Det här är en typ av minne som har allokerats av komponenten men som inte längre behövs och som väntar på att frigöras av Garbage Collection. Helst ska koden inte generera något skräp (eller vara så nära 0 som möjligt).

Tid ms är också ett viktigt värde, det visar hur lång tid det tog att köra koden i millisekunder, så helst bör du sikta på att minska detta värde också (genom att cachelagra värden, undvika att anropa prestandakrävande funktioner varje uppdatering, etc.).

För att hitta de besvärliga delarna snabbare, klicka på kolumnen GC Alloc för att sortera värdena från högre till lägre)

  • Klicka var som helst i diagrammet för CPU-användning för att hoppa till den ramen. Specifikt måste vi titta på toppar, där fps var lägst:

Unity CPU-användningsdiagram

Här är vad Profiler avslöjade:

GUI.Repaint allokerar 45,4KB, vilket är ganska mycket, och utökar det avslöjade mer information:

  • Det visar att de flesta av allokeringarna kommer från GUIUtility.BeginGUI()- och OnGUI()-metoden i SC_ShowName-skriptet, med vetskapen om att vi kan börja optimera.

GUIUtility.BeginGUI() representerar en tom OnGUI()-metod (Ja, även den tomma OnGUI()-metoden allokerar ganska mycket minne).

Använd Google (eller annan sökmotor) för att hitta de namn du inte känner igen.

Här är OnGUI()-delen som behöver optimeras:

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

Optimering

Låt oss börja optimera.

Varje SC_ShowName-skript anropar sin egen OnGUI()-metod, vilket inte är bra med tanke på att vi har 100 instanser. Så vad kan man göra åt det? Svaret är: Att ha ett enda skript med metoden OnGUI() som anropar GUI-metoden för varje kub.

  • Först ersatte jag standard OnGUI() i SC_ShowName-skriptet med public void GUIMethod() som kommer att anropas från ett annat skript:
    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);
    }
  • Sedan skapade jag ett nytt skript och kallade det SC_GUIMetod:

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_GUIMetod kommer att kopplas till ett slumpmässigt objekt i scenen och anropa alla GUI-metoder.

  • Vi gick från att ha 100 individuella OnGUI()-metoder till att bara ha en, låt oss trycka på play och se resultatet:

  • GUIUtility.BeginGUI() allokerar nu bara 368B istället för 36,7KB, en stor minskning!

Men metoden OnGUI() allokerar fortfarande minne, men eftersom vi vet att den bara anropar GUImethod() från scriptet SC_ShowName, går vi direkt till att felsöka den metoden.

Men Profilern visar bara global information, hur ser vi exakt vad som händer i metoden?

För att felsöka inuti metoden har Unity ett praktiskt API som heter Profiler.BeginSample

Profiler.BeginSample låter dig fånga en specifik del av skriptet, som visar hur lång tid det tog att slutföra och hur mycket minne som tilldelades.

  • Innan vi använder klassen Profiler i kod måste vi importera UnityEngine.Profiling-namnområdet i början av skriptet:
using UnityEngine.Profiling;
  • Profiler-exemplet fångas genom att lägga till Profiler.BeginSample("SOME_NAME"); i början av insamlingen och lägga till Profiler.EndSample(); i slutet av fångsten, så här:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Eftersom jag inte vet vilken del av GUIMethod() som orsakar minnesallokering, omslöt jag varje rad i Profiler.BeginSample och Profiler.EndSample (Men om din metod har många rader, behöver du definitivt inte bifoga varje rad, bara dela upp den i jämna bitar och arbeta sedan därifrån).

Här är en sista metod med Profiler Samples implementerade:

    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();
    }
  • Nu trycker jag på Play och ser vad det visar i Profiler:
  • För enkelhetens skull sökte jag efter "sc_show_" i Profiler, eftersom alla exempel börjar med det namnet.

  • Intressant... Mycket minne tilldelas i sc_show_names del 3, vilket motsvarar denna del av koden:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

Efter lite googling upptäckte jag att att få Objects namn allokerar ganska mycket minne. Lösningen är att tilldela ett objekts namn till en strängvariabel i void Start(), på så sätt kommer den bara att anropas en gång.

Här är den optimerade koden:

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();
    }
}
  • Låt oss se vad Profiler visar:

Alla samplingar allokerar 0B, så inget mer minne allokeras.