Pelajari rendering dalam game loop

Salah satu cara yang sangat populer untuk menerapkan game loop adalah seperti ini:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until it’s time to do the next frame
}

Ada beberapa masalah dengan cara ini. Yang paling mendasar adalah ide bahwa game dapat menentukan apa itu "frame". Setiap layar memiliki kecepatan frame yang berbeda-beda, dan kecepatan ini dapat berubah dari waktu ke waktu. Jika memunculkan frame secara lebih cepat daripada yang dapat ditampilkan oleh layar, Anda terkadang harus melompati frame. Jika memunculkan terlalu lambat, SurfaceFlinger akan sesekali gagal menemukan buffer baru dan akan menampilkan kembali frame sebelumnya. Kedua situasi ini dapat menyebabkan gangguan yang terlihat.

Yang perlu Anda lakukan adalah mencocokkan kecepatan frame layar, dan membuat status game berjalan maju berdasarkan jumlah waktu yang telah berlalu sejak frame sebelumnya. Ada beberapa cara untuk melakukannya:

  • Menggunakan library Frame Pacing Android (disarankan)
  • Isi BufferQueue hingga penuh dan andalkan back-pressure "swap buffer"
  • Gunakan Choreographer (API 16+)

Library Frame Pacing Android

Lihat Mencapai kecepatan frame yang tepat untuk mengetahui informasi tentang cara menggunakan library ini.

Penjejalan antrean

Ini sangat mudah diterapkan: cukup tukar buffer secepat mungkin. Pada versi awal Android, hal ini sebenarnya dapat menyebabkan penalti ketika SurfaceView#lockCanvas() akan membuat Anda masuk mode tidur selama 100 md. Sekarang ini digerakkan oleh BufferQueue, dan BufferQueue dikosongkan secepat yang SurfaceFlinger bisa lakukan.

Salah satu contoh dari pendekatan ini dapat dilihat di Android Breakout. Aplikasi ini menggunakan GLSurfaceView, yang berjalan dalam loop yang memanggil callback onDrawFrame() aplikasi, lalu menukar buffer. Jika BufferQueue penuh, panggilan eglSwapBuffers() akan menunggu hingga buffer tersedia. Buffer akan tersedia saat SurfaceFlinger melepaskannya, yaitu setelah SurfaceFlinger memperoleh buffer baru untuk layar. Karena hal ini terjadi di VSYNC, setelan waktu loop gambar akan cocok dengan kecepatan refresh. Ini berlaku untuk sebagian besar kasus.

Ada beberapa masalah dengan pendekatan ini. Pertama, aplikasi dipengaruhi oleh aktivitas SurfaceFlinger, yang akan memakan waktu yang berbeda-beda tergantung pada seberapa banyak pekerjaan yang harus dilakukan dan apakah aktivitas tersebut harus berebut waktu CPU dengan proses lain. Karena status game berjalan maju sesuai dengan waktu antara satu penukaran buffer ke penukaran lainnya, animasi tidak akan diperbarui dengan kecepatan yang konsisten. Namun, saat berjalan pada kecepatan 60 fps dengan inkonsistensi yang dirata-ratakan dari waktu ke waktu, Anda mungkin tidak akan menyadari adanya perubahan.

Kedua, beberapa pertukaran buffer pertama akan terjadi dengan sangat cepat karena BufferQueue belum penuh. Waktu yang dihitung antar-frame akan mendekati nol, sehingga game akan menghasilkan beberapa frame yang tidak menunjukkan perubahan. Dalam game seperti Breakout, yang memperbarui layar setiap kali dimuat ulang, antrean selalu penuh kecuali saat game pertama kali dimulai (atau dijalankan lagi setelah dijeda), sehingga efeknya tidak terlihat. Game yang sesekali menjeda animasi, lalu kembali ke mode kecepatan optimal sesekali dapat menunjukkan gangguan.

Koreografer

Koreografer memungkinkan Anda menyetel callback yang diaktifkan pada VSYNC berikutnya. Waktu VSYNC yang sebenarnya akan diteruskan sebagai argumen. Jadi, meskipun aplikasi tidak langsung aktif, Anda masih memiliki gambaran yang akurat tentang kapan periode pemuatan ulang layar dimulai. Menggunakan nilai ini, bukan waktu saat ini, menghasilkan sumber waktu yang konsisten untuk logika pembaruan status game.

Sayangnya, fakta bahwa Anda mendapatkan callback setelah setiap VSYNC tidak menjamin bahwa callback akan dijalankan tepat waktu atau Anda akan dapat menindaklanjutinya dengan cukup cepat. Aplikasi Anda perlu mendeteksi situasi saat terjadi keterlambatan dan melompati frame secara manual.

Kita dapat melihat contohnya dari aktivitas "Record GL app" di Grafika. Di beberapa perangkat (misalnya Nexus 4 dan Nexus 5), aktivitas akan mulai melompati frame jika Anda hanya duduk dan menonton. Rendering GL sama sekali tidak berat, tetapi terkadang elemen Tampilan digambar ulang, dan penyesuaian ukuran/tata letak dapat memakan waktu yang sangat lama jika perangkat beralih ke mode daya rendah. (Menurut systrace, diperlukan 28 md, bukan 6 md, setelah jam melambat di Android 4.4. Jika Anda menggeserkan jari di layar, layar menganggap Anda sedang berinteraksi dengan aktivitas, sehingga kecepatan jam tetap tinggi dan Anda tidak akan pernah melompati frame.)

Cara perbaikan yang paling mudah adalah dengan melompati frame di callback Koreografer jika waktu saat ini lebih dari N milidetik setelah waktu VSYNC. Idealnya nilai N ditentukan berdasarkan interval VSYNC yang diamati sebelumnya. Misalnya, jika periode muat ulang adalah 16,7 md (60fps), Anda dapat melompati frame jika terlambat lebih dari 15 md.

Jika menonton "Record GL app" berjalan, Anda akan melihat jumlah frame yang dilompati meningkat, dan bahkan melihat kilatan merah pada pergantian frame saat ada frame yang dilompati. Kecuali jika mata Anda sangat bagus, Anda tidak akan melihat animasinya tersendat-sendat. Pada 60fps, aplikasi dapat menurunkan frame sesekali tanpa ada yang menyadarinya selama animasi tersebut terus bergerak dengan kecepatan konstan. Seberapa jauh animasi dapat berjalan tanpa tampak tersendat akan kurang lebih tergantung pada apa yang Anda gambar, karakteristik tampilan, dan seberapa baik orang yang menggunakan aplikasi dalam mendeteksi jank.

Pengelolaan thread

Secara umum, jika merender ke SurfaceView, GLSurfaceView, atau TextureView, Anda ingin melakukan rendering tersebut di thread khusus. Jangan pernah melakukan "pekerjaan berat" atau apa pun yang membutuhkan waktu yang tidak bisa ditentukan pada thread UI. Sebagai gantinya, buat dua thread untuk game: thread game dan thread render. Lihat Meningkatkan performa game untuk informasi selengkapnya.

Perincian dan "Record GL app" menggunakan thread perender khusus, dan aplikasi tersebut juga memperbarui status animasi pada thread tersebut. Ini adalah pendekatan yang wajar selama status game dapat diperbarui dengan cepat.

Game lain memisahkan logika game dan rendering sepenuhnya. Jika Anda memiliki game sederhana yang tidak melakukan apa pun selain memindahkan blok setiap 100 md, Anda mungkin akan memiliki thread khusus yang hanya melakukan hal ini:

run() {
    Thread.sleep(100);
    synchronized (mLock) {
        moveBlock();
    }
}

(Anda mungkin ingin mendasarkan waktu tidur pada jam yang tetap untuk mencegah penyimpangan -- sleep() tidak sepenuhnya konsisten, dan moveBlock() membutuhkan jumlah waktu yang tidak nol -- Anda pasti sudah paham.)

Saat kode gambar aktif, kode hanya mengambil kunci, mendapatkan posisi blok saat ini, melepaskan kunci, dan menggambar. Alih-alih melakukan gerakan yang sangat kecil berdasarkan waktu delta antar-frame, Anda hanya memiliki satu thread yang menggerakkan sesuatu dan thread lain yang menggambar sesuatu di mana pun mereka berada saat penggambaran dimulai.

Untuk scene dengan tingkat kerumitan apa pun, Anda sebaiknya membuat daftar acara mendatang yang diurutkan berdasarkan waktu aktif, dan tidur hingga acara berikutnya selesai, tetapi konsepnya tetap sama.