Framerate mempengaruhi kecepatan objek

9

Saya bereksperimen dengan membuat mesin game dari awal di Jawa, dan saya punya beberapa pertanyaan. Loop game utama saya terlihat seperti ini:

        int FPS = 60;
        while(isRunning){
            /* Current time, before frame update */
            long time = System.currentTimeMillis();
            update();
            draw();
            /* How long each frame should last - time it took for one frame */
            long delay = (1000 / FPS) - (System.currentTimeMillis() - time);
            if(delay > 0){
                try{
                    Thread.sleep(delay);
                }catch(Exception e){};
            }
        }

Seperti yang Anda lihat, saya telah menetapkan framerate pada 60FPS, yang digunakan dalam delayperhitungan. Penundaan memastikan bahwa setiap frame membutuhkan jumlah waktu yang sama sebelum merender berikutnya. Dalam update()fungsi saya yang saya lakukan x++yang meningkatkan nilai horizontal dari objek grafis saya menggambar dengan yang berikut:

bbg.drawOval(x,40,20,20);

Yang membingungkan saya adalah kecepatannya. ketika saya mengatur FPSke 150, lingkaran yang diberikan melintasi kecepatan sangat cepat, sementara mengatur FPSke 30 bergerak melintasi layar dengan setengah kecepatan. Tidakkah framerate hanya memengaruhi "kehalusan" rendering dan bukan kecepatan objek yang dirender? Saya pikir saya kehilangan sebagian besar, saya ingin klarifikasi.

Carpetfizz
sumber
4
Berikut ini adalah artikel bagus tentang loop game: perbaiki tanda waktu Anda
Bupati Kostya
2
Sebagai catatan tambahan, kami biasanya mencoba meletakkan hal-hal yang tidak harus dilakukan setiap loop di luar loop. Dalam kode Anda, 1000 / FPSdivisi Anda dapat dilakukan dan hasilnya ditetapkan ke variabel sebelumwhile(isRunning) loop Anda . Ini membantu menghemat beberapa instruksi CPU untuk melakukan sesuatu lebih dari sekali tanpa guna.
Vaillancourt

Jawaban:

21

Anda memindahkan lingkaran dengan satu piksel per bingkai. Seharusnya tidak mengejutkan bahwa, jika loop rendering Anda berjalan pada 30 FPS, lingkaran Anda akan bergerak 30 pada piksel per detik.

Anda pada dasarnya memiliki tiga cara yang mungkin untuk menangani masalah ini:

  1. Cukup pilih satu frame rate dan pertahankan. Itulah yang dilakukan oleh banyak permainan old-school - mereka berjalan pada tingkat tetap 50 atau 60 FPS, biasanya disinkronkan dengan kecepatan refresh layar, dan hanya merancang logika permainan mereka untuk melakukan semua yang diperlukan dalam interval waktu tetap itu. Jika, karena suatu alasan, itu tidak terjadi, game hanya perlu melompati frame (atau mungkin crash), secara efektif memperlambat gambar dan fisika game menjadi setengah kecepatan.

    Secara khusus, permainan bahwa fitur digunakan seperti deteksi tabrakan hardware sprite cukup banyak harus bekerja seperti ini, karena permainan logika mereka terkait erat dengan rendering, yang dilakukan di hardware pada tingkat bunga tetap.

  2. Gunakan cap waktu variabel untuk fisika game Anda. Pada dasarnya, ini berarti menulis ulang lingkaran permainan Anda agar terlihat seperti ini:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        float timestep = 0.001 * (time - lastTime);  // in seconds
        if (timestep <= 0 || timestep > 1.0) {
            timestep = 0.001;  // avoid absurd time steps
        }
        update(timestep);
        draw();
        // ... sleep until next frame ...
        lastTime = time;
    }

    dan, di dalam update(), menyesuaikan rumus fisika untuk memperhitungkan timestep variabel, misalnya seperti ini:

    speed += timestep * acceleration;
    position += timestep * (speed - 0.5 * timestep * acceleration);

    Satu masalah dengan metode ini adalah sulit untuk menjaga fisika (kebanyakan) tidak bergantung pada timestep ; Anda benar-benar tidak ingin pemain jarak bisa melompat bergantung pada frame rate mereka. Rumus yang saya perlihatkan di atas berfungsi dengan baik untuk akselerasi konstan, mis. Di bawah gravitasi (dan yang ada di pos tertaut cukup baik meskipun akselerasi bervariasi dari waktu ke waktu), tetapi bahkan dengan rumus fisika yang paling sempurna, bekerja dengan mengapung kemungkinan besar akan menghasilkan sedikit "noise numerik" yang, khususnya, dapat membuat replay yang tepat menjadi tidak mungkin. Jika itu adalah sesuatu yang Anda pikir Anda inginkan, Anda mungkin ingin memilih metode lain.

  3. Pisahkan pembaruan dan gambar langkah-langkah. Di sini, idenya adalah Anda memperbarui status permainan menggunakan stempel waktu tetap, tetapi jalankan berbagai pembaruan di antara setiap frame. Artinya, lingkaran game Anda mungkin terlihat seperti ini:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        if (time - lastTime > 1000) {
            lastTime = time;  // we're too far behind, catch up
        }
        int updatesNeeded = (time - lastTime) / updateInterval;
        for (int i = 0; i < updatesNeeded; i++) {
            update();
            lastTime += updateInterval;
        }
        draw();
        // ... sleep until next frame ...
    }

    Untuk membuat gerakan yang dirasakan lebih lancar, Anda mungkin juga ingin agar draw()metode Anda menginterpolasi hal-hal seperti posisi objek dengan lancar antara kondisi permainan sebelumnya dan berikutnya. Ini berarti Anda harus melewati offset interpolasi yang benar ke draw()metode, misalnya seperti ini:

        int remainder = (time - lastTime) % updateInterval;
        draw( (float)remainder / updateInterval );  // scale to 0.0 - 1.0

    Anda juga perlu memiliki update()metode yang benar-benar menghitung keadaan permainan selangkah lebih maju (atau mungkin beberapa, jika Anda ingin melakukan interpolasi spline tingkat tinggi), dan menyimpannya pada posisi objek sebelumnya sebelum memperbaruinya, sehingga draw()metode ini dapat menginterpolasi diantara mereka. (Mungkin juga untuk meramalkan posisi yang diprediksi berdasarkan kecepatan dan percepatan objek, tetapi ini bisa terlihat tersentak terutama jika objek bergerak dengan cara yang rumit, menyebabkan prediksi sering gagal.)

    Salah satu keuntungan dari interpolasi adalah bahwa, untuk beberapa jenis gim, ini dapat memungkinkan Anda untuk mengurangi laju pembaruan logika gim secara signifikan, sambil tetap mempertahankan ilusi gerak yang halus. Misalnya, Anda mungkin dapat memperbarui status permainan Anda saja, katakanlah, 5 kali per detik, sementara masih menggambar 30 hingga 60 frame interpolasi per detik. Dalam melakukan hal ini, Anda mungkin juga ingin mempertimbangkan interleaving logika game Anda dengan gambar (yaitu memiliki parameter untuk update()metode Anda yang memberitahukannya untuk menjalankan x % dari pembaruan penuh sebelum kembali), dan / atau menjalankan permainan fisika / logika dan kode rendering di utas terpisah (waspadalah terhadap gangguan sinkronisasi!).

Tentu saja, mungkin juga untuk menggabungkan metode-metode ini dengan berbagai cara. Misalnya, dalam permainan multipemain client-server, Anda mungkin memiliki server (yang tidak perlu menggambar apa pun) menjalankan pembaruannya pada stempel waktu tetap (untuk fisika yang konsisten dan replayability yang tepat), sambil meminta klien melakukan pembaruan prediktif (untuk ditimpa oleh server, jika ada ketidaksepakatan) pada timestep variabel untuk kinerja yang lebih baik. Dimungkinkan juga untuk menggabungkan pembaruan interpolasi dan variabel-timestep yang bermanfaat; misalnya, dalam skenario klien-server yang baru saja dijelaskan, benar-benar tidak ada gunanya meminta klien menggunakan timestep pembaruan yang lebih pendek daripada server, sehingga Anda dapat menetapkan batas yang lebih rendah pada timestep klien, dan melakukan interpolasi pada tahap menggambar untuk memungkinkan lebih tinggi FPS.

(Sunting: Menambahkan kode untuk menghindari interval / hitungan pembaruan yang absurd, dalam kasus, katakanlah, komputer untuk sementara ditangguhkan atau dibekukan selama lebih dari satu detik saat loop permainan sedang berjalan. Terima kasih kepada Mooing Duck karena mengingatkan saya tentang perlunya untuk itu) .)

Ilmari Karonen
sumber
1
Terima kasih banyak telah meluangkan waktu untuk menjawab pertanyaan saya, saya sangat menghargainya. Saya sangat suka pendekatan # 3, itu paling masuk akal bagi saya. Dua pertanyaan, apa definisi pembaruanInterval, dan mengapa Anda membaginya?
Carpetfizz
1
@Carpetfizz: updateIntervalhanyalah jumlah milidetik yang Anda inginkan di antara pembaruan kondisi permainan. Untuk, katakanlah, 10 pembaruan per detik, Anda akan atur updateInterval = (1000 / 10) = 100.
Ilmari Karonen
1
currentTimeMillisbukan jam monoton. Gunakan nanoTimesebaliknya, kecuali jika Anda ingin sinkronisasi waktu jaringan untuk mengacaukan dengan kecepatan hal-hal dalam game Anda
user253751
@ MoooDuck: Terlihat dengan baik. Saya sudah memperbaikinya sekarang, saya pikir. Terima kasih!
Ilmari Karonen
@IlmariKaronen: Sebenarnya, melihat kode, mungkin lebih mudah untuk melakukannya while(lastTime+=updateInterval <= time). Itu hanya sebuah pemikiran, bukan koreksi.
Mooing Duck
7

Kode Anda saat ini berjalan setiap kali sebuah frame merender. Jika frame rate lebih tinggi atau lebih rendah dari frame rate yang Anda tentukan, hasil Anda akan berubah karena pembaruan tidak memiliki waktu yang sama.

Untuk mengatasi ini, Anda harus merujuk ke Delta Timing .

Tujuan dari Delta Timing adalah untuk menghilangkan efek lag pada komputer yang mencoba menangani grafik yang kompleks atau banyak kode, dengan menambahkan hingga kecepatan objek sehingga mereka pada akhirnya akan bergerak pada kecepatan yang sama, terlepas dari lag.

Untuk melakukan ini:

Hal ini dilakukan dengan memanggil timer setiap frame per detik yang menahan waktu antara panggilan sekarang dan terakhir dalam milidetik.

Anda perlu mengalikan waktu delta dengan nilai yang ingin Anda ubah berdasarkan waktu. Sebagai contoh:

distanceTravelledSinceLastFrame = Speed * DeltaTime
Statis
sumber
3
Juga, letakkan batas pada batas minimum dan maksimum. Jika komputer hibernasi kemudian dilanjutkan, Anda tidak ingin hal-hal diluncurkan dari layar. Jika keajaiban muncul dan time()mengembalikan yang sama dua kali, Anda tidak ingin kesalahan div / 0 dan pemrosesan yang sia-sia.
Mooing Duck
@ MoingDuck: Itu poin yang sangat bagus. Saya telah mengedit jawaban saya sendiri untuk mencerminkannya. (Biasanya, Anda tidak boleh membagi apa pun dengan timestep dalam pembaruan kondisi permainan tipikal, jadi nol timestep harus aman, tetapi membiarkannya menambahkan sumber kesalahan potensial tambahan untuk sedikit atau tanpa keuntungan, dan karenanya harus dihindari.)
Ilmari Karonen
5

Itu karena Anda membatasi frame rate Anda, tetapi Anda hanya melakukan satu pembaruan per frame. Jadi mari kita asumsikan game berjalan pada target 60 fps, Anda mendapatkan 60 pembaruan logika per detik. Jika frame rate turun menjadi 15 fps, Anda hanya akan memiliki 15 pembaruan logika per detik.

Alih-alih, coba akumulasi waktu bingkai yang telah dilewati sejauh ini dan kemudian perbarui logika permainan Anda sekali untuk setiap rentang waktu yang diberikan, misalnya untuk menjalankan logika Anda pada 100 fps, Anda akan menjalankan pembaruan satu kali untuk setiap 10 ms yang diakumulasikan (dan kurangi yang dari melawan).

Tambahkan alternatif (lebih baik untuk visual) perbarui logika Anda berdasarkan waktu yang berlalu.

Mario
sumber
1
yaitu pembaruan (elapsedSeconds);
Jon
2
Dan di dalam, posisi + = kecepatan * elapsedSeconds;
Jon