Unity optimer dit spil ved hjælp af Profiler
Ydeevne er et nøgleaspekt i ethvert spil, og det er ingen overraskelse, uanset hvor godt spillet er, hvis det kører dårligt på brugerens maskine, vil det ikke føles så behageligt.
Da ikke alle har en avanceret pc eller enhed (hvis du målretter mod mobil), er det vigtigt at huske på ydeevnen under hele udviklingsforløbet.
Der er flere grunde til, at spillet kan køre langsomt:
- Gengivelse (for mange højpolymasker, komplekse shaders eller billedeffekter)
- Lyd (for det meste forårsaget af forkerte lydimportindstillinger)
- Uoptimeret kode (scripts, der indeholder præstationskrævende funktioner de forkerte steder)
I denne tutorial vil jeg vise, hvordan du optimerer din kode ved hjælp af Unity Profiler.
Profiler
Historisk set var fejlfinding i Unity en kedelig opgave, men siden da er en ny funktion blevet tilføjet, kaldet Profiler.
Profiler er et værktøj i Unity, der lader dig hurtigt lokalisere flaskehalsene i dit spil ved at overvåge hukommelsesforbruget, hvilket i høj grad forenkler optimeringsprocessen.
Dårlig præstation
Dårlig ydeevne kan ske når som helst: Lad os sige, at du arbejder på fjendens instans, og når du placerer den i scenen, fungerer den fint uden problemer, men efterhånden som du afføder flere fjender, vil du muligvis bemærke fps (frames-per-second) ) begynder at falde.
Tjek eksemplet nedenfor:
I scenen har jeg en terning med et script knyttet til det, som flytter terningen fra side til side og viser objektnavnet:
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);
}
}
Ser vi på statistikken, kan vi se, at spillet kører med godt 800+ fps, så det har knapt nok nogen indflydelse på ydeevnen.
Men lad os se, hvad der vil ske, når vi dublerer terningen 100 gange:
Fps faldt med mere end 700 point!
BEMÆRK: Alle test blev udført med Vsync deaktiveret
Generelt er det en god idé at begynde at optimere, når spillet begynder at udvise hakken, fryser eller fps falder til under 120.
Hvordan bruger jeg Profiler?
For at begynde at bruge Profiler skal du bruge:
- Start dit spil ved at trykke på Play
- Åbn Profiler ved at gå til Vindue -> Analyse -> Profiler (eller tryk på Ctrl + 7)
- Der vises et nyt vindue, der ser sådan ud:
- Det ser måske skræmmende ud i starten (især med alle de diagrammer osv.), men det er ikke den del, vi vil se på.
- Klik på fanen Tidslinje og skift den til Hierarki:
- Du vil bemærke 3 sektioner (EditorLoop, PlayerLoop og Profiler.CollectEditorStats):
- Udvid PlayerLoop for at se alle de dele, hvor beregningskraften bliver brugt (BEMÆRK: Hvis PlayerLoop-værdierne ikke opdateres, skal du klikke på knappen "Clear" øverst i Profiler-vinduet).
For de bedste resultater skal du rette din spilkarakter til den situation (eller stedet), hvor spillet halter mest, og vent et par sekunder.
- Efter at have ventet lidt, stop spillet og observer PlayerLoop-listen
Du skal se på GC Alloc værdien, som står for Garbage Collection Allocation. Dette er en type hukommelse, der er blevet tildelt af komponenten, men som ikke længere er nødvendig og venter på at blive frigivet af Garbage Collection. Ideelt set bør koden ikke generere noget skrald (eller være så tæt på 0 som muligt).
Tid ms er også en vigtig værdi, den viser, hvor lang tid det tog koden at køre i millisekunder, så ideelt set bør du også forsøge at reducere denne værdi (ved at cache værdier, undgå at kalde præstationskrævende funktioner hver opdatering osv..).
For at finde de besværlige dele hurtigere, klik på GC Alloc-kolonnen for at sortere værdierne fra højere til lavere)
- Klik hvor som helst i CPU-brugsdiagrammet for at springe til den ramme. Specifikt skal vi se på peaks, hvor fps var den laveste:
Her er, hvad profileren afslørede:
GUI.Repaint allokerer 45.4KB, hvilket er ret meget, og udvider det afslørede mere info:
- Det viser, at de fleste tildelinger kommer fra GUIUtility.BeginGUI()- og OnGUI()-metoden i SC_ShowName-scriptet, velvidende at vi kan begynde at optimere.
GUIUtility.BeginGUI() repræsenterer en tom OnGUI()-metode (Ja, selv den tomme OnGUI()-metode allokerer ret meget hukommelse).
Brug Google (eller en anden søgemaskine) til at finde de navne, du ikke genkender.
Her er OnGUI() delen, der skal optimeres:
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
Lad os begynde at optimere.
Hvert SC_ShowName-script kalder sin egen OnGUI()-metode, hvilket ikke er godt i betragtning af, at vi har 100 forekomster. Så hvad kan man gøre ved det? Svaret er: At have et enkelt script med OnGUI()-metoden, der kalder GUI-metoden for hver terning.
- Først erstattede jeg standard OnGUI() i SC_ShowName scriptet med public void GUIMethod(), som vil blive kaldt fra et andet 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);
}
- Så oprettede jeg et nyt script og kaldte 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 vil blive knyttet til et tilfældigt objekt i scenen og kalder alle GUI-metoderne.
- Vi gik fra at have 100 individuelle OnGUI()-metoder til kun at have én, lad os trykke på play og se resultatet:
- GUIUtility.BeginGUI() allokerer nu kun 368B i stedet for 36,7KB, en stor reduktion!
OnGUI()-metoden allokerer dog stadig hukommelse, men da vi ved, at den kun kalder GUImethod() fra SC_ShowName-scriptet, går vi direkte til at fejlsøge denne metode.
Men Profileren viser kun global information, hvordan kan vi se, hvad der præcist sker inde i metoden?
For at debugge inde i metoden har Unity en praktisk API kaldet Profiler.BeginSample
Profiler.BeginSample giver dig mulighed for at fange en specifik sektion af scriptet, der viser hvor lang tid det tog at fuldføre, og hvor meget hukommelse der blev tildelt.
- Før vi bruger Profiler-klassen i kode, skal vi importere UnityEngine.Profiling-navneområdet i begyndelsen af scriptet:
using UnityEngine.Profiling;
- Profiler-eksemplet fanges ved at tilføje Profiler.BeginSample("SOME_NAME"); i starten af optagelsen og tilføje Profiler.EndSample(); i slutningen af optagelsen, som f.eks. det her:
Profiler.BeginSample("SOME_CODE");
//...your code goes here
Profiler.EndSample();
Da jeg ikke ved, hvilken del af GUIMethod() der forårsager hukommelsestildelinger, indesluttede jeg hver linje i Profiler.BeginSample og Profiler.EndSample (Men hvis din metode har mange linjer, behøver du bestemt ikke at omslutte hver linje, skal du bare dele den i lige bidder og derefter arbejde derfra).
Her er en sidste metode med Profiler Samples implementeret:
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 trykker jeg på Afspil og ser, hvad det viser i Profiler:
- For nemheds skyld søgte jeg efter "sc_show_" i Profiler, da alle prøver starter med det navn.
- Interessant... Meget hukommelse bliver allokeret i sc_show_names del 3, som svarer til denne del af koden:
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Efter noget google opdagede jeg, at det at få Objects navn tildeler ret meget hukommelse. Løsningen er at tildele et objekts navn til en strengvariabel i void Start(), på den måde bliver den kun kaldt én gang.
Her er den optimerede kode:
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();
}
}
- Lad os se, hvad Profileren viser:
Alle samples allokerer 0B, så der allokeres ikke mere hukommelse.