Unity Optimalkan Game Anda Menggunakan Profiler
Performa adalah aspek kunci dari game apa pun dan tidak mengherankan, betapapun bagusnya game tersebut, jika dijalankan dengan buruk di mesin pengguna, game tersebut tidak akan terasa menyenangkan.
Karena tidak semua orang memiliki PC atau perangkat kelas atas (jika Anda menargetkan perangkat seluler), penting untuk mempertimbangkan kinerja selama keseluruhan pengembangan.
Ada beberapa alasan mengapa game bisa berjalan lambat:
- Rendering (Terlalu banyak jerat poli tinggi, shader kompleks, atau efek gambar)
- Audio (Sebagian besar disebabkan oleh pengaturan impor audio yang salah)
- Kode Tidak Dioptimalkan (Skrip yang berisi fungsi yang menuntut kinerja di tempat yang salah)
Dalam tutorial ini, saya akan menunjukkan cara mengoptimalkan kode Anda dengan bantuan Unity Profiler.
Profiler
Secara historis, men-debug kinerja di Unity adalah tugas yang membosankan, tetapi sejak itu, fitur baru telah ditambahkan, yang disebut Profiler.
Profiler adalah alat di Unity yang memungkinkan Anda dengan cepat menentukan hambatan dalam game Anda dengan memantau konsumsi memori, yang sangat menyederhanakan proses pengoptimalan.
Performa Buruk
Performa buruk dapat terjadi kapan saja: Katakanlah Anda sedang mengerjakan instance musuh dan ketika Anda menempatkannya di tempat kejadian, itu berfungsi dengan baik tanpa masalah apa pun, namun saat Anda menelurkan lebih banyak musuh, Anda mungkin melihat fps (frame per detik) ) mulai turun.
Periksa contoh di bawah ini:
Dalam Scene, saya memiliki sebuah Cube dengan skrip yang melekat padanya, yang menggerakkan Cube dari sisi ke sisi dan menampilkan nama objek:
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);
}
}
Melihat statistiknya, kita dapat melihat bahwa game ini berjalan pada 800+ fps, sehingga hampir tidak berdampak pada performa.
Tapi mari kita lihat apa yang akan terjadi jika kita menduplikasi Kubus tersebut sebanyak 100 kali:
Fps turun lebih dari 700 poin!
CATATAN: Semua pengujian dilakukan dengan Vsync dinonaktifkan
Secara umum, merupakan ide bagus untuk mulai mengoptimalkan saat game mulai menunjukkan kegagapan, macet, atau fps turun di bawah 120.
Bagaimana Cara Menggunakan Profiler?
Untuk mulai menggunakan Profiler, Anda memerlukan:
- Mulai permainan Anda dengan menekan Mainkan
- Buka Profiler dengan masuk ke Window -> Analysis -> Profiler (atau tekan Ctrl + 7)
- Akan muncul Jendela Baru yang kira-kira seperti ini:
- Ini mungkin terlihat mengintimidasi pada awalnya (terutama dengan semua grafik, dll.), tapi itu bukan bagian yang akan kita lihat.
- Klik pada tab Timeline dan ubah menjadi Hierarchy:
- Anda akan melihat 3 bagian (EditorLoop, PlayerLoop, dan Profiler.CollectEditorStats):
- Perluas PlayerLoop untuk melihat semua bagian di mana daya komputasi digunakan (CATATAN: Jika nilai PlayerLoop tidak diperbarui, klik tombol "Clear" di bagian atas jendela Profiler).
Untuk hasil terbaik, arahkan karakter game Anda ke situasi (atau tempat) di mana game paling lambat dan tunggu beberapa detik.
- Setelah menunggu sebentar, Hentikan permainan dan amati daftar PlayerLoop
Anda perlu melihat nilai GC Alloc, yang merupakan singkatan dari Garbage Collection Allocation. Ini adalah jenis memori yang telah dialokasikan oleh komponen tetapi tidak lagi diperlukan dan menunggu untuk dibebaskan oleh Pengumpulan Sampah. Idealnya, kode tersebut tidak boleh menghasilkan sampah apa pun (atau sedekat mungkin dengan 0).
Waktu ms juga merupakan nilai yang penting, ini menunjukkan berapa lama waktu yang dibutuhkan kode untuk dijalankan dalam milidetik, jadi idealnya, Anda juga harus berusaha mengurangi nilai ini (dengan menyimpan nilai dalam cache, menghindari memanggil fungsi yang menuntut kinerja setiap Pembaruan, dll..).
Untuk menemukan bagian yang bermasalah dengan lebih cepat, klik kolom GC Alloc untuk mengurutkan nilai dari yang lebih tinggi ke yang lebih rendah)
- Di bagan Penggunaan CPU, klik di mana saja untuk melompat ke bingkai itu. Secara khusus, kita perlu melihat puncak, dengan fps terendah:
Inilah yang diungkapkan Profiler:
GUI.Repaint mengalokasikan 45.4KB, yang cukup banyak, memperluasnya mengungkapkan lebih banyak info:
- Ini menunjukkan bahwa sebagian besar alokasi berasal dari metode GUIUtility.BeginGUI() dan OnGUI() dalam skrip SC_ShowName, mengetahui bahwa kita dapat mulai mengoptimalkan.
GUIUtility.BeginGUI() mewakili metode OnGUI() yang kosong (Ya, bahkan metode OnGUI() yang kosong mengalokasikan cukup banyak memori).
Gunakan Google (atau mesin pencari lainnya) untuk menemukan nama yang tidak Anda kenali.
Berikut bagian OnGUI() yang perlu dioptimasi:
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);
}
Optimasi
Mari mulai mengoptimalkan.
Setiap skrip SC_ShowName memanggil metode OnGUI() miliknya sendiri, yang tidak baik mengingat kita memiliki 100 instance. Jadi apa yang bisa dilakukan mengenai hal itu? Jawabannya adalah: Untuk memiliki satu skrip dengan metode OnGUI() yang memanggil metode GUI untuk setiap Cube.
- Pertama, saya mengganti OnGUI() default di skrip SC_ShowName dengan public void GUIMethod() yang akan dipanggil dari skrip lain:
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);
}
- Lalu saya membuat skrip baru dan menamainya SC_GUIMethod:
SC_GUIMetode.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 akan dilampirkan ke objek acak di adegan dan memanggil semua metode GUI.
- Kita beralih dari 100 metode OnGUI() individual menjadi hanya satu, mari tekan putar dan lihat hasilnya:
- GUIUtility.BeginGUI() sekarang hanya mengalokasikan 368B, bukan 36,7KB, pengurangan yang besar!
Namun, metode OnGUI() masih mengalokasikan memori, tetapi karena kita tahu metode tersebut hanya memanggil GUIMethod() dari skrip SC_ShowName, kita akan langsung men-debug metode tersebut.
Namun Profiler hanya menampilkan informasi global, bagaimana kita melihat apa yang sebenarnya terjadi di dalam metode tersebut?
Untuk melakukan debug di dalam metode ini, Unity memiliki API praktis yang disebut Profiler.BeginSample
Profiler.BeginSample memungkinkan Anda menangkap bagian tertentu dari skrip, menunjukkan berapa lama waktu yang dibutuhkan untuk menyelesaikannya dan berapa banyak memori yang dialokasikan.
- Sebelum menggunakan kelas Profiler dalam kode, kita perlu mengimpor namespace UnityEngine.Profiling di awal skrip:
using UnityEngine.Profiling;
- Sampel Profiler diambil dengan menambahkan Profiler.BeginSample("SOME_NAME"); di awal pengambilan dan menambahkan Profiler.EndSample(); di akhir pengambilan, seperti ini:
Profiler.BeginSample("SOME_CODE");
//...your code goes here
Profiler.EndSample();
Karena saya tidak tahu bagian mana dari GUIMethod() yang menyebabkan alokasi memori, saya menyertakan setiap baris di Profiler.BeginSample dan Profiler.EndSample (Tetapi jika metode Anda memiliki banyak baris, Anda pasti tidak perlu menyertakan setiap baris, bagi saja menjadi beberapa bagian lalu kerjakan dari sana).
Berikut adalah metode terakhir dengan penerapan Sampel Profiler:
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();
}
- Sekarang saya tekan Mainkan dan lihat apa yang ditampilkan di Profiler:
- Untuk kenyamanan, saya mencari "sc_show_" di Profiler, karena semua sampel dimulai dengan nama itu.
- Menarik... Banyak memori yang dialokasikan di sc_show_names bagian 3, yang sesuai dengan bagian kode ini:
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Setelah beberapa Googling, saya menemukan bahwa mendapatkan nama Objek mengalokasikan cukup banyak memori. Solusinya adalah dengan menetapkan nama Objek ke variabel string di void Start(), sehingga hanya akan dipanggil satu kali.
Berikut adalah kode yang dioptimalkan:
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();
}
}
- Mari kita lihat apa yang ditampilkan Profiler:
Semua sampel mengalokasikan 0B, sehingga tidak ada lagi memori yang dialokasikan.