Unity Optimaliseer je spel met Profiler

Prestaties zijn een belangrijk aspect van elk spel en het is geen verrassing dat, hoe goed het spel ook is, het niet zo plezierig zal aanvoelen als het slecht draait op de computer van de gebruiker.

Omdat niet iedereen een geavanceerde pc of apparaat heeft (als je je op mobiel richt), is het belangrijk om tijdens het hele ontwikkelingstraject rekening te houden met de prestaties.

Er zijn meerdere redenen waarom het spel langzaam kan werken:

  • Rendering (te veel high-poly meshes, complexe shaders of afbeeldingseffecten)
  • Audio (meestal veroorzaakt door onjuiste instellingen voor het importeren van audio)
  • Niet-geoptimaliseerde code (scripts die prestatie-eisende functies op de verkeerde plaatsen bevatten)

In deze zelfstudie laat ik zien hoe u uw code kunt optimaliseren met behulp van Unity Profiler.

Profiler

Historisch gezien was het opsporen van fouten in Unity een vervelende taak, maar sindsdien is er een nieuwe functie toegevoegd, genaamd Profiler.

Profiler is een tool in Unity waarmee je snel de knelpunten in je spel kunt opsporen door het geheugenverbruik te monitoren, wat het optimalisatieproces aanzienlijk vereenvoudigt.

Unity Profiler-venster

Slechte prestatie

Slechte prestaties kunnen op elk moment optreden: stel dat u aan de vijandelijke instantie werkt en wanneer u deze in de scène plaatst, werkt deze prima zonder problemen, maar naarmate u meer vijanden spawnt, merkt u mogelijk fps (frames per seconde ) beginnen te dalen.

Bekijk het onderstaande voorbeeld:

In de scène heb ik een kubus waaraan een script is gekoppeld, dat de kubus heen en weer beweegt en de objectnaam weergeeft:

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

Als we naar de statistieken kijken, kunnen we zien dat de game ruim 800+ fps draait, dus het heeft nauwelijks invloed op de prestaties.

Maar laten we eens kijken wat er zal gebeuren als we de kubus 100 keer dupliceren:

Fps gedaald met meer dan 700 punten!

OPMERKING: Alle tests zijn uitgevoerd met Vsync uitgeschakeld

Over het algemeen is het een goed idee om te beginnen met optimaliseren wanneer de game haperingen begint te vertonen, vastloopt of de fps onder de 120 zakt.

Hoe gebruik ik Profiler?

Om Profiler te gaan gebruiken, heeft u het volgende nodig:

  • Start je spel door op Play te drukken
  • Open Profiler door naar Venster -> Analyse -> Profiler te gaan (of druk op Ctrl + 7)

  • Er verschijnt een nieuw venster dat er ongeveer zo uitziet:

Unity 3D Profiler-venster

  • Het ziet er in eerste instantie misschien intimiderend uit (vooral met al die grafieken enz.), maar het is niet het deel waar we naar zullen kijken.
  • Klik op het tabblad Tijdlijn en wijzig dit in Hiërarchie:

  • Je zult 3 secties opmerken (EditorLoop, PlayerLoop en Profiler.CollectEditorStats):

  • Vouw de PlayerLoop uit om alle onderdelen te zien waar de rekenkracht wordt besteed (LET OP: als de PlayerLoop-waarden niet worden bijgewerkt, klikt u op de knop "Clear" bovenaan het Profiler-venster).

Voor het beste resultaat richt je je spelpersonage naar de situatie (of plaats) waar het spel het meest achterblijft en wacht je een paar seconden.

  • Nadat je even hebt gewacht, stop je het spel en bekijk je de PlayerLoop-lijst

Je moet kijken naar de waarde GC Alloc, wat staat voor Garbage Collection Allocation. Dit is een type geheugen dat is toegewezen door de component, maar niet langer nodig is en wacht om te worden vrijgegeven door de Garbage Collection. Idealiter zou de code geen afval moeten genereren (of zo dicht mogelijk bij 0 moeten zijn).

Tijd ms is ook een belangrijke waarde. Deze laat zien hoe lang het duurde voordat de code werd uitgevoerd in milliseconden, dus idealiter zou je moeten proberen deze waarde ook te verlagen (door waarden in het cachegeheugen op te slaan en te vermijden dat je bij elke update prestatie-eisende functies aanroept, enz.).

Om de lastige onderdelen sneller te lokaliseren, klikt u op de GC Alloc-kolom om de waarden van hoger naar lager te sorteren)

  • Klik in het CPU-gebruiksdiagram ergens om naar dat frame te gaan. Concreet moeten we kijken naar pieken, waarbij de fps het laagst was:

Unity CPU-gebruiksgrafiek

Dit is wat de Profiler onthulde:

GUI.Repaint wijst 45,4 KB toe, wat behoorlijk veel is. Als je het uitbreidt, komt er meer informatie naar voren:

  • Het laat zien dat de meeste toewijzingen afkomstig zijn van de methoden GUIUtility.BeginGUI() en OnGUI() in het SC_ShowName-script, wetende dat we kunnen beginnen met optimaliseren.

GUIUtility.BeginGUI() vertegenwoordigt een lege OnGUI()-methode (ja, zelfs de lege OnGUI()-methode wijst behoorlijk veel geheugen toe).

Gebruik Google (of een andere zoekmachine) om de namen te vinden die u niet herkent.

Hier is het OnGUI()-gedeelte dat moet worden geoptimaliseerd:

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

Optimalisatie

Laten we beginnen met optimaliseren.

Elk SC_ShowName-script roept zijn eigen OnGUI()-methode aan, wat niet goed is gezien het feit dat we 100 exemplaren hebben. Dus wat kan er aan gedaan worden? Het antwoord is: om één enkel script te hebben met de OnGUI()-methode die de GUI-methode voor elke kubus aanroept.

  • Eerst heb ik de standaard OnGUI() in het SC_ShowName-script vervangen door public void GUIMethod() die vanuit een ander script wordt aangeroepen:
    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);
    }
  • Vervolgens heb ik een nieuw script gemaakt en het SC_GUIMethode genoemd:

SC_GUIMethode.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 wordt aan een willekeurig object in de scène gekoppeld en roept alle GUI-methoden aan.

  • We zijn van 100 individuele OnGUI()-methoden naar slechts één gegaan. Laten we op play drukken en het resultaat bekijken:

  • GUIUtility.BeginGUI() wijst nu slechts 368B toe in plaats van 36,7KB, een grote reductie!

De OnGUI()-methode wijst echter nog steeds geheugen toe, maar omdat we weten dat deze alleen GUIMethod() aanroept vanuit het SC_ShowName-script, gaan we meteen verder met het debuggen van die methode.

Maar de Profiler toont alleen globale informatie, hoe zien we wat er precies gebeurt binnen de methode?

Om fouten binnen de methode op te sporen, heeft Unity een handige API genaamd Profiler.BeginSample

Met Profiler.BeginSample kunt u een specifiek gedeelte van het script vastleggen, waarbij u kunt zien hoe lang het duurde om het script te voltooien en hoeveel geheugen er was toegewezen.

  • Voordat we de klasse Profiler in code gebruiken, moeten we de naamruimte UnityEngine.Profiling aan het begin van het script importeren:
using UnityEngine.Profiling;
  • Het Profiler-voorbeeld wordt vastgelegd door Profiler.BeginSample("SOME_NAME"); toe te voegen aan het begin van de opname en Profiler.EndSample(); aan het einde van de opname, zoals dit:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Omdat ik niet weet welk deel van de GUIMethod() geheugentoewijzingen veroorzaakt, heb ik elke regel ingesloten in de Profiler.BeginSample en Profiler.EndSample (maar als uw methode veel regels heeft, hoeft u deze zeker niet in te sluiten elke regel, splits deze gewoon in even stukken en werk vanaf daar).

Hier is een laatste methode waarbij Profiler-voorbeelden zijn geïmplementeerd:

    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 druk ik op Play en kijk wat er in de Profiler wordt weergegeven:
  • Voor het gemak heb ik in de Profiler naar "sc_show_" gezocht, omdat alle voorbeelden met die naam beginnen.

  • Interessant... Er wordt veel geheugen toegewezen in sc_show_names deel 3, wat overeenkomt met dit deel van de code:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

Na wat Googlen ontdekte ik dat het verkrijgen van de naam van Object behoorlijk veel geheugen toewijst. De oplossing is om de naam van een object toe te wijzen aan een stringvariabele in void Start(), zodat deze slechts één keer wordt aangeroepen.

Hier is de geoptimaliseerde code:

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();
    }
}
  • Laten we eens kijken wat de Profiler laat zien:

Alle samples wijzen 0B toe, dus er wordt geen geheugen meer toegewezen.

Voorgestelde artikelen
Optimalisatietips voor eenheid
Update in Unity gebruiken
De prestaties van een mobiel spel in eenheid verbeteren
De Billboard-generator voor eenheid
Unity Audio Clip importinstellingen voor de beste prestaties
Hoe je een betere programmeur kunt worden in Unity
Hoe je een FNAF-geïnspireerd spel in Unity maakt