Ada banyak artikel dan tutorial Unity performance yang hebat. Kami tidak mencoba untuk mengganti atau memperbaikinya dengan artikel ini, ini hanya ringkasan singkat dari langkah-langkah yang kami ambil setelah membaca artikel ini, serta langkah-langkah yang memungkinkan kami untuk menyelesaikan masalah kami. Saya sangat menyarankan agar Anda setidaknya mempelajari materi di
https://learn.unity.com/ .
Dalam proses pengembangan game kami, kami menemui masalah yang dari waktu ke waktu menyebabkan terhambatnya proses game. Setelah menghabiskan beberapa waktu di Unity Profiler, kami menemukan dua jenis masalah:
- Shader Non-Dioptimalkan
- Skrip tidak dioptimalkan dalam C #
Sebagian besar masalah disebabkan oleh kelompok kedua, jadi saya memutuskan untuk fokus pada skrip C # dalam artikel ini (mungkin juga karena saya belum menulis satu shader pun dalam hidup saya).
Cari kelemahannya
Tujuan artikel ini bukan untuk menulis tutorial tentang cara menggunakan profiler; Saya hanya ingin berbicara tentang apa yang paling kami minati selama proses pembuatan profil.
Unity Profiler selalu merupakan cara terbaik untuk menemukan penyebab keterlambatan skrip. Saya sangat merekomendasikan untuk
membuat profil game langsung di perangkat , dan tidak di editor. Karena permainan kami dibuat untuk iOS, saya harus menghubungkan perangkat dan menggunakan Pengaturan Bangun yang ditunjukkan pada gambar, setelah itu profiler terhubung secara otomatis.
Bangun Pengaturan untuk Pembuatan ProfilJika Anda mencoba google "Random lag in Unity" atau permintaan serupa lainnya, Anda akan menemukan bahwa kebanyakan orang merekomendasikan
fokus pada pengumpulan sampah , yang persis seperti yang saya lakukan. Sampah dihasilkan setiap kali Anda berhenti menggunakan beberapa objek (instance kelas), setelah itu pengumpul sampah Unity mulai dari waktu ke waktu untuk membersihkan kekacauan dan membebaskan memori, yang membutuhkan jumlah waktu yang gila dan menyebabkan penurunan frame rate.
Bagaimana menemukan skrip sampah di profiler?
Cukup pilih Penggunaan CPU -> Pilih tampilan Hirarki -> Urutkan berdasarkan GC Alloc
Opsi profiler untuk pengumpulan sampahTugas Anda adalah mencapai beberapa angka nol di kolom alokasi GC untuk adegan permainan.
Cara lain yang baik adalah dengan
mengurutkan entri berdasarkan ms Waktu (run time) dan mengoptimalkan skrip sehingga mereka mengambil sesedikit mungkin waktu. Langkah ini memiliki dampak besar bagi kami, karena salah satu komponen kami berisi
besar untuk loop , yang membutuhkan waktu lama untuk menyelesaikan (ya, kami belum menemukan cara untuk menghilangkan loop), jadi mengoptimalkan waktu eksekusi semua skrip benar-benar diperlukan bagi kami, karena kami perlu menghemat runtime pada loop yang mahal ini, sambil mempertahankan frekuensi stabil 60 fps.
Berdasarkan data profil, saya membagi pengoptimalan menjadi dua bagian:
- Membuang sampah
- Mengurangi waktu tunggu
Bagian 1: melawan sampah
Pada bagian ini saya akan memberi tahu Anda apa yang kami lakukan untuk membuang sampah. Ini adalah pengetahuan paling mendasar yang harus dipahami oleh pengembang mana pun; mereka telah menjadi bagian penting dari analisis harian kami dalam setiap permintaan tarikan / penggabungan.
Aturan pertama: tidak ada objek baru dalam metode Perbarui
Idealnya, metode
Pembaruan, FixedUpdate, dan LateUpdate tidak boleh mengandung kata kunci "baru" . Anda harus selalu menggunakan apa yang sudah Anda miliki.
Terkadang
membuat objek baru tersembunyi di beberapa metode internal Unity, jadi tidak begitu jelas. Kami akan membicarakan ini nanti.
Aturan kedua: buat sekali dan gunakan kembali!
Intinya, ini berarti Anda harus mengalokasikan memori untuk semua yang Anda bisa dalam metode Mulai dan Sedarlah. Aturan ini sangat mirip dengan yang pertama. Ini sebenarnya hanyalah cara lain untuk menghilangkan kata kunci "baru" dari metode Pembaruan.
Kode itu:
- menciptakan instance baru
- mencari objek permainan
Anda harus selalu mencoba untuk beralih dari metode Perbarui ke Mulai atau Sedarlah.
Berikut adalah contoh dari perubahan kami:
Alokasi memori untuk daftar dalam metode Mulai, pembersihan (Bersihkan) dan digunakan kembali jika perlu.
Menyimpan tautan dan menggunakannya kembali sebagai berikut:
Hal yang sama berlaku untuk metode FindGameObjectsWithTag atau metode lain yang mengembalikan array baru.
Aturan ketiga: waspadai string dan hindari penggabungannya
Ketika membuat sampah, garisnya mengerikan. Bahkan operasi string yang paling sederhana pun dapat menghasilkan banyak sampah. Mengapa String hanyalah array, dan array ini tidak berubah. Ini berarti bahwa setiap kali Anda menggabungkan dua baris, array baru dibuat, dan yang lama berubah menjadi sampah. Untungnya, StringBuilder dapat digunakan untuk menghindari atau meminimalkan pembuatan sampah tersebut.
Berikut adalah contoh bagaimana Anda dapat memperbaiki situasi:
Semuanya baik-baik saja dengan contoh yang ditunjukkan di atas, tetapi masih ada banyak kemungkinan untuk meningkatkan kode. Seperti yang Anda lihat, hampir seluruh string dapat dianggap statis. Kami membagi string menjadi dua bagian untuk dua objek UI. Teks. Pertama, yang satu hanya berisi teks statis
"Player" + nama + "memiliki skor" , yang dapat ditetapkan dalam metode Mulai, dan yang kedua berisi nilai skor, yang diperbarui di setiap frame.
Selalu buat garis statis benar-benar statis dan hasilkan dalam metode Mulai atau Sedarlah . Setelah peningkatan ini, hampir semuanya beres, tetapi sedikit sampah masih dihasilkan saat memanggil Int.ToString (), Float.ToString (), dll.
Kami memecahkan masalah ini dengan membuat dan mengalokasikan memori untuk semua lini yang memungkinkan. Ini mungkin tampak seperti pemborosan memori yang bodoh, tetapi solusi seperti itu sangat cocok dengan kebutuhan kita dan sepenuhnya menyelesaikan masalah. Jadi, pada akhirnya, kami mendapat array statis, akses yang dapat langsung diakses menggunakan indeks untuk mengambil string yang diinginkan yang menunjukkan angka:
public static readonly string[] NUMBERS_THREE_DECIMAL = { "000", "001", "002", "003", "004", "005", "006",..........
Aturan Keempat: Nilai-Nilai Cache Dikembalikan dengan Metode Akses
Ini bisa sangat sulit, karena bahkan metode pengaksesan sederhana seperti yang ditunjukkan di bawah ini menghasilkan sampah:
Cobalah untuk menghindari menggunakan metode akses dalam metode Pembaruan. Panggil metode akses hanya sekali dalam metode Mulai dan cache nilai kembali.
Secara umum, saya sarankan
TIDAK memanggil metode akses string atau metode akses array dalam metode Pembaruan . Dalam kebanyakan kasus, cukup
untuk mendapatkan tautan sekali dalam metode Mulai .
Berikut adalah dua contoh umum dari kode metode akses lain yang tidak dioptimalkan:
Aturan Kelima: Gunakan Fungsi yang Tidak Mengalokasikan Memori
Untuk beberapa fungsi Unity, alternatif non-memori dapat ditemukan. Dalam kasus kami, semua fungsi ini terkait dengan fisika. Pengakuan tabrakan kami didasarkan pada
Physics2D. CircleCast();
Untuk kasus khusus ini, Anda dapat menemukan fungsi non-memori yang disebut
Physics2D. CircleCastNonAlloc();
Banyak fungsi lain juga memiliki alternatif yang serupa, jadi
selalu periksa dokumentasi untuk fungsi NonAlloc .
Aturan keenam: jangan gunakan LINQ
Tapi jangan lakukan itu. Maksud saya, Anda tidak perlu menggunakannya dalam kode apa pun yang sering berjalan. Saya tahu bahwa ketika menggunakan LINQ, kodenya lebih mudah dibaca, tetapi dalam banyak kasus kinerja dan alokasi memori dari kode semacam itu mengerikan. Tentu saja, kadang-kadang dapat digunakan, tetapi, jujur saja, dalam game kami, kami tidak menggunakan LINQ sama sekali.
Aturan ketujuh: buat sekali dan gunakan kembali, bagian 2
Kali ini kita berbicara tentang objek penyatuan. Saya tidak akan masuk ke rincian pooling, karena ini telah dikatakan berkali-kali, misalnya, pelajari tutorial ini:
https://learn.unity.com/tutorial/object-poolingDalam kasus kami, skrip pengumpulan objek berikut digunakan. Kami memiliki level yang dihasilkan diisi dengan rintangan yang ada selama periode waktu tertentu sampai pemain melewati bagian level ini. Contoh hambatan seperti itu dibuat dari cetakan jika kondisi tertentu dipenuhi. Kode ini dalam metode Pembaruan. Kode ini sama sekali tidak efisien dalam hal memori dan runtime. Kami memecahkan masalah dengan membuat kumpulan 40 rintangan: jika perlu, kami mendapatkan hambatan dari kolam dan mengembalikan objek ke kolam ketika tidak lagi diperlukan.
Aturan kedelapan: lebih penuh perhatian dengan transformasi kemasan (Tinju)!
Tinju menghasilkan sampah! Tapi apa itu tinju? Paling sering, tinju terjadi ketika Anda melewatkan tipe nilai (int, float, bool, dll.) Ke fungsi yang mengharapkan objek tipe objek.
Berikut adalah contoh tinju yang perlu kami perbaiki dalam proyek kami:
Kami menerapkan sistem pesan kami sendiri di proyek. Setiap pesan dapat berisi jumlah data yang tidak terbatas. Data disimpan dalam kamus yang didefinisikan sebagai berikut:
Dictionary<string, object> data;
Kami juga memiliki setter yang menetapkan nilai dalam kamus ini:
public Action SetAttribute(string attribute, object value) { data[attribute] = value; }
Tinju di sini cukup jelas. Anda dapat memanggil fungsi sebagai berikut:
SetAttribute("my_int_value", 12);
Kemudian nilai "12" dikenakan tinju dan ini menghasilkan sampah.
Kami memecahkan masalah dengan membuat wadah data terpisah untuk setiap jenis primitif, dan wadah Obyek sebelumnya hanya digunakan untuk jenis referensi.
Dictionary<string, object> data; Dictionary<string, bool> dataBool; Dictionary<string, int> dataInt; .......
Kami juga memiliki setter terpisah untuk setiap tipe data:
SetBoolAttribute(string attribute, bool value) SetIntAttribute(string attribute, int value)
Dan semua setter ini diimplementasikan sedemikian rupa sehingga mereka memanggil fungsi umum yang sama:
SetAttribute<T>(ref Dictionary<string, T> dict, string attribute, T value)
Masalah tinju telah diatasi!
Baca lebih lanjut tentang ini di artikel
https://docs.microsoft.com/cs-cz/dotnet/csharp/programming-guide/types/boxing-and-unboxing .
Aturan kesembilan: siklus selalu dicurigai
Aturan ini sangat mirip dengan yang pertama dan kedua. Coba hapus semua kode opsional dari loop karena alasan kinerja dan memori.
Dalam kasus umum, kami berupaya untuk menghilangkan loop dalam metode Pembaruan, tetapi jika kami tidak dapat melakukannya tanpa mereka, maka kami setidaknya akan menghindari alokasi memori dalam loop tersebut. Jadi, ikuti
aturan 1–8 dan terapkan pada loop secara umum, bukan hanya metode Perbarui.
Aturan 10: tidak ada sampah di perpustakaan eksternal
Jika ternyata sebagian sampah dihasilkan oleh kode yang diunduh dari Asset store, maka masalah ini memiliki banyak solusi. Tetapi sebelum melakukan reverse engineering dan debugging, kembali saja ke Asset store dan perbarui perpustakaan. Dalam kasus kami, semua aset yang digunakan masih didukung oleh penulis yang terus merilis pembaruan peningkatan kinerja, jadi ini menyelesaikan semua masalah kami.
Ketergantungan harus relevan! Saya lebih suka menyingkirkan perpustakaan daripada tetap tidak didukung.
Bagian 2: memaksimalkan runtime
Beberapa aturan di atas membuat perbedaan yang halus jika kodenya jarang dipanggil. Ada satu loop besar dalam kode kami yang berjalan di setiap frame, jadi bahkan perubahan kecil ini memiliki efek yang sangat besar.
Beberapa perubahan ini, jika digunakan secara tidak tepat atau dalam situasi yang salah, dapat menyebabkan waktu berjalan yang lebih buruk.
Selalu periksa profiler setelah memasukkan setiap optimasi dalam kode untuk memastikan bahwa Anda bergerak ke arah yang benar .
Sejujurnya, beberapa
aturan ini mengarah pada kode yang dapat dibaca lebih buruk , dan kadang-kadang bahkan
melanggar rekomendasi , misalnya, penyematan kode yang disebutkan dalam salah satu aturan di bawah ini.
Banyak dari aturan ini tumpang tindih dengan yang disajikan di bagian pertama artikel. Biasanya, kinerja kode penghasil sampah lebih rendah dibandingkan dengan kode tanpa penghasil sampah.
Aturan pertama: urutan eksekusi yang benar
Pindahkan kode dari metode FixedUpdate, Update, LateUpdate ke metode Mulai dan Sedarlah . Saya tahu ini kedengarannya gila, tetapi percayalah, jika Anda mempelajari kode Anda, Anda akan menemukan ratusan baris kode yang dapat dipindahkan ke metode yang dijalankan hanya sekali.
Dalam kasus kami, kode ini biasanya dikaitkan dengan
- Panggilan ke GetComponent <>
- Komputasi yang benar-benar mengembalikan hasil yang sama di setiap frame
- Banyak contoh dari objek yang sama, biasanya daftar
- Cari GameObjects
- Mendapatkan tautan ke Transform dan menggunakan metode akses lainnya
Berikut adalah daftar kode sampel yang telah kami pindahkan dari metode Perbarui ke metode Mulai:
Aturan kedua: jalankan kode hanya bila perlu
Dalam kasus kami, ini terutama terkait dengan skrip pembaruan UI. Berikut adalah contoh bagaimana kami mengubah implementasi kode yang menampilkan status terkini dari item yang dikumpulkan di level.
Karena pada setiap level hanya ada beberapa item untuk dikumpulkan, tidak masuk akal untuk mengubah teks UI di setiap frame. Oleh karena itu, kami mengubah teks hanya ketika angkanya berubah.
Kode ini jauh lebih baik, terutama jika tindakannya jauh lebih rumit daripada hanya mengubah UI.
Jika Anda mencari solusi yang lebih komprehensif, saya sarankan menerapkan
template Observer menggunakan acara C # (
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/ ).
Bagaimanapun, ini masih belum cukup bagi kami, dan kami ingin menerapkan solusi yang sepenuhnya digeneralisasi, jadi kami membuat perpustakaan yang mengimplementasikan
Flux in Unity. Ini mengarah ke solusi yang sangat sederhana, di mana seluruh keadaan permainan disimpan di objek "Store", dan semua elemen UI dan komponen lainnya diberi tahu ketika keadaan berubah dan bereaksi terhadap perubahan ini tanpa kode dalam metode Pembaruan.
Aturan ketiga: siklus selalu dicurigai
Ini persis aturan yang sama yang saya sebutkan di bagian pertama artikel. Jika kode memiliki semacam loop yang berulang memotong sejumlah besar elemen, maka untuk meningkatkan kinerja loop, gunakan kedua aturan dari kedua bagian artikel.
Aturan Keempat: For Better Than Foreach
Loop Foreach sangat mudah untuk ditulis, tetapi "sangat sulit" untuk dieksekusi. Di dalam loop Foreach, Enumerator digunakan untuk memproses iteratif dataset dan mengembalikan nilainya. Ini lebih rumit daripada mengulangi indeks dalam loop sederhana.
Oleh karena itu, dalam proyek kami, kami sedapat mungkin mengganti Foreach loop dengan For:
Dalam kasus kami dengan besar untuk loop, perubahan ini sangat signifikan.
Sederhana untuk loop mempercepat kode dua kali .
Aturan kelima: array lebih baik daripada daftar
Dalam kode kami, kami menemukan bahwa sebagian besar daftar memiliki panjang konstan, atau kami dapat menghitung jumlah elemen maksimum. Oleh karena itu, kami menerapkannya kembali berdasarkan array, dan dalam beberapa kasus ini menyebabkan percepatan dua kali lipat pengulangan atas data.
Dalam beberapa kasus, daftar atau struktur data kompleks lainnya tidak dapat dihindari. Kebetulan Anda sering harus menambah atau menghapus elemen, dan dalam hal ini lebih baik menggunakan daftar. Tetapi secara umum,
array harus selalu digunakan untuk daftar panjang tetap .
Aturan Keenam: Operasi Apung Lebih Baik Daripada Operasi Vektor
Perbedaan ini hampir tidak terlihat jika Anda tidak melakukan ribuan operasi seperti itu, seperti yang terjadi dalam kasus kami, jadi bagi kami peningkatan produktivitas ternyata signifikan.
Kami membuat perubahan serupa:
Vector3 pos1 = new Vector3(1,2,3); Vector3 pos2 = new Vector3(4,5,6);
Aturan ketujuh: mencari objek dengan benar
Selalu pikirkan apakah Anda benar-benar perlu menggunakan metode GameObject.Find (). Metode ini berat dan membutuhkan waktu yang tidak sedikit. Anda tidak boleh menggunakan metode ini dalam metode Perbarui. Kami menemukan bahwa sebagian besar panggilan Temukan kami dapat
diganti dengan tautan langsung di editor , yang tentu saja jauh lebih baik.
Jika ini tidak mungkin dilakukan, maka setidaknya
pertimbangkan untuk menggunakan tag (Tag) dan mencari objek dengan labelnya menggunakan GameObject.FindWithTag .
Jadi, dalam kasus umum:
Tautan langsung> GameObject.FindWithTag ()> GameObject.Find ()Aturan Kedelapan: Hanya Bekerja dengan Objek yang Relevan
Dalam kasus kami, ini penting untuk mengenali tabrakan menggunakan RayCast-s (CircleCast, dll.). Alih-alih mengenali tumbukan dan memutuskan mana yang penting dalam kode,
kami memindahkan objek game ke lapisan yang sesuai sehingga kami dapat menghitung tumbukan hanya untuk objek yang diperlukan.
Berikut ini sebuah contoh
Aturan kesembilan: gunakan label dengan benar
Tidak ada keraguan bahwa label sangat berguna dan dapat meningkatkan kinerja kode, tetapi ingat bahwa
hanya ada satu cara yang benar untuk membandingkan label objek !
Aturan kesepuluh: Waspadalah terhadap trik dengan kamera!
Sangat mudah untuk menggunakan
Camera.main , tetapi kinerja dari tindakan ini sangat buruk. Alasannya adalah bahwa di balik layar setiap panggilan ke Camera.main, mesin Unity benar-benar mengeksekusi hasil FindGameObjectsWithTag (), jadi kami sudah mengerti bahwa Anda tidak perlu sering memanggilnya, dan yang terbaik untuk menyelesaikan masalah ini dengan
caching tautan dalam metode Mulai atau bangun.
Aturan Kesebelas: Posisi Lokal Lebih Baik Dari Posisi
Jika memungkinkan, gunakan Transform.LocalPosition untuk getter dan setter alih-alih Transform.Position . Di dalam setiap panggilan Transform.Position, lebih banyak operasi dilakukan, misalnya, menghitung posisi global dalam kasus panggilan pengambil atau menghitung posisi lokal dari global dalam kasus panggilan setter. Dalam proyek kami, ternyata Anda dapat menggunakan LocalPositions dalam 99% kasus menggunakan Transform.Position, dan Anda tidak perlu membuat perubahan lain dalam kode.
Aturan Keduabelas: Jangan Gunakan LINQ
Ini sudah dibahas di bagian pertama. Tapi jangan gunakan itu, itu saja.
Aturan ketigabelas: jangan takut (kadang-kadang) melanggar aturan
Kadang-kadang bahkan memanggil fungsi sederhana bisa jadi terlalu mahal. Dalam hal ini, Anda harus selalu mempertimbangkan menyematkan kode (Code Inlining). Apa artinya ini? Bahkan, kami hanya mengambil kode dari fungsi dan menyalinnya langsung ke tempat di mana kami ingin menggunakan fungsi untuk menghindari memanggil metode tambahan.
Dalam kebanyakan kasus, ini tidak akan berpengaruh apa pun, karena penyisipan kode dilakukan secara otomatis pada tahap kompilasi, tetapi ada aturan tertentu yang digunakan oleh kompiler untuk memutuskan apakah akan menyematkan kode (misalnya, metode virtual tidak pernah disematkan; untuk lebih jelasnya, lihat
https: //docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity8.html ). Jadi buka saja profiler, luncurkan game pada perangkat target dan lihat apakah ada sesuatu yang dapat ditingkatkan.
Dalam kasus kami, ada beberapa fungsi yang kami putuskan untuk diintegrasikan untuk meningkatkan kinerja, terutama pada loop yang besar.
Kesimpulan
Menerapkan aturan yang tercantum dalam artikel, kami dengan mudah mencapai stabil 60 fps di game untuk iOS, bahkan di iPhone 5S. Mungkin beberapa aturan mungkin hanya spesifik untuk proyek kami, tetapi saya pikir sebagian besar dari mereka harus diingat ketika menulis kode atau memeriksanya untuk menghindari masalah di masa depan. Itu selalu lebih baik untuk terus-menerus menulis kode berdasarkan kinerja daripada nanti untuk refactor potongan kode besar.