Di level bisa ribuan musuh.Quest Defender: Valley of the Forgotten DX selalu memiliki masalah lama dengan kecepatan, dan saya akhirnya berhasil menyelesaikannya. Insentif utama untuk peningkatan kecepatan besar-besaran adalah
port kami
di PlayStation Vita . Gim ini sudah dirilis pada PC dan bekerja dengan baik, jika tidak sempurna, di
Xbox One dengan
PS4 . Tetapi tanpa perbaikan besar pada game, kami tidak akan pernah bisa meluncurkannya di Vita.
Ketika sebuah permainan melambat, komentator di Internet biasanya menyalahkan bahasa pemrograman atau mesin. Memang benar bahwa bahasa seperti C # dan Java lebih mahal daripada C dan C ++, dan alat-alat seperti Unity memiliki masalah yang tidak dapat dipecahkan, seperti pengumpulan sampah. Bahkan, orang-orang datang dengan penjelasan seperti itu karena bahasa dan mesin adalah sifat paling jelas dari perangkat lunak. Tapi pembunuh kinerja sejati mungkin detail kecil yang bodoh yang tidak ada hubungannya dengan arsitektur.
0. Alat Pembuatan Profil
Hanya ada satu cara nyata untuk membuat game lebih cepat - untuk melakukan profiling. Cari tahu apa yang menghabiskan terlalu banyak waktu di komputer dan membuatnya menghabiskan lebih sedikit waktu di atasnya, atau bahkan lebih baik, membuatnya tidak membuang waktu
sama sekali .
Alat profiling paling sederhana adalah monitor sistem Windows standar (monitor kinerja):
Sebenarnya, ini adalah alat yang cukup fleksibel dan sangat mudah digunakan. Cukup tekan Ctrl + Alt + Delete, buka "Task Manager" dan klik pada tab "Performance". Namun, jangan menjalankan terlalu banyak program lain. Jika Anda perhatikan lebih dekat, Anda dapat dengan mudah mendeteksi puncak dalam penggunaan CPU dan bahkan kebocoran memori. Ini adalah cara yang tidak informatif, tetapi mungkin ini adalah langkah pertama dalam menemukan tempat yang lambat.
Quest Defender ditulis dalam bahasa
Haxe tingkat
tinggi yang dikompilasi ke dalam bahasa lain (target utama saya adalah C ++). Ini berarti bahwa alat apa pun yang mampu membuat profil C ++ juga dapat membuat profil kode C ++ Haxe yang saya hasilkan. Jadi ketika saya ingin memahami penyebab masalah, saya meluncurkan Performance Explorer dari Visual Studio:
Selain itu, konsol yang berbeda memiliki alat profil mereka sendiri, yang sangat nyaman, tetapi karena NDA saya tidak dapat memberi tahu Anda tentang mereka. Tetapi jika Anda memiliki akses ke mereka, tetapi pastikan untuk menggunakannya!
Alih-alih menulis tutorial yang mengerikan tentang cara menggunakan alat profiling seperti Performance Explorer, saya hanya meninggalkan tautan ke
dokumentasi resmi dan beralih ke topik utama - hal-hal menakjubkan yang menyebabkan peningkatan besar dalam produktivitas, dan bagaimana saya berhasil menemukannya !
1. Deteksi Masalah
Kinerja game tidak hanya kecepatan itu sendiri, tetapi juga persepsinya. Defender's Quest adalah game bergenre menara pertahanan yang dirender pada 60 FPS, tetapi dengan kecepatan gameplay bervariasi mulai dari 1 / 4x hingga 16x. Terlepas dari kecepatan game, simulasi menggunakan
cap waktu tetap dengan 60 pembaruan per detik dari waktu simulasi 1x. Artinya, jika Anda menjalankan game dengan kecepatan 16x, maka logika pembaruan akan benar-benar bekerja dengan frekuensi
960 FPS . Jujur saja, ini permintaan terlalu tinggi untuk game! Tapi akulah yang menciptakan mode ini, dan jika ternyata lambat, maka para pemain pasti akan menyadarinya.
Dan di dalam game ada level
seperti itu :
Ini adalah pertempuran bonus terakhir "Endless 2", ini juga "mimpi buruk pribadi saya." Tangkapan layar diambil dalam mode Game + Baru, di mana musuh tidak hanya jauh lebih kuat, tetapi juga memiliki fitur seperti memulihkan kesehatan. Strategi favorit pemain di sini adalah untuk memompa naga ke tingkat Roar maksimum (serangan AOE yang membuat musuh tertegun), dan di belakang mereka taruh sejumlah ksatria dengan Knockback dipompa semaksimal mungkin untuk mendorong semua orang yang melewati naga kembali ke area aksi mereka. Efek kumulatifnya adalah bahwa sekelompok besar monster tanpa henti tinggal di satu tempat, lebih lama daripada para pemain harus bertahan hidup jika mereka benar-benar membunuh mereka. Karena pemain harus
menunggu ombak dan tidak
membunuh mereka untuk menerima hadiah dan prestasi, strategi seperti itu sangat efektif dan brilian - ini persis perilaku pemain yang saya distimulasi.
Sayangnya, ini juga ternyata menjadi kasus
patologis untuk kinerja,
terutama ketika pemain ingin bermain dengan kecepatan 16x atau 8x. Tentu saja, hanya pemain yang paling hardcore yang akan mencoba untuk mendapatkan pencapaian "Hundredth Wave" di Game Baru + di level Endless 2, tetapi mereka hanya mereka yang berbicara permainan paling keras, jadi saya ingin mereka bahagia.
Ini hanya permainan 2D dengan banyak sprite, apa yang salah dengan itu?
Dan memang. Mari kita perbaiki.
2. Resolusi Tabrakan
Lihatlah screenshot ini:
Lihat bagel ini di sekitar ranger? Ini adalah area dampaknya - perhatikan bahwa ada juga zona mati yang tidak
dapat mencapai target. Setiap kelas memiliki area serangannya sendiri, dan setiap pemain bertahan memiliki area ukuran yang berbeda, tergantung pada tingkat dorongan dan parameter pribadi. Dan setiap bek dalam teori dapat membidik musuh apa pun di bidang jangkauannya. Hal yang sama berlaku untuk musuh jenis tertentu. Mungkin ada 36 pembela di peta (tidak termasuk karakter utama Azru), tetapi tidak ada batas atas pada jumlah musuh. Setiap bek dan musuh memiliki daftar target yang mungkin, dibuat berdasarkan panggilan untuk memeriksa area pada setiap langkah pembaruan (minus cut-off logis dari mereka yang tidak dapat menyerang saat ini, dan sebagainya).
Saat ini, prosesor video sangat cepat - jika Anda tidak terlalu memaksakannya, maka mereka dapat memproses hampir semua jumlah poligon. Tetapi bahkan CPU tercepat pun sangat mudah memiliki “bottleneck” dalam prosedur sederhana, terutama yang tumbuh secara eksponensial. Itu sebabnya permainan 2D bisa menjadi lebih lambat daripada permainan 3D yang jauh lebih indah - bukan karena programmer tidak bisa mengatasinya (mungkin ini juga, setidaknya dalam kasus saya), tetapi pada prinsipnya karena logika kadang-kadang bisa lebih mahal, dari menggambar! Pertanyaannya bukan berapa banyak objek yang ada di layar, tetapi apa yang mereka
lakukan .
Mari menjelajahi dan mempercepat pengenalan tabrakan. Sebagai perbandingan, saya akan mengatakan bahwa sebelum optimasi, pengenalan tabrakan memakan waktu hingga ~ 50% dari waktu CPU dalam siklus pertempuran utama. Setelah optimasi, kurang dari 5%.
Ini semua tentang pohon kuadran
Solusi utama untuk masalah pengenalan tabrakan lambat adalah dengan
membagi ruang - dan sejak awal kami menggunakan implementasi
pohon kuadran yang berkualitas tinggi. Pada dasarnya, ini secara efektif memisahkan ruang sehingga banyak pemeriksaan tabrakan opsional dapat dilewati.
Di setiap frame, kami memperbarui seluruh pohon kuadran (QuadTree) untuk melacak posisi setiap objek, dan ketika musuh atau pembela ingin membidik seseorang, ia meminta QuadTree untuk daftar objek terdekat. Tetapi profiler memberi tahu kami bahwa kedua operasi ini jauh lebih lambat dari yang seharusnya.
Apa yang salah di sini?
Ternyata - banyak.
Mengetik string
Karena saya menyimpan musuh dan pembela di satu pohon kuadran, saya harus menunjukkan apa yang saya cari, dan ini dilakukan seperti ini:
var things:Array<XY> = _qtree.queryRange(zone.bounds, "e"); //"e" - "enemy"
Dalam jargon programmer, ini disebut kode
pengetikan string , dan, di antara alasan lain, ini buruk karena perbandingan string selalu lebih lambat daripada perbandingan integer.
Saya segera mengambil konstanta integer dan mengganti kode dengan ini:
var things:Array<XY> = _qtree.queryRange(zone.bounds, QuadTree.ENEMY);
(Ya, mungkin layak menggunakan
Enum Abstract untuk keamanan tipe maksimum, tapi saya sedang terburu-buru, dan saya perlu melakukan pekerjaan terlebih dahulu.)
Perubahan ini sendiri memberikan kontribusi yang
sangat besar , karena fungsi ini disebut
terus -
menerus dan secara rekursif, setiap kali seseorang membutuhkan daftar tujuan yang baru.
Array vs Vektor
Lihatlah ini:
var things:Array<XY>
Array Haxe sangat mirip dengan array ActionScript dan JS karena mereka adalah kumpulan objek yang dapat diubah ukurannya, tetapi di Haxe mereka sangat diketik.
Namun, ada struktur data lain yang lebih efisien dengan bahasa target statis seperti cpp, yaitu
haxe.ds.Vector . Vektor Haxe pada dasarnya sama dengan array, kecuali ketika mereka dibuat mereka mendapatkan ukuran tetap.
Karena pohon kuadran saya sudah memiliki volume yang tetap, saya mengganti array dengan vektor untuk mencapai peningkatan kecepatan yang nyata.
Minta hanya apa yang Anda butuhkan
Sebelumnya, fungsi
queryRange
saya mengembalikan daftar objek, instance
XY
. Mereka berisi koordinat x / y dari objek game yang direferensikan dan pengenal bilangan bulatnya yang unik (indeks pencarian di larik utama). Objek game yang mengeksekusi permintaan menerima XY ini, mengekstraksi pengidentifikasi integer untuk mendapatkan targetnya, dan kemudian melupakan sisanya.
Jadi mengapa saya harus meneruskan semua referensi ini ke objek XY untuk setiap node QuadTree
secara rekursif , dan bahkan
960 kali per frame? Cukup bagi saya untuk mengembalikan daftar pengidentifikasi bilangan bulat.
PETUNJUK PROFESIONAL: bilangan bulat jauh lebih cepat untuk dikirim daripada hampir semua tipe data lainnya!Dibandingkan dengan koreksi lain, ini cukup sederhana, tetapi pertumbuhan kinerja masih terlihat, karena loop internal ini digunakan dengan sangat aktif.
Optimasi rekursi ekor
Ada hal yang elegan yang disebut
optimasi Tail-call . Sulit untuk dijelaskan, jadi saya akan menunjukkan kepada Anda sebuah contoh.
Itu:
nw.queryRange(Range, -1, result);
ne.queryRange(Range, -1, result);
sw.queryRange(Range, -1, result);
se.queryRange(Range, -1, result);
return result;
Itu menjadi:
return se.queryRange(Range, filter, sw.queryRange(Range, filter, ne.queryRange(Range, filter, nw.queryRange(Range, filter, result))));
Kode mengembalikan hasil logis yang sama, tetapi menurut profiler, opsi kedua lebih cepat, setidaknya ketika menerjemahkan ke cpp. Kedua contoh melakukan logika yang persis sama - mereka membuat perubahan pada struktur data "hasil" dan meneruskannya ke fungsi berikutnya sebelum kembali. Ketika kita melakukan ini secara rekursif, kita dapat menghindari kompiler yang menghasilkan referensi sementara, karena ia dapat langsung mengembalikan hasil dari fungsi sebelumnya, daripada menempel padanya dalam langkah tambahan. Atau sesuatu seperti itu. Saya tidak sepenuhnya mengerti cara kerjanya, jadi baca posting di tautan di atas.
(Menilai dari apa yang saya ketahui, versi saat ini dari kompiler Haxe tidak memiliki fungsi optimisasi rekursi ekor, yaitu, mungkin merupakan karya kompiler C ++ - jadi jangan heran jika trik ini tidak berfungsi ketika menerjemahkan kode Haxe bukan dalam cpp.)
Pooling Objek
Jika saya membutuhkan hasil yang akurat, maka saya harus menghancurkan dan membangun kembali QuadTree lagi dengan setiap panggilan pembaruan. Membuat instance QuadTree baru adalah tugas yang cukup umum, tetapi dengan sejumlah besar objek AABB dan XY baru, QuadTrees yang bergantung padanya menyebabkan memori yang berlebihan. Karena ini adalah objek yang sangat sederhana, akan logis untuk mengalokasikan banyak objek seperti itu di muka dan hanya terus menggunakannya kembali. Ini disebut
objek pool .
Dulu saya melakukan sesuatu seperti ini:
nw = new QuadTree( new AABB( cx - hs2x, cy - hs2y, hs2x, hs2y) );
ne = new QuadTree( new AABB( cx + hs2x, cy - hs2y, hs2x, hs2y) );
sw = new QuadTree( new AABB( cx - hs2x, cy + hs2y, hs2x, hs2y) );
se = new QuadTree( new AABB( cx + hs2x, cy + hs2y, hs2x, hs2y) );
Tapi kemudian saya mengganti kode dengan ini:
nw = new QuadTree( AABB.get( cx - hs2x, cy - hs2y, hs2x, hs2y) );
ne = new QuadTree( AABB.get( cx + hs2x, cy - hs2y, hs2x, hs2y) );
sw = new QuadTree( AABB.get( cx - hs2x, cy + hs2y, hs2x, hs2y) );
se = new QuadTree( AABB.get( cx + hs2x, cy + hs2y, hs2x, hs2y) );
Kami menggunakan kerangka kerja
HaxeFlixel open source, jadi kami
menerapkan ini menggunakan kelas
FlxPool HaxeFlixel. Dalam hal optimasi yang sangat terspesialisasi, saya sering mengganti beberapa elemen Flixel dasar (misalnya, pengenalan tabrakan) dengan implementasi saya sendiri (seperti yang saya lakukan dengan QuadTrees), tetapi FlxPool lebih baik daripada semua yang saya tulis sendiri dan ia melakukan persis apa yang dibutuhkan.
Spesialisasi jika perlu
Objek
XY
adalah kelas sederhana yang memiliki properti
x
,
y
dan
int_id
. Karena ini digunakan dalam loop internal yang sangat aktif digunakan, saya bisa menyimpan banyak perintah alokasi memori dan operasi dengan memindahkan semua data ini ke struktur data khusus yang menyediakan fungsi yang sama dengan
Vector<XY>
. Saya memanggil kelas
XYVector
baru ini dan hasilnya dapat dilihat di
sini . Ini adalah aplikasi yang sangat khusus dan tidak fleksibel pada saat yang bersamaan, tetapi telah memberi kami beberapa peningkatan kecepatan.
Fungsi Bawaan
Sekarang, setelah kita menyelesaikan fase luas pengakuan tabrakan, kita perlu melakukan banyak pemeriksaan untuk mengetahui objek mana yang benar-benar bertabrakan. Jika memungkinkan, saya mencoba membandingkan poin dan angka, bukan angka dan angka, tetapi kadang-kadang saya harus melakukan yang terakhir. Bagaimanapun, semua ini memerlukan pemeriksaan khusus sendiri:
private static function _collide_circleCircle(a:Zone, b:Zone):Bool { var dx:Float = a.centerX - b.centerX; var dy:Float = a.centerY - b.centerY; var d2:Float = (dx * dx) + (dy * dy); var r2:Float = (a.radius2) + (b.radius2); return d2 < r2; }
Semua ini dapat ditingkatkan dengan satu
inline
:
private static inline function _collide_circleCircle(a:Zone, b:Zone):Bool { var dx:Float = a.centerX - b.centerX; var dy:Float = a.centerY - b.centerY; var d2:Float = (dx * dx) + (dy * dy); var r2:Float = (a.radius2) + (b.radius2); return d2 < r2; }
Ketika kita menambahkan sebaris ke suatu fungsi, kita memberitahu kompiler untuk menyalin dan menempelkan kode ini dan menempelkan variabel ketika digunakan, dan tidak membuat panggilan eksternal ke fungsi terpisah, yang mengarah ke biaya yang tidak perlu. Penyematan tidak selalu berlaku (misalnya, ia mengembang jumlah kode), tetapi sangat ideal untuk situasi di mana fungsi kecil dipanggil berulang kali.
Kami mengingatkan konflik
Pelajaran sebenarnya di sini adalah bahwa di dunia nyata, optimisasi tidak selalu dari jenis yang sama. Perbaikan semacam itu adalah campuran dari teknik-teknik canggih, peretasan murah, penerapan rekomendasi logis dan menghilangkan kesalahan bodoh. Semua ini secara umum memberi kita dorongan kinerja.
Tapi tetap saja -
ukur tujuh kali, potong satu!Dua jam optimisasi fungsi yang hebat, yang disebut sekali setiap enam frame dan mengambil 0,001 ms, tidak sepadan dengan usaha, meskipun keburukan dan kebodohan kode.
3. Urutkan semuanya
Sebenarnya, itu adalah salah satu peningkatan terakhir saya, tetapi ternyata sangat menguntungkan sehingga pantas disebut judulnya sendiri. Selain itu, itu adalah yang paling sederhana dan berulang kali membuktikan dirinya. Profiler menunjukkan kepada saya prosedur yang tidak bisa saya perbaiki sama sekali - pengundian utama () loop, yang memakan waktu terlalu banyak. Alasannya adalah fungsi yang mengurutkan semua elemen layar sebelum rendering - yaitu,
menyortir semua sprite memakan waktu lebih lama daripada menggambar mereka!
Jika Anda melihat tangkapan layar dari permainan, Anda akan melihat bahwa semua musuh dan pembela diurutkan terlebih dahulu oleh
y
, dan kemudian oleh
x
, sehingga elemen-elemen saling tumpang tindih dari belakang ke depan, dari kiri ke kanan, ketika kita bergerak dari kiri atas ke sudut kanan bawah layar.
Salah satu cara untuk mengecoh penyortiran adalah dengan melewatkan rendering sortir melalui frame. Ini adalah trik yang berguna untuk beberapa fungsi yang mahal, tetapi segera menyebabkan bug visual yang sangat nyata, jadi itu tidak cocok untuk kita.
Akhirnya, keputusan datang dari salah satu pengelola HaxeFlixel,
Jens Fisher . Dia bertanya: "Apakah Anda memastikan Anda menggunakan algoritma pengurutan yang cepat untuk array yang hampir diurutkan?"
Tidak! Ternyata tidak. Saya menggunakan penyortiran array dari pustaka standar Haxe (saya pikir itu
menggabungkan penyortiran - pilihan yang baik untuk kasus-kasus umum. Tetapi saya memiliki kasus yang sangat
istimewa . Ketika menyortir di setiap frame, posisi penyortiran hanya mengubah sejumlah kecil sprite, bahkan jika ada banyak. Saya mengganti panggilan sortir lama dengan
menyortir dengan menyisipkan , dan
boom! - kecepatan langsung meningkat.
4. Masalah teknis lainnya
Pengenalan tabrakan dan penyortiran adalah kemenangan besar dalam logika
update()
dan
draw()
, tetapi banyak lagi jebakan yang disembunyikan dalam loop internal yang aktif digunakan.
Std.is () dan pemain
Dalam loop batin "panas" yang berbeda, saya memiliki kode yang sama:
if(Std.is(something,Type)) { var typed:Type = cast(something,Type); }
Dalam bahasa Haxe,
Std.is()
memberi tahu kita apakah suatu objek termasuk tipe tertentu (Type) atau kelas (Class), dan
cast
mencoba untuk melemparkannya ke tipe tertentu selama eksekusi program.
Ada versi
cast
yang aman dan tidak terlindungi yang menyebabkan penurunan kinerja, tetapi cast yang tidak terlindungi tidak.
Aman:
cast(something, Type);
Tidak dilindungi:
var typed:Type = cast something;
Ketika upaya pemeran yang tidak aman gagal, kami mendapatkan nol, sementara pemeran yang aman melempar pengecualian. Tetapi jika kita tidak akan menangkap pengecualian, lalu apa gunanya melakukan cast yang aman? Tanpa tangkapan, operasinya masih gagal, tetapi kerjanya lebih lambat.
Selain itu, tidak ada gunanya untuk mendahului gips yang aman dengan
Std.is()
. Satu-satunya alasan untuk menggunakan pemeran aman adalah pengecualian yang dijamin, tetapi jika kami memeriksa jenis sebelum pemeran, kami sudah menjamin bahwa pemeran tidak akan gagal!
Saya bisa mempercepat sedikit dengan pemain
Std.is()
setelah memeriksa
Std.is()
. Tetapi mengapa kita perlu menulis ulang hal yang sama jika saya tidak perlu memeriksa jenis kelas sama sekali?
Misalkan saya memiliki
CreatureSprite
, yang dapat menjadi turunan dari subclass dari
DefenderSprite
atau
EnemySprite
. Alih-alih memanggil
Std.is(this,DefenderSprite)
kita bisa membuat bidang integer di
CreatureSprite
dengan nilai-nilai seperti
CreatureType.DEFENDER
atau
CreatureType.ENEMY
, yang diperiksa lebih cepat.
Saya ulangi, perlu untuk memperbaikinya hanya di tempat-tempat di mana pelambatan signifikan dicatat dengan jelas.
Ngomong-ngomong, Anda dapat membaca lebih lanjut tentang pemeran yang
aman dan
tidak terlindungi dalam
manual Haxe .
Serialisasi / Deserialisasi Semesta
Menjengkelkan menemukan tempat-tempat seperti itu dalam kode:
function copy():SomeClass { return SomeClass.fromXML(this.toXML()); }
Ya Untuk menyalin suatu objek, kami membuat
serialisasi ke dalam XML , dan kemudian
parsing semua XML ini , setelah itu kami langsung membuang XML dan mengembalikan objek baru. Ini mungkin cara paling lambat untuk menyalin suatu objek, selain itu, itu membebani memori. Awalnya, saya menulis panggilan XML untuk menyimpan dan memuat dari disk, dan saya pikir saya terlalu malas untuk menulis prosedur penyalinan yang benar.
Mungkin, semuanya akan beres jika fungsi ini jarang digunakan, tetapi panggilan ini muncul di tempat yang tidak pantas di tengah gameplay. Jadi saya duduk dan mulai menulis dan menguji fungsi salin yang benar.
Katakan tidak pada Null
Pemeriksaan kesetaraan untuk null digunakan cukup sering, tetapi ketika menerjemahkan Haxe ke cpp, objek yang memungkinkan nilai tidak terbatas mengarah ke biaya yang tidak perlu yang tidak muncul jika kompilator dapat mengasumsikan bahwa objek tidak akan pernah menjadi nol. Hal ini terutama berlaku untuk tipe dasar seperti
Int
- Haxe mengimplementasikan validitas nilai yang tidak ditentukan untuk mereka dalam sistem target statis dengan "pengepakan" mereka, yang terjadi tidak hanya untuk variabel yang secara eksplisit dinyatakan sebagai nol (
var myVar:Null<Int>
), tetapi juga untuk hal-hal seperti opsi pembantu (
?myParam:Int
). Selain itu, pemeriksaan nol sendiri menyebabkan pemborosan yang tidak perlu.
Saya dapat memperbaiki beberapa masalah ini hanya dengan melihat kode dan memikirkan alternatif - dapatkah saya melakukan pemeriksaan yang lebih sederhana, yang akan selalu benar ketika objeknya nol? Bisakah saya menangkap nol lebih awal dalam rantai panggilan fungsi dan mengoper bilangan bulat sederhana atau boolean ke panggilan anak? Bisakah saya menyusun semuanya sehingga nilainya
tidak pernah dijamin menjadi nol? Dan sebagainya. Kami tidak dapat sepenuhnya menghilangkan cek nol dan nilai nullable, tetapi mengeluarkannya dari fungsi sangat membantu saya.
5. Waktu pengunduhan
Di PSVita, kami memiliki masalah serius khusus dengan waktu pemuatan beberapa adegan. Ketika membuat profil, ternyata alasan utamanya adalah rasterisasi teks, perenderan perangkat lunak yang tidak perlu, perenderan tombol yang mahal, dan hal-hal lainnya.
Teks
HaxeFlixel didasarkan pada
OpenFL , yang memiliki TextField luar biasa dan andal. Tapi saya menggunakan objek FlxText dengan cara yang tidak sempurna - objek FlxText memiliki bidang teks OpenFL internal yang dirasterisasi. Namun, ternyata saya tidak membutuhkan sebagian besar fungsi teks yang kompleks ini, tetapi karena cara bodoh mengatur sistem UI saya, bidang teks harus dirender sebelum semua objek lain ditemukan. Ini menyebabkan lompatan kecil tapi nyata, misalnya, saat memuat jendela sembulan.
Di sini saya membuat tiga koreksi - pertama, saya mengganti teks sebanyak mungkin dengan font raster. Flixel memiliki dukungan bawaan untuk berbagai format font raster, termasuk
BMFont AngelCode , yang membuatnya mudah untuk bekerja dengan Unicode, style dan kerning, tetapi API teks raster sedikit berbeda dari API teks biasa, jadi saya harus menulis kelas pembungkus kecil untuk menyederhanakan transisi. (Saya memberinya nama yang sesuai,
FlxUITextHack
).
Ini sedikit meningkatkan pekerjaan - font bitmap render dengan sangat cepat - tetapi sedikit meningkatkan kerumitan: Saya harus secara khusus menyiapkan set karakter terpisah dan menambahkan logika switching tergantung pada lokal, alih-alih hanya menyiapkan kotak teks yang melakukan semua pekerjaan.
Perbaikan kedua adalah membuat objek UI baru yang merupakan tempat
penampung sederhana untuk teks tetapi memiliki properti publik yang sama dengan teks. Saya menyebutnya "area teks" dan membuat kelas baru untuk itu di perpustakaan UI saya sehingga sistem UI saya dapat menggunakan area teks ini dengan cara yang sama seperti bidang teks nyata, tetapi tidak membuat apa pun sampai menghitung ukuran dan posisi untuk yang lainnya. Kemudian, ketika adegan saya disiapkan, saya memulai prosedur penggantian area teks ini dengan bidang teks nyata (atau bidang teks font bitmap).
Koreksi ketiga menyangkut persepsi. Jika ada jeda antara input dan reaksi bahkan dalam setengah detik, pemain menganggap ini sebagai pengereman. Oleh karena itu, saya mencoba untuk menemukan semua adegan di mana terdapat keterlambatan dalam input hingga transisi berikutnya, dan menambahkan lapisan yang tembus cahaya dengan kata "Memuat ..." atau hanya lapisan tanpa teks. Koreksi sederhana semacam itu sangat meningkatkan
persepsi responsif permainan, karena sesuatu terjadi segera setelah pemain menyentuh kontrol, bahkan jika perlu beberapa saat untuk menampilkan menu.
Render perangkat lunak
Sebagian besar menu menggunakan kombinasi penskalaan perangkat lunak dan pengomposisian 9-slice. Ini terjadi karena dalam versi PC ada UI resolusi-independen yang dapat bekerja dengan rasio aspek 4: 3 dan 16: 9, diskalakan sesuai. Tetapi pada PSVita kita sudah
mengetahui resolusi, yaitu, kita tidak membutuhkan semua sumber daya resolusi tinggi ekstra ini dan algoritma penskalaan waktu-nyata. Kami hanya dapat melakukan pra-render sumber daya ke resolusi yang tepat dan menempatkannya di layar.
Pertama, saya masuk ke markup UI untuk kondisi Vita yang mengubah permainan menggunakan sumber daya paralel. Kemudian saya perlu membuat sumber daya ini disiapkan untuk satu izin.
Debugger HaxeFlixel ternyata sangat berguna di
sini - saya menambahkan skrip saya ke sana sehingga hanya menghapus cache raster ke disk. Kemudian saya membuat konfigurasi build khusus untuk Windows yang mensimulasikan izin untuk Vita, membuka semua menu game secara bergantian, beralih ke debugger, dan meluncurkan perintah ekspor untuk versi sumber daya yang diskalakan sebagai PNG siap pakai. Kemudian saya mengganti nama mereka dan menggunakannya sebagai sumber daya untuk Vita.
Tombol Rendering
Sistem UI saya memiliki masalah nyata dengan tombol - ketika mereka dibuat, tombol-tombol yang diberikan set sumber daya default, dan sesaat kemudian mereka mengubah ukuran (dan membuat lagi) kode boot UI, dan kadang-kadang bahkan
ketiga kalinya, sebelum seluruh UI dimuat . Saya memecahkan masalah ini dengan menambahkan opsi yang menunda rendering tombol ke tahap terakhir.
Pemindaian teks opsional
Majalah itu memuat sangat lambat. Awalnya saya pikir masalahnya adalah pada bidang teks, tetapi tidak. Teks majalah dapat berisi tautan ke halaman lain, yang ditunjukkan oleh karakter khusus yang tertanam dalam teks mentah itu sendiri. Karakter-karakter ini kemudian dipotong dan digunakan untuk menghitung lokasi tautan.
Ternyata. bahwa saya memindai
setiap bidang teks untuk menemukan dan mengganti karakter-karakter ini dengan tautan yang diformat dengan benar, bahkan tanpa memeriksa terlebih dahulu apakah ada karakter khusus dalam bidang teks ini! Lebih buruk lagi, menurut desain, tautan
hanya digunakan pada halaman konten, tapi saya memeriksanya di setiap kotak teks di setiap halaman.
Saya berhasil menyiasati semua cek ini menggunakan if if dari bentuk "Apakah kotak teks ini menggunakan tautan sama sekali". Jawaban atas pertanyaan ini biasanya tidak. Akhirnya, halaman yang membutuhkan waktu paling lama untuk memuat ternyata adalah halaman indeks. Karena tidak pernah berubah dalam menu jurnal, mengapa kita tidak menyimpannya?
6. Memori profil
Kecepatan bukan hanya CPU. Memori juga bisa menjadi masalah, terutama pada platform yang lemah seperti Vita. Bahkan ketika Anda berhasil menghilangkan kebocoran memori terakhir, Anda mungkin masih memiliki masalah dengan penggunaan memori gigi gergaji di lingkungan pengumpulan sampah.Apa itu penggunaan memori gigi gergaji? Pengumpul sampah berfungsi sebagai berikut: data dan objek yang tidak Anda gunakan menumpuk dari waktu ke waktu dan secara berkala dibersihkan. Tetapi Anda tidak memiliki kontrol yang jelas kapan hal ini terjadi, sehingga grafik penggunaan memori terlihat seperti gergaji:Buang sampah
Karena pembersihan tidak instan, jumlah total RAM yang Anda gunakan biasanya lebih besar dari yang sebenarnya Anda butuhkan. Tetapi jika Anda melebihi jumlah total sistem RAM, satu dari dua hal dapat terjadi - pada PC, Anda mungkin hanya menggunakan file halaman , yaitu, sementara mengubah sebagian ruang hard disk ke RAM virtual. Alternatif dalam lingkungan memori yang terbatas (seperti konsol) adalah untuk crash aplikasi, bahkan jika tidak ada cukup sepasang byte yang menyedihkan. Dan ini akan terjadi bahkan jika Anda tidak menggunakan byte ini dan pengumpulan sampah akan dilakukan di dalamnya segera!Hal yang baik tentang Haxe adalah bahwa itu sepenuhnya open source, yaitu, Anda tidak terkunci dalam kotak hitam yang tidak dapat Anda perbaiki, seperti halnya dengan Unity. Dan backend hxcpp menyediakan manajemen pengumpulan sampah yang luas langsung dari API!Kami menggunakannya untuk menghapus memori secara instan setelah level besar agar tetap dalam batas yang diberikan:cpp.vm.Gc.run(false); // (true/false - / )
Anda tidak boleh menggunakannya secara sukarela jika Anda tidak tahu apa yang Anda lakukan, tetapi nyaman jika ada alat seperti itu saat dibutuhkan.7. Penanganan masalah melalui desain
Semua peningkatan kinerja ini lebih dari cukup untuk mengoptimalkan permainan untuk PC, tetapi kami juga mencoba untuk merilis versi untuk PSVita, dan kami memiliki rencana jangka panjang untuk Nintendo Switch, jadi kami harus memeras segala sesuatu mulai dari kode hingga drop.Namun seringkali ada "tunnel vision" ketika Anda hanya berfokus pada peretasan teknis dan lupa bahwa perubahan desain yang sederhana dapat sangat memperbaiki situasi .Mempercepat efek dengan kecepatan tinggi
Pada 16x, banyak efek terjadi begitu cepat sehingga pemain bahkan tidak melihatnya. Kami telah menggunakan satu trik - kilat Azra menjadi lebih mudah dengan kecepatan permainan, dan jumlah partikel untuk serangan AOE lebih rendah. Kami melengkapi teknik ini dengan menonaktifkan angka kerusakan berkecepatan tinggi dan trik serupa lainnya.Kami juga menyadari bahwa pada titik tertentu, kecepatan 16x sebenarnya mungkin lebih lambat dari kecepatan 8x ketika ada terlalu banyak objek di layar, jadi ketika jumlah musuh meningkat hingga batas tertentu, kami secara otomatis mengurangi kecepatan permainan menjadi 8x atau 4x. Dalam praktiknya, pemain cenderung melihat ini hanya di Endless Battle 2. Ini memungkinkan kinerja yang mulus dan rendering tanpa membebani CPU.Kami juga menggunakan batasan khusus untuk platform. Pada Vita, kami melewatkan efek kilat ketika Azra memicu atau mempercepat karakter, dan menggunakan trik serupa lainnya.Menyembunyikan tubuh
Dan bagaimana dengan tumpukan besar musuh di sudut kanan bawah Endless Battle 2 - ada ratusan atau bahkan ribuan musuh yang menggambar satu di atas yang lain. Mengapa kita tidak melewatkan rendering yang tidak bisa kita lihat?Ini adalah trik desain licik yang membutuhkan pemrograman licik, karena kita membutuhkan algoritma pintar yang mendefinisikan objek tersembunyi.Sebagian besar game ini digambar menggunakan algoritme artis - objek sebelumnya dalam daftar gambar diblokir oleh semua yang datang setelahnya.Dengan membalik urutan rendering algoritme artis, Anda dapat membuat "peta penutup" dan mencari tahu apa yang harus disembunyikan. Saya menciptakan "kanvas" palsu dengan 8 tingkat "kegelapan" (hanya dua byte array) dengan resolusi yang jauh lebih rendah daripada medan perang nyata. Mulai dari akhir daftar render, kami mengambil kotak pembatas dari setiap objek dan "menggambar" di kanvas, meningkatkan "kegelapan" dari titik dengan 1 untuk setiap "piksel" yang tercakup oleh kotak pembatas resolusi rendah. Pada saat yang sama, kita membaca "kegelapan" rata-rata dari daerah di mana kita akan menggambar. Bahkan, kami memperkirakan berapa banyak menggambar ulang setiap objek akan mengalami dengan panggilan imbang nyata.Jika jumlah redraw yang diprediksi cukup tinggi, maka saya menandai musuh sebagai "dikubur," dengan dua ambang batas - dikubur sepenuhnya, yaitu, sepenuhnya tidak terlihat, atau dikubur sebagian, yaitu, ia akan ditarik, tetapi tanpa membuat bilah kesehatan.(Ngomong-ngomong, ini adalah fungsi pengecekan redraws.)Agar ini berfungsi dengan benar, Anda harus mengkonfigurasi dengan benar resolusi hide map. Jika terlalu besar, maka kita harus melakukan banyak panggilan draw yang disederhanakan, jika terlalu kecil, maka kita akan menyembunyikan objek terlalu agresif dan mendapatkan bug visual. Jika Anda memilih kartu dengan benar, efeknya hampir tidak terlihat, tetapi peningkatan kecepatan sangat terlihat - tidak ada cara untuk menggambar sesuatu lebih cepat daripada tidak menggambar sama sekali !Preload yang lebih baik daripada rem
Di tengah-tengah perkelahian, saya perhatikan sering mengerem, yang, saya yakin, disebabkan oleh jeda dalam pengumpulan sampah. Namun, profiling menunjukkan bahwa ini tidak benar. Pengujian lebih lanjut mengungkapkan bahwa ini terjadi pada awal gelombang musuh, dan kemudian saya menemukan bahwa ini hanya terjadi ketika itu adalah gelombang musuh yang tidak ada sebelumnya. Jelas, beberapa kode konfigurasi musuh menyebabkan masalah, dan tentu saja, ketika membuat profil, fungsi "panas" ditemukan dalam pengaturan gambar. Saya mulai bekerja pada pengaturan unduhan multi-utas yang kompleks, tetapi kemudian saya menyadari bahwa saya bisa memasukkan semua prosedur pemuatan grafik musuh ke dalam preload pertempuran. Secara terpisah, ini adalah unduhan yang sangat kecil, bahkan pada platform paling lambat menambahkan kurang dari satu detik untuk total waktu pemuatan pertempuran, tetapi mereka menghindari pengereman yang sangat nyata selama bermain game.Kami cadangan stok untuk nanti
Jika Anda bekerja di lingkungan dengan memori terbatas, maka Anda dapat menggunakan trik kuno industri kami - untuk mengalokasikan sebagian besar memori begitu saja, dan kemudian melupakannya sampai akhir proyek. Di akhir proyek, setelah menyia-nyiakan seluruh anggaran memori yang tersedia, Anda dapat diselamatkan berkat “telur sarang” ini.Kami menemukan diri kami dalam situasi seperti itu - kami hanya membutuhkan selusin byte untuk menyelamatkan perakitan untuk PSVita, tapi sial - kami lupa tentang trik ini dan karenanya macet! Satu-satunya pilihan yang tersisa adalah minggu operasi kode putus asa dan menyakitkan!Tapi tunggu sebentar! Salah satu optimisasi saya (gagal) memuat sumber daya sebanyak mungkin dan abadimenyimpannya dalam memori, karena saya keliru berasumsi bahwa waktu buka yang besar disebabkan oleh sumber daya membaca selama pelaksanaan program. Ternyata ini tidak benar, sehingga hampir semua panggilan ekstra untuk preloading dan penyimpanan kekal ini dapat sepenuhnya dihapus, dan saya masih memiliki memori gratis!Menyingkirkan hal-hal yang tidak kami gunakan
Saat mengerjakan build untuk PSVita, kami sangat jelas bahwa ada banyak hal yang tidak kami butuhkan. Karena resolusi rendah, mode grafik sumber dan mode grafis HD tidak bisa dibedakan, jadi untuk semua sprite kami menggunakan grafik asli. Kami juga berhasil meningkatkan fungsi penggantian palet dengan bantuan pixel shader khusus (sebelumnya kami menggunakan fungsi rendering program).Contoh lain adalah peta pertempuran itu sendiri - pada PC dan konsol rumah, kami menumpuk banyak kartu ubin di atas satu sama lain untuk membuat peta multi-layer. Tapi karena peta tidak pernah berubah, pada Vita kita bisa memanggang semuanya menjadi satu gambar jadi sehingga akan dipanggil dalam satu panggilan undian.Selain sumber daya tambahan, permainan memiliki banyak panggilan tambahan, misalnya, pembela dan musuh mengirimkan sinyal regenerasi di setiap frame, bahkan ketika mereka tidak memiliki kemampuan untuk regenerasi . Jika UI terbuka untuk makhluk seperti itu, maka digambar ulang di setiap bingkai .Ada setengah lusin contoh lain dari algoritma kecil yang menghitung sesuatu di dalam fungsi "panas", tetapi tidak pernah mengembalikan hasil di mana pun. Biasanya ini adalah hasil dari menciptakan struktur pada tahap awal pengembangan, jadi kami hanya memotongnya.NaNopocalypse
Kasus ini lucu. Profiler melaporkan bahwa dibutuhkan banyak waktu untuk menghitung sudut. Berikut adalah kode Haxe C ++ yang dihasilkan di profiler:Ini adalah salah satu fungsi yang mengambil nilai suka -90
dan dikonversi ke 270
. Terkadang Anda mendapatkan nilai seperti -724
, yang dalam beberapa siklus dikurangi menjadi 4
.Untuk beberapa alasan, nilai diteruskan ke fungsi ini -2147483648
.Mari kita lakukan perhitungan. Jika dalam setiap siklus kita menambahkan 360 ke -2147483648, maka akan dibutuhkan sekitar 5,965.233 iterasi hingga menjadi lebih besar dari 0 dan menyelesaikan siklus. Omong-omong, siklus ini dilakukan dengan setiap pembaruan (tidak di setiap frame - di setiap pembaruan !) - setiap kali ketika proyektil (atau sesuatu yang lain) berubah sudutnya.Tentu saja, itu adalah kesalahan saya, karena saya memberikan nilai NaN
- nilai khusus yang berarti "Bukan angka" (bukan angka), yang biasanya menandakan kesalahan yang sebelumnya terjadi dalam kode. Jika Anda membawanya ke integer tanpa terlebih dahulu memeriksa, maka hal-hal aneh seperti itu terjadi.Sebagai perbaikan sementara, saya menambahkan cekMath.isNan()
, yang mengatur ulang sudut ketika peristiwa (agak jarang, tapi tak terhindarkan) terjadi. Pada saat yang sama, saya terus mencari akar penyebab kesalahan, menemukannya, dan penundaan segera menghilang. Ternyata jika Anda tidak melakukan 6 juta iterasi yang tidak berarti, maka Anda bisa mendapatkan peningkatan besar dalam kecepatan!(Perbaikan untuk kesalahan ini dimasukkan ke dalam HaxeFlixel sendiri).Jangan mengakali diri sendiri
OpenFL dan HaxeFlixel didasarkan pada caching sumber daya. Ini berarti bahwa ketika kita memuat sumber daya, waktu berikutnya sumber daya ini diterima, itu diambil dari cache, dan tidak dimuat ulang dari disk. Perilaku ini dapat ditimpa, dan terkadang masuk akal.Namun, saya masuk ke beberapa hal aneh yang dibuat-buat: Saya mengunduh sumber daya, secara eksplisit mengatakan kepada sistem untuk tidak men- cache hasil, karena saya benar-benar yakin dengan apa yang saya lakukan dan tidak ingin "membuang memori" pada cache. Bertahun-tahun kemudian, panggilan "pintar" ini membuat saya memuat sumber daya yang sama berulang-ulang, memperlambat permainan dan membuang-buang memori yang berharga, yang saya "simpan" dengan meninggalkan cache.8. Selain itu, mungkin tidak layak melakukan level seperti Endless Battle 2
Ya, sangat bagus bahwa kami menerapkan semua trik kecil ini untuk meningkatkan kecepatan. Sejujurnya, kami tidak memperhatikan sebagian besar dari mereka sampai kami mulai memindahkan game ke sistem yang kurang kuat, ketika pada beberapa level masalahnya menjadi sangat tak tertahankan. Saya senang bahwa pada akhirnya kami berhasil meningkatkan kecepatan, tetapi saya pikir desain level patologis juga harus dihindari. Endless Battle 2 memberi terlalu banyak tekanan pada sistem, terutama dibandingkan dengan semua level permainan lainnya .Bahkan setelah semua perubahan ini, versi PSVita masih tidak dapat mengatasi desain Endless 2 yang asli, dan saya tidak ingin mengambil risiko kecepatan pada model dasar XB1 dan PS4, jadi saya mengubah keseimbangan untuk versi konsol Endless 2. Saya mengurangi jumlah musuh, tetapi menambah karakteristik mereka, tetapi menambah karakteristik mereka. sehingga levelnya memiliki kesulitan yang kira-kira sama. Selain itu, pada PSVita kami membatasi jumlah gelombang hingga seratus untuk menghindari risiko kegagalan memori, tetapi tidak menambahkan batasan pada PS4 dan XB1. Berkat ini, mencapai pencapaian ketahanan masih sama sulitnya di semua konsol. Dalam versi PC, desain level Endless Batlte 2 tetap tidak berubah.Semua ini adalah pelajaran bagi kami, yang akan kami perhitungkan saat membuat Defender's Quest II - kami akan sangat memperhatikan level tanpa batas atas jumlah musuh di layar! Tentu saja, misi "tanpa akhir" sangat menarik bagi penggemar Tower Defense, jadi saya tidak akan menyingkirkan mereka sepenuhnya, tetapi bagaimana dengan level dengan pos pemeriksaan di mana pemain HARUS menghancurkan segala sesuatu di layar sebelum pindah ke gelombang berikutnya? Ini tidak hanya akan memungkinkan kita untuk membatasi jumlah musuh di layar, tetapi juga menyadari menyelamatkan di tengah level tanpa ribut-ribut dengan mengurutkan keadaan sup benda gila dalam pertempuran yang intens - itu akan cukup bagi kita untuk hanya menyimpan koordinat para pembela, meningkatkan level, dll.9. Pikiran dalam kesimpulan
Performa permainan adalah topik yang kompleks karena pemain seringkali tidak memahami apa itu, dan kita seharusnya tidak mengharapkan pemahaman seperti itu dari mereka. Tapi saya harap artikel ini sedikit mengklarifikasi bagi Anda bagaimana segala sesuatu terlihat di dalamnya, dan Anda telah belajar lebih banyak tentang bagaimana desain, pertukaran teknis dan keputusan bodoh memperlambat permainan.Intinya adalah bahwa bahkan dalam game dengan desain bagus yang dikembangkan oleh tim yang berbakat, fragmen kode "berkarat" kecil seperti itu dapat ditemukan di mana-mana . Namun dalam praktiknya, hanya sebagian kecil dari mereka yang benar-benar memengaruhi kinerja. Kemampuan untuk mendeteksi dan menghilangkannya sama-sama seni dan sains.Saya senang bahwa kami akan memanfaatkan semua keuntungan ini dalam pengembangan Quest II dari Defender. Jujur saja, jika kami belum membuat port untuk PSVita, maka saya mungkin tidak akan mencoba setengah dari optimasi ini. Dan bahkan jika Anda tidak membeli game untuk PSVita, Anda dapat berterima kasih kepada konsol kecil ini, yang secara signifikan meningkatkan kecepatan Quest Defender.