Unity ottimizza il tuo gioco utilizzando Profiler
Le prestazioni sono un aspetto chiave di qualsiasi gioco e nessuna sorpresa, non importa quanto sia buono il gioco, se funziona male sulla macchina dell'utente, non sarà altrettanto piacevole.
Poiché non tutti dispongono di un PC o di un dispositivo di fascia alta (se si punta ai dispositivi mobili), è importante tenere a mente le prestazioni durante l'intero corso dello sviluppo.
Ci sono diversi motivi per cui il gioco potrebbe funzionare lentamente:
- Rendering (troppe mesh ad alto numero di poligoni, shader complessi o effetti immagine)
- Audio (principalmente causato da impostazioni di importazione audio errate)
- Codice non ottimizzato (script che contengono funzioni che richiedono prestazioni elevate nei posti sbagliati)
In questo tutorial, mostrerò come ottimizzare il tuo codice con l'aiuto di Unity Profiler.
Profiler
Storicamente, il debug delle prestazioni in Unity era un compito noioso, ma da allora è stata aggiunta una nuova funzionalità, chiamata Profiler.
Profiler è uno strumento in Unity che ti consente di individuare rapidamente i colli di bottiglia nel tuo gioco monitorando il consumo di memoria, il che semplifica notevolmente il processo di ottimizzazione.
Prestazione negativa
Le cattive prestazioni possono verificarsi in qualsiasi momento: diciamo che stai lavorando sull'istanza nemica e quando la metti nella scena, funziona bene senza problemi, ma man mano che generi più nemici potresti notare fps (fotogrammi al secondo ) iniziano a scendere.
Controlla l'esempio qui sotto:
Nella scena, ho un cubo con uno script allegato, che sposta il cubo da un lato all'altro e visualizza il nome dell'oggetto:
SC_MostraNome.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);
}
}
Guardando le statistiche, possiamo vedere che il gioco gira a ben 800+ fps, quindi non ha quasi alcun impatto sulle prestazioni.
Ma vediamo cosa accadrà quando duplichiamo il Cubo 100 volte:
Fps scesi di oltre 700 punti!
NOTA: tutti i test sono stati eseguiti con Vsync disabilitato
In generale, è una buona idea iniziare l'ottimizzazione quando il gioco inizia a mostrare balbettii, blocchi o gli fps scendono sotto i 120.
Come usare il profilatore?
Per iniziare a utilizzare Profiler avrai bisogno di:
- Inizia il gioco premendo Riproduci
- Apri Profiler andando su Finestra -> Analisi -> Profiler (o premi Ctrl + 7)
- Apparirà una nuova finestra che assomiglia a questa:
- All'inizio potrebbe sembrare intimidatorio (specialmente con tutte quelle classifiche ecc.), ma non è la parte che vedremo.
- Fare clic sulla scheda Timeline e modificarla in Gerarchia:
- Noterai 3 sezioni (EditorLoop, PlayerLoop e Profiler.CollectEditorStats):
- Espandi PlayerLoop per vedere tutte le parti in cui viene spesa la potenza di calcolo (NOTA: se i valori di PlayerLoop non si aggiornano, fai clic sul pulsante "Clear" nella parte superiore della finestra di Profiler).
Per ottenere i migliori risultati, indirizza il tuo personaggio del gioco verso la situazione (o il luogo) in cui il gioco rallenta di più e attendi un paio di secondi.
- Dopo aver atteso un po', ferma il gioco e osserva l'elenco PlayerLoop
Devi guardare il valore GC Alloc, che sta per Garbage Collection Allocation. Questo è un tipo di memoria che è stata allocata dal componente ma non è più necessaria ed è in attesa di essere liberata dalla Garbage Collection. Idealmente, il codice non dovrebbe generare alcuna spazzatura (o essere il più vicino possibile a 0).
Anche il tempo ms è un valore importante, mostra quanto tempo ha impiegato il codice per essere eseguito in millisecondi, quindi idealmente dovresti mirare a ridurre anche questo valore (memorizzando nella cache i valori, evitando di chiamare funzioni che richiedono prestazioni ogni aggiornamento, ecc.).
Per individuare più rapidamente le parti problematiche, fare clic sulla colonna GC Alloc per ordinare i valori dal più alto al più basso)
- Nel grafico sull'utilizzo della CPU, fare clic in un punto qualsiasi per passare a quel frame. Nello specifico, dobbiamo esaminare i picchi, in cui gli fps erano i più bassi:
Ecco cosa ha rivelato il Profiler:
GUI.Repaint sta allocando 45.4KB, che è parecchio, espandendolo ha rivelato maggiori informazioni:
- Mostra che la maggior parte delle allocazioni proviene dal metodo GUIUtility.BeginGUI() e OnGUI() nello script SC_ShowName, sapendo che possiamo iniziare l'ottimizzazione.
GUIUtility.BeginGUI() rappresenta un metodo OnGUI() vuoto (Sì, anche il metodo OnGUI() vuoto alloca molta memoria).
Usa Google (o un altro motore di ricerca) per trovare i nomi che non riconosci.
Ecco la parte OnGUI() che deve essere ottimizzata:
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);
}
Ottimizzazione
Iniziamo a ottimizzare.
Ogni script SC_ShowName chiama il proprio metodo OnGUI(), il che non va bene considerando che abbiamo 100 istanze. Quindi cosa si può fare al riguardo? La risposta è: avere un singolo script con il metodo OnGUI() che chiama il metodo GUI per ogni cubo.
- Innanzitutto, ho sostituito OnGUI() predefinito nello script SC_ShowName con public void GUIMethod() che verrà chiamato da un altro 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);
}
- Quindi ho creato un nuovo script e l'ho chiamato 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 verrà collegato a un oggetto casuale nella scena e chiamerà tutti i metodi della GUI.
- Siamo passati dall'avere 100 singoli metodi OnGUI() ad averne solo uno, premiamo play e vediamo il risultato:
- GUIUtility.BeginGUI() ora alloca solo 368B invece di 36,7KB, una grande riduzione!
Tuttavia, il metodo OnGUI() sta ancora allocando memoria, ma poiché sappiamo che sta chiamando solo GUIMethod() dallo script SC_ShowName, andremo direttamente al debug di quel metodo.
Ma il Profiler mostra solo informazioni globali, come possiamo vedere cosa sta succedendo esattamente all'interno del metodo?
Per eseguire il debug all'interno del metodo, Unity ha una comoda API chiamata Profiler.BeginSample
Profiler.BeginSample consente di acquisire una sezione specifica dello script, mostrando quanto tempo è stato necessario per il completamento e quanta memoria è stata allocata.
- Prima di utilizzare la classe Profiler nel codice, dobbiamo importare lo spazio dei nomi UnityEngine.Profiling all'inizio dello script:
using UnityEngine.Profiling;
- L'esempio di Profiler viene acquisito aggiungendo Profiler.BeginSample("SOME_NAME"); all'inizio dell'acquisizione e aggiungendo Profiler.EndSample(); alla fine dell'acquisizione, in questo modo:
Profiler.BeginSample("SOME_CODE");
//...your code goes here
Profiler.EndSample();
Poiché non so quale parte di GUIMethod() stia causando allocazioni di memoria, ho racchiuso ogni riga in Profiler.BeginSample e Profiler.EndSample (ma se il tuo metodo ha molte righe, sicuramente non è necessario racchiudere ogni riga, basta dividerla in parti uguali e poi lavorare da lì).
Ecco un metodo finale con Profiler Samples implementato:
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();
}
- Ora premo Play e vedo cosa mostra nel Profiler:
- Per comodità, ho cercato "sc_show_" nel Profiler, poiché tutti i campioni iniziano con quel nome.
- Interessante... Molta memoria viene allocata in sc_show_names parte 3, che corrisponde a questa parte del codice:
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Dopo alcune ricerche su Google, ho scoperto che ottenere il nome di Object alloca molta memoria. La soluzione è assegnare il nome di un oggetto a una variabile stringa in void Start(), in questo modo verrà chiamato solo una volta.
Ecco il codice ottimizzato:
SC_MostraNome.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();
}
}
- Vediamo cosa mostra il Profiler:
Tutti i campioni stanno allocando 0B, quindi non viene allocata più memoria.