Bayangkan masalahnya: Anda memiliki gim, dan Anda membutuhkannya untuk bekerja pada 60 fps pada monitor 60 Hz. Komputer Anda cukup cepat untuk rendering dan pembaruan untuk mengambil jumlah waktu yang tidak signifikan, sehingga Anda menyalakan vsync dan menulis loop game ini:
while(running) { update(); render(); display(); }
Sangat mudah! Sekarang gim ini bekerja pada 60fps dan semuanya berjalan seperti jarum jam. Selesai Terima kasih telah membaca posting ini.
Yah, jelas, semuanya tidak begitu baik. Bagaimana jika seseorang memiliki komputer yang lemah yang tidak dapat membuat game dengan kecepatan yang cukup untuk menyediakan 60fps? Bagaimana jika seseorang membeli salah satu dari 144 monitor hertz baru yang keren itu? Bagaimana jika dia mematikan vsync di pengaturan driver?
Anda mungkin berpikir: Saya perlu mengukur waktu di suatu tempat dan memberikan pembaruan dengan frekuensi yang benar. Ini cukup sederhana - cukup kumpulkan waktu di setiap siklus dan perbarui setiap kali melebihi ambang batas dengan 1/60 detik.
while(running) { deltaTime = CurrentTime()-OldTime; oldTime = CurrentTime(); accumulator += deltaTime; while(accumulator > 1.0/60.0){ update(); accumulator -= 1.0/60.0; } render(); display(); }
Selesai, tidak ada tempat yang lebih mudah. Bahkan, ada banyak permainan di mana kode dasarnya terlihat seperti itu. Tapi ini salah. Ini cocok untuk mengatur timing, tetapi menyebabkan masalah dengan menyentak (gagap) dan ketidakcocokan lainnya. Masalah seperti itu sangat umum: frame tidak ditampilkan tepat 1/60 detik; bahkan ketika vsync dihidupkan, selalu ada sedikit noise pada saat mereka ditampilkan (dan dalam keakuratan pengatur waktu OS). Oleh karena itu, akan ada situasi ketika Anda membuat bingkai, dan permainan percaya bahwa waktu untuk memperbarui kembali belum tiba (karena baterai tertinggal sebagian kecil), jadi itu hanya mengulangi frame yang sama lagi, tetapi sekarang permainan sudah terlambat untuk bingkai, sehingga gandakan pembaruan. Ini berkedut!
Googling, Anda dapat menemukan beberapa solusi siap pakai untuk menghilangkan kedutan ini. Misalnya, gim dapat menggunakan variabel daripada langkah waktu yang konstan, dan cukup meninggalkan baterai dalam kode pewaktuan. Atau Anda dapat menerapkan langkah waktu konstan dengan penyaji interpolasi, dijelaskan dalam artikel yang agak terkenal "
Perbaiki Waktu Anda " oleh Glenn Fielder. Atau Anda dapat membuat kembali kode pengatur waktu sehingga sedikit lebih fleksibel, seperti yang dijelaskan dalam posting
Frame Timing Issues Slick Entertainment (sayangnya blog ini sudah tidak ada lagi).
Pengaturan waktu yang kabur
Metode Slick Entertainment dengan "timing fuzzy" di mesin saya adalah yang paling mudah diterapkan, karena tidak memerlukan perubahan dalam logika dan rendering game. Jadi pada akhirnya aku sudah menggunakannya. Sudah cukup hanya dengan memasukkannya ke mesin. Bahkan, itu hanya memungkinkan game untuk diperbarui "sedikit lebih awal" untuk menghindari masalah dengan ketidakcocokan waktu. Jika game menyertakan vsync, maka itu hanya memungkinkan Anda untuk menggunakan vsync sebagai timer utama gim, dan memberikan gambar yang halus.
Beginilah tampilan kode pembaruan sekarang (permainan “dapat bekerja” pada 62 fps, tetapi masih memproses setiap langkah waktu seolah-olah bekerja pada 60fps. Saya tidak mengerti mengapa harus membatasi sehingga nilai baterai tidak turun di bawah 0, tetapi tanpa kode ini tidak berfungsi). Anda dapat menafsirkannya dengan cara ini: "permainan diperbarui dengan langkah tetap, jika diberikan dalam interval dari 60fps hingga 62fps":
while(accumulator > 1.0/62.0){ update(); accumulator -= 1.0/60.0; if(accumulator < 0) accumulator = 0; }
Jika vsync diaktifkan, itu pada dasarnya memungkinkan game untuk bekerja dengan nada tetap, yang cocok dengan kecepatan refresh monitor, dan memberikan gambar yang halus. Masalah utama di sini adalah ketika vsync dinonaktifkan, gim akan bekerja
sedikit lebih cepat, tetapi perbedaannya sangat kecil sehingga tidak ada yang akan memperhatikannya.
Pelari kecepatan. Speedrunners akan melihat. Tak lama setelah permainan dirilis, mereka memperhatikan bahwa beberapa orang di daftar speedscore speedran memiliki waktu tempuh yang lebih buruk, tetapi ternyata lebih baik daripada yang lain. Dan alasan langsung untuk ini adalah timing yang tidak jelas dan terputusnya vsync dalam game (atau 144 Hz monitor). Oleh karena itu, menjadi jelas bahwa Anda perlu mematikan kekaburan ini saat melepas vsync.
Oh, tapi kami masih tidak dapat memeriksa apakah vsync dinonaktifkan. Tidak ada panggilan untuk ini di OS, dan meskipun kami dapat meminta dari aplikasi untuk mengaktifkan atau menonaktifkan vsync, sebenarnya itu sepenuhnya tergantung pada OS dan driver grafis. Satu-satunya hal yang dapat dilakukan adalah merender banyak frame, mencoba mengukur waktu pelaksanaan tugas ini, dan kemudian membandingkan apakah mereka membutuhkan waktu yang sama. Itulah tepatnya yang saya lakukan untuk
The End . Jika gim ini tidak menyertakan vsync dengan frekuensi 60 Hz, maka gulir kembali ke timer frame asli dengan "fps ketat 60". Selain itu, saya menambahkan parameter ke file konfigurasi yang memaksa game untuk tidak menggunakan ketidakjelasan (terutama untuk pelari kecepatan yang membutuhkan waktu yang akurat) dan menambahkan pengatur waktu dalam permainan yang tepat untuk mereka, yang memungkinkan menggunakan autosplitter (ini adalah skrip yang bekerja dengan timer waktu atom).
Beberapa pengguna masih mengeluh tentang sesekali menyentak frame individu, tetapi mereka tampak sangat langka sehingga mereka dapat dijelaskan oleh peristiwa OS atau alasan eksternal lainnya. Bukan masalah besar. Benar?
Melihat-lihat kode timer saya baru-baru ini, saya melihat sesuatu yang aneh. Baterai diganti, setiap frame membutuhkan waktu sedikit lebih lama dari 1/60 detik, jadi dari waktu ke waktu permainan berpikir sudah terlambat untuk frame, dan melakukan pembaruan ganda. Ternyata monitor saya bekerja dengan frekuensi 59,94 Hz, dan bukan 60 Hz. Ini berarti bahwa setiap 1000 frame ia harus melakukan pembaruan ganda untuk "mengejar ketinggalan". Namun, ini sangat mudah untuk diperbaiki - cukup ubah interval frekuensi bingkai yang diizinkan (bukan dari 60 menjadi 62, tetapi dari 59 menjadi 61).
while(accumulator > 1.0/61.0){ update(); accumulator -= 1.0/59.0; if(accumulator < 0) accumulator = 0; }
Masalah yang dijelaskan di atas dengan monitor vsync terputus dan frekuensi tinggi masih berlanjut, dan solusi yang sama berlaku untuk itu (kembalikan ke timer ketat jika monitor
tidak disinkronkan vsync oleh 60).
Tetapi bagaimana Anda tahu jika ini adalah solusi yang tepat? Bagaimana memastikan bahwa itu akan bekerja dengan benar pada semua kombinasi komputer dengan berbagai jenis monitor, dengan dan tanpa vsync dihidupkan, dan seterusnya? Sangat sulit untuk melacak semua masalah timer ini di kepala, dan untuk memahami apa yang menyebabkan desync, loop aneh dan sejenisnya.
Monitor simulator
Mencoba menemukan solusi yang dapat diandalkan untuk "masalah monitor hertz 59,94", saya menyadari bahwa saya tidak bisa hanya melakukan pemeriksaan coba-coba, berharap menemukan solusi yang dapat diandalkan. Saya membutuhkan cara yang mudah untuk menguji berbagai upaya untuk menulis timer berkualitas tinggi dan cara mudah untuk memeriksa apakah itu menyebabkan sentakan atau pergeseran waktu dalam konfigurasi monitor yang berbeda.
Monitor Simulator muncul di tempat kejadian. Ini adalah kode "kotor dan cepat" yang saya tulis, mensimulasikan "operasi monitor", dan pada dasarnya menunjukkan kepada saya banyak angka yang memberikan gambaran stabilitas setiap timer yang diuji.
Misalnya, untuk penghitung waktu paling sederhana, nilai-nilai berikut ini ditampilkan dari awal artikel:
20211012021011202111020211102012012102012[...]
TOTAL UPDATES: 10001
TOTAL VSYNCS: 10002
TOTAL DOUBLE UPDATES: 2535
TOTAL SKIPPED RENDERS: 0
GAME TIME: 166.683
SYSTEM TIME: 166.7
Pertama, kode menampilkan untuk setiap vsync yang ditiru jumlah jumlah "pembaruan" ke siklus permainan setelah vsync sebelumnya. Nilai apa pun selain solid 1 mengarah ke gambar gugup. Pada akhirnya, kode menampilkan statistik akumulasi.
Saat menggunakan "fuzzy timer" (dengan interval 60-62fps) pada monitor 59,94-Hertz, kode ini menampilkan yang berikut:
111111111111111111111111111111111111111111111[...]
TOTAL UPDATES: 10000
TOTAL VSYNCS: 9991
TOTAL DOUBLE UPDATES: 10
TOTAL SKIPPED RENDERS: 0
GAME TIME: 166.667
SYSTEM TIME: 166.683
Menyentak frame sangat jarang, sehingga bisa sulit untuk melihat dengan sejumlah 1. Tapi statistik yang ditampilkan jelas menunjukkan bahwa permainan telah melakukan beberapa pembaruan ganda di sini, yang mengarah pada menyentak. Dalam versi tetap (dengan interval 59-61 fps), ada 0 pembaruan dilewati atau dua kali lipat.
Anda juga dapat menonaktifkan vsync. Sisa data statistik menjadi tidak penting, tetapi ini dengan jelas menunjukkan kepada saya besarnya "pergeseran waktu" (waktu sistem bergeser relatif ke tempat waktu permainan seharusnya).
GAME TIME: 166.667
SYSTEM TIME: 169.102
Itulah sebabnya ketika vsync dinonaktifkan, Anda harus beralih ke penghitung waktu yang ketat, jika tidak, perbedaan ini menumpuk dari waktu ke waktu.
Jika saya mengatur waktu rendering ke 0,02 (yaitu, "lebih dari satu frame" diperlukan untuk rendering), maka saya akan mendapatkan kedutan. Idealnya, pola permainan harus seperti 202020202020, tetapi agak tidak merata.
Dalam situasi ini, timer ini berperilaku sedikit lebih baik dari yang sebelumnya, tetapi menjadi lebih membingungkan dan lebih sulit untuk mengetahui bagaimana dan mengapa ia bekerja. Tapi saya hanya bisa memasukkan tes ke dalam simulator ini dan memeriksa bagaimana mereka berperilaku, dan Anda bisa mengetahui alasannya nanti. Trial and error, sayang!
while(accumulator >= 1.0/61.0){ simulate_update(); accumulator -= 1.0/60.0; if(accumulator < 1.0/59.0–1.0/60.0) accumulator = 0; }
Anda dapat mengunduh
simulator monitor dan secara independen memeriksa berbagai metode penghitungan waktu.
Email saya jika Anda menemukan sesuatu yang lebih baik.
Saya tidak 100% puas dengan keputusan saya (masih membutuhkan peretasan dengan "vsync recognition" dan sesekali menyentak dapat terjadi selama desinkronisasi), tetapi saya percaya bahwa itu hampir sama baiknya dengan upaya untuk mengimplementasikan siklus permainan dengan langkah yang tetap. Sebagian dari masalah ini muncul karena sangat sulit untuk menentukan parameter dari apa yang dianggap “dapat diterima” di sini. Kesulitan utama terletak pada tradeoff antara time shift dan double / skipping frames. Jika Anda menjalankan game 60 Hz pada monitor 50 Hz PAL ... lalu apa keputusan yang tepat? Apakah Anda ingin menyentak liar, atau bermain game terasa lebih lambat? Kedua opsi itu tampak buruk.
Render terpisah
Dalam metode sebelumnya, saya menggambarkan apa yang saya sebut "rendststep rendering". Gim memperbarui statusnya, lalu merender, dan saat merender, gim ini selalu menampilkan kondisi gim terbaru. Rendering dan pembaruan terhubung bersama.
Tetapi Anda dapat memisahkan mereka. Inilah yang metode yang dijelaskan dalam pos "
Perbaiki Your Timestep "
tidak . Saya tidak akan mengulangi lagi, Anda pasti harus membaca posting ini. Ini (seperti yang saya mengerti) adalah "standar industri" yang digunakan dalam game AAA dan mesin seperti Unity dan Unreal (namun, dalam game 2D aktif yang intens, mereka biasanya lebih suka menggunakan langkah tetap (lockstep), karena terkadang akurasi yang memberi Anda metode ini).
Tetapi jika kita menjelaskan secara singkat posting Glenn, itu hanya menjelaskan metode pembaruan dengan frame rate yang tetap, tetapi ketika rendering, interpolasi dilakukan antara keadaan "saat ini" dan "sebelumnya" dari permainan, dan nilai baterai saat ini digunakan sebagai nilai interpolasi. Dengan metode ini, Anda dapat merender dalam frame rate apa pun dan memperbarui game pada frekuensi berapa pun, dan gambar akan selalu mulus. Tidak menyentak, bekerja secara universal.
while(running){ computeDeltaTimeSomehow(); accumulator += deltaTime; while(accumulator >= 1.0/60.0){ previous_state = current_state; current_state = update(); accumulator -= 1.0/60.0; } render_interpolated_somehow(previous_state, current_state, accumulator/(1.0/60.0)); display(); }
Jadi, dasar. Masalahnya teratasi.
Sekarang Anda hanya perlu memastikan bahwa permainan dapat membuat status yang diinterpolasi ... tapi tunggu sebentar, itu benar-benar tidak mudah sama sekali. Dalam posting Glenn, hanya diasumsikan bahwa ini bisa dilakukan. Cukup mudah untuk men-cache posisi sebelumnya dari objek game dan melakukan interpolasi gerakannya, tetapi status game jauh lebih dari itu. Perlu untuk mempertimbangkan di dalamnya keadaan animasi, penciptaan dan penghancuran objek, dan banyak hal.
Plus, dalam logika permainan, Anda perlu mempertimbangkan apakah objek tersebut diteleportasi atau jika perlu dipindahkan dengan lancar sehingga interpolator tidak membuat asumsi yang salah tentang jalur yang dibuat oleh objek game ke posisi saat ini. Kekacauan nyata dapat terjadi dengan belokan, terutama jika dalam satu bingkai pergantian objek dapat berubah lebih dari 180 derajat. Dan bagaimana cara memproses objek yang dibuat dan dihancurkan dengan benar?
Saat ini, saya hanya mengerjakan tugas ini di mesin saya. Faktanya, saya hanya menginterpolasi gerakannya, dan membiarkan yang lainnya apa adanya. Anda tidak akan melihat menyentak jika objek tidak bergerak dengan lancar, sehingga melewatkan bingkai animasi dan menyinkronkan pembuatan / penghancuran objek hingga satu bingkai tidak akan menjadi masalah jika semuanya dilakukan dengan lancar.
Namun, aneh bahwa, pada kenyataannya, metode ini membuat permainan dalam keadaan yang terlambat oleh 1 keadaan permainan dari tempat simulasi sekarang berada. Ini tidak mencolok, tetapi dapat dihubungkan ke sumber keterlambatan lain, misalnya, keterlambatan input dan monitor kecepatan refresh, sehingga mereka yang membutuhkan gameplay paling responsif (saya berbicara tentang Anda, pelari kecepatan) kemungkinan besar akan lebih suka menggunakan penguncian dalam permainan.
Di mesin saya, saya hanya memberi pilihan. Jika Anda memiliki monitor 60 hertz dan komputer yang cepat, yang terbaik adalah menggunakan lockstep dengan mengaktifkan vsync. Jika monitor memiliki kecepatan refresh yang tidak standar, atau komputer Anda yang lemah tidak dapat membuat 60 frame per detik secara konstan, maka aktifkan interpolasi frame. Saya ingin menyebut opsi ini "membuka framerate", tetapi orang mungkin berpikir bahwa itu berarti "aktifkan opsi ini jika Anda memiliki komputer yang bagus." Namun, masalah ini bisa diselesaikan nanti.
Sebenarnya, ada metode untuk mengatasi masalah ini.
Pembaruan langkah waktu variabel
Banyak orang bertanya kepada saya mengapa tidak hanya memperbarui game dengan langkah waktu variabel, dan programmer teoritis sering mengatakan: "jika game ditulis dengan BENAR, maka Anda dapat memperbaruinya dengan langkah waktu sewenang-wenang".
while(running) { deltaTime = CurrentTime()-OldTime; oldTime = CurrentTime(); update(deltaTime); render(); display(); }
Tidak ada keanehan dengan pengaturan waktu. Tidak ada rendering interpolasi yang aneh. Semuanya sederhana, semuanya berfungsi.
Jadi, dasar. Masalahnya teratasi. Dan sekarang selamanya! Tidak mungkin mencapai hasil yang lebih baik!
Sekarang cukup sederhana untuk membuat logika game berfungsi dengan langkah waktu sewenang-wenang. Sederhana, ganti saja semua kode ini:
position += speed;
tentang ini:
position += speed * deltaTime;
dan ganti kode berikut:
speed += acceleration; position += speed;
tentang ini:
speed += acceleration * deltaTime; position += speed * deltaTime;
dan ganti kode berikut:
speed += acceleration; speed *= friction; position += speed;
tentang ini:
Vec3D p0 = position; Vec3D v0 = velocity; Vec3D a = acceleration*(1.0/60.0); double f = friction; double n = dt*60; double fN = pow(friction, n); position = p0 + ((f*(a*(f*fN-f*(n+1)+n)+(f-1)*v0*(fN-1)))/((f-1)*(f-1)))*(1.0/60.0); velocity = v0*fN+a*(f*(fN-1)/(f-1));
... jadi, tunggu
Dari mana semua ini berasal?
Bagian terakhir secara harfiah disalin dari kode bantu mesin saya, yang melakukan "gerakan frame rate-independent yang benar, dengan kecepatan membatasi gesekan". Ada sedikit sampah di dalamnya (perkalian dan pembagian ini dengan 60). Tetapi ini adalah versi kode yang “benar” dengan langkah waktu variabel untuk fragmen sebelumnya. Saya menemukan jawabannya selama lebih dari satu jam dengan
Wolfram Alpha .
Sekarang mereka mungkin bertanya kepada saya mengapa tidak melakukannya seperti ini:
speed += acceleration * deltaTime; speed *= pow(friction, deltaTime); position += speed * deltaTime;
Dan meskipun tampaknya berhasil, sebenarnya itu salah. Anda bisa memeriksanya sendiri. Lakukan dua pembaruan dengan deltaTime = 1, dan kemudian lakukan satu pembaruan dengan deltaTime = 2, dan hasilnya akan berbeda. Biasanya kami berusaha agar gim bekerja dalam konser, sehingga perbedaan seperti itu tidak diterima. Ini mungkin solusi yang cukup baik, jika Anda tahu pasti bahwa deltaTime selalu kira-kira sama dengan satu nilai, tetapi kemudian Anda perlu menulis kode untuk memastikan bahwa pembaruan dilakukan pada frekuensi yang konstan dan ... ya. Itu benar, sekarang kami mencoba melakukan segalanya "DENGAN BENAR."
Jika sepotong kecil kode terungkap menjadi perhitungan matematika yang mengerikan, maka bayangkan pola gerakan yang lebih kompleks di mana banyak objek yang berinteraksi berpartisipasi, dan sejenisnya. Sekarang Anda dapat dengan jelas melihat bahwa solusi "benar" tidak dapat direalisasikan. Maksimum yang bisa kita capai adalah "perkiraan kasar". Mari kita lupakan untuk saat ini, dan misalkan kita benar-benar memiliki versi fungsi gerak yang “benar-benar benar”. Bagus kan?
Tidak, sebenarnya. Ini adalah contoh nyata dari masalah yang saya alami dengan ini di
Bombernauts . Seorang pemain dapat bangkit sekitar 1 ubin, dan permainan berlangsung dalam kotak blok dalam 1 ubin. Untuk mendarat di blok, kaki karakter harus naik di atas permukaan atas blok.
Tetapi karena pengenalan tabrakan di sini dilakukan dengan langkah diskrit, maka jika permainan bekerja dengan frame rate yang rendah, kadang-kadang kaki tidak akan mencapai permukaan ubin, meskipun mereka mengikuti kurva gerakan yang sama, dan bukannya mengangkat pemain akan meluncur turun dari dinding.
Jelas, masalah ini bisa dipecahkan. Tapi itu menggambarkan jenis masalah yang kita temui ketika mencoba untuk mengimplementasikan pekerjaan siklus permainan dengan benar dengan langkah waktu variabel. Kami kehilangan koherensi dan determinisme, jadi kami harus menyingkirkan fungsi replay game dengan merekam input pemain, multipemain deterministik, dan sejenisnya. Untuk game 2D cepat berbasis refleks, konsistensi sangat penting (dan sapa lagi untuk pelari cepat).
Jika Anda mencoba menyesuaikan langkah waktu sehingga tidak terlalu besar atau terlalu kecil, maka Anda akan kehilangan keuntungan utama yang diperoleh dari langkah waktu variabel, dan Anda dapat dengan aman menggunakan dua metode lain yang dijelaskan di sini. Permainan ini tidak sebanding dengan lilin. Terlalu banyak upaya ekstra akan dimasukkan ke dalam logika permainan (implementasi matematika gerak yang benar), dan terlalu banyak korban akan diperlukan di bidang determinisme dan konsistensi. Saya akan menggunakan metode ini hanya untuk permainan ritme musik (di mana persamaan geraknya sederhana dan membutuhkan responsif dan kehalusan maksimum). Dalam semua kasus lain, saya akan memilih pembaruan tetap.
Kesimpulan
Sekarang Anda tahu cara membuat gim bekerja pada frekuensi konstan 60fps. Ini sangat sederhana, dan tidak ada orang lain yang memiliki masalah dengannya. Tidak
ada masalah lain yang menyulitkan tugas ini.