Il framerate sta influenzando la velocità dell'oggetto


9

Sto sperimentando la costruzione di un motore di gioco da zero in Java e ho un paio di domande. Il mio ciclo di gioco principale è simile al seguente:

        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){};
            }
        }

Come puoi vedere, ho impostato il framerate a 60FPS, che viene utilizzato nel delaycalcolo. Il ritardo assicura che ogni fotogramma impieghi lo stesso tempo prima di eseguire il rendering del successivo. Nella mia update()funzione faccio ciò x++che aumenta il valore orizzontale di un oggetto grafico che disegno con il seguente:

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

Ciò che mi confonde è la velocità. quando ho impostato FPSsu 150, il cerchio renderizzato attraversa la velocità molto velocemente, mentre l'impostazione FPSsu 30 si sposta sullo schermo a metà della velocità. Il framerate non influisce solo sulla "levigatezza" del rendering e non sulla velocità di rendering degli oggetti? Penso che mi manchi una grande parte, mi piacerebbe qualche chiarimento.


4
Ecco un buon articolo sul loop di gioco: correggi il tuo timestep
Kostya Regent,

2
Come nota a margine, generalmente cerchiamo di mettere cose che non devono essere eseguite in ogni loop al di fuori dei loop. Nel tuo codice, la 1000 / FPSdivisione potrebbe essere eseguita e il risultato assegnato a una variabile prima del while(isRunning)ciclo. Questo aiuta a salvare un paio di istruzioni della CPU per fare qualcosa più di una volta inutilmente.
Vaillancourt

Risposte:


21

Stai spostando il cerchio di un pixel per fotogramma. Non dovrebbe sorprendere che, se il tuo ciclo di rendering viene eseguito a 30 FPS, il tuo cerchio si sposterà di 30 a pixel al secondo.

Fondamentalmente hai tre modi possibili per affrontare questo problema:

  1. Basta selezionare un frame rate e attenersi ad esso. È quello che facevano molti giochi della vecchia scuola: giravano a una velocità fissa di 50 o 60 FPS, di solito sincronizzati con la frequenza di aggiornamento dello schermo e progettavano semplicemente la loro logica di gioco per fare tutto il necessario entro quell'intervallo di tempo fisso. Se, per qualche motivo, ciò non accadesse, il gioco dovrebbe semplicemente saltare un frame (o possibilmente schiantarsi), rallentando efficacemente sia il disegno che la fisica del gioco a metà velocità.

    In particolare, i giochi che utilizzavano funzionalità come il rilevamento delle collisioni degli sprite hardware dovevano praticamente funzionare in questo modo, poiché la loro logica di gioco era indissolubilmente legata al rendering, che veniva eseguito in hardware a velocità fissa.

  2. Utilizzare un timestep variabile per la fisica del gioco. Fondamentalmente, questo significa riscrivere il tuo ciclo di gioco per assomigliare a questo:

    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;
    }

    e, all'interno update(), adeguando le formule di fisica per tenere conto del timestep variabile, ad esempio in questo modo:

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

    Un problema con questo metodo è che può essere difficile mantenere la fisica (principalmente) indipendente dal timestep ; non vuoi davvero che la distanza che i giocatori possano saltare dipenda dal loro frame rate. La formula che ho mostrato sopra funziona bene per un'accelerazione costante, ad esempio sotto gravità (e quella nel post collegato fa abbastanza bene anche se l'accelerazione varia nel tempo), ma anche con le formule fisiche più perfette possibili, lavorare con i galleggianti è probabile che produce un po 'di "rumore numerico" che, in particolare, può rendere impossibile la riproduzione esatta. Se è qualcosa che pensi di voler, potresti preferire gli altri metodi.

  3. Disaccoppia l'aggiornamento e disegna i passaggi. Qui, l'idea è di aggiornare lo stato del gioco usando un timestep fisso, ma eseguire un numero variabile di aggiornamenti tra ciascun frame. Cioè, il tuo loop di gioco potrebbe assomigliare a questo:

    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 ...
    }

    Per rendere più fluido il movimento percepito, potresti anche desiderare che il tuo draw()metodo interpoli le cose come le posizioni degli oggetti senza intoppi tra lo stato di gioco precedente e quello successivo. Ciò significa che è necessario passare l'offset di interpolazione corretto al draw()metodo, ad esempio in questo modo:

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

    Dovresti anche fare in modo che il tuo update()metodo calcoli effettivamente lo stato del gioco un passo avanti (o forse più, se desideri eseguire l'interpolazione della spline di ordine superiore) e farlo salvare le posizioni degli oggetti precedenti prima di aggiornarle, in modo che il draw()metodo possa interpolare tra loro. (È anche possibile estrapolare le posizioni previste in base alle velocità e alle accelerazioni degli oggetti, ma questo può sembrare a scatti soprattutto se gli oggetti si muovono in modi complicati, causando spesso il fallimento delle previsioni.)

    Un vantaggio dell'interpolazione è che, per alcuni tipi di giochi, può consentire di ridurre in modo significativo la frequenza di aggiornamento della logica di gioco, mantenendo comunque l'illusione di un movimento fluido. Ad esempio, potresti essere in grado di aggiornare il tuo stato di gioco solo, diciamo, 5 volte al secondo, pur disegnando da 30 a 60 fotogrammi interpolati al secondo. Nel fare ciò, potresti anche prendere in considerazione l'idea di interfogliare la logica del tuo gioco con il disegno (ovvero avere un parametro nel tuo update()metodo che gli dice di eseguire solo x % di un aggiornamento completo prima di tornare) e / o eseguire la fisica del gioco / la logica e il codice di rendering in thread separati (attenzione ai problemi di sincronizzazione!).

Naturalmente, è anche possibile combinare questi metodi in vari modi. Ad esempio, in una partita multiplayer client-server, è possibile che il server (che non ha bisogno di disegnare nulla) esegua i suoi aggiornamenti a un intervallo di tempo fisso (per una fisica coerente e rigiocabilità esatta), mentre il client esegue aggiornamenti predittivi (per essere sovrascritto dal server, in caso di disaccordo) a un orario variabile per prestazioni migliori. È anche possibile mescolare utilmente interpolazione e aggiornamenti a tempo variabile; ad esempio, nello scenario client-server appena descritto, non ha davvero molto senso che il client utilizzi timestep di aggiornamento più brevi rispetto al server, quindi è possibile impostare un limite inferiore sul timestep del client e interpolare nella fase di disegno per consentire FPS.

(Modifica: codice aggiunto per evitare assurdi intervalli / conteggi di aggiornamento, nel caso, diciamo, il computer è temporaneamente sospeso o comunque bloccato per più di un secondo mentre il ciclo di gioco è in esecuzione. Grazie a Mooing Duck per avermi ricordato la necessità di questo .)


1
Grazie mille per aver dedicato del tempo a rispondere alla mia domanda, lo apprezzo molto. Mi piace molto l'approccio # 3, ha più senso per me. Due domande, da cosa è definito updateInterval e perché dividi per esso?
Carpetfizz

1
@Carpetfizz: updateIntervalè solo il numero di millisecondi che desideri tra gli aggiornamenti dello stato del gioco. Per, diciamo, 10 aggiornamenti al secondo, avresti impostato updateInterval = (1000 / 10) = 100.
Ilmari Karonen,

1
currentTimeMillisnon è un orologio monotonico. Usa nanoTimeinvece, a meno che tu non voglia sincronizzare il tempo di rete con la velocità delle cose nel tuo gioco.
user253751

@MooingDuck: ben individuato. L'ho risolto ora, penso. Grazie!
Ilmari Karonen,

@IlmariKaronen: In realtà, guardando il codice, potrebbe essere più semplice solo per while(lastTime+=updateInterval <= time). Questo è solo un pensiero, non una correzione.
Mooing Duck,

7

Il codice è attualmente in esecuzione ogni volta che viene eseguito il rendering di un frame. Se la frequenza dei fotogrammi è superiore o inferiore alla frequenza dei fotogrammi specificata, i risultati cambieranno poiché gli aggiornamenti non hanno la stessa tempistica.

Per risolvere questo, è necessario fare riferimento a Delta Timing .

Lo scopo di Delta Timing è quello di eliminare gli effetti del ritardo sui computer che tentano di gestire grafica complessa o molto codice, aggiungendo alla velocità degli oggetti in modo che alla fine si sposteranno alla stessa velocità, indipendentemente dal ritardo.

Per farlo:

Viene fatto chiamando un timer ogni frame al secondo che contiene il tempo tra ora e l'ultima chiamata in millisecondi.

Dovresti quindi moltiplicare il tempo delta per il valore che vuoi cambiare per tempo. Per esempio:

distanceTravelledSinceLastFrame = Speed * DeltaTime

3
Inoltre, imposta i limiti sui tempi minimi e massimi. Se il computer va in letargo quindi riprende, non si desidera che le cose vengano avviate fuori dallo schermo. Se appare un miracolo e time()restituisce lo stesso due volte, non si desidera errori div / 0 ed elaborazione sprecata.
Mooing Duck il

@MooingDuck: è un ottimo punto. Ho modificato la mia risposta per rispecchiarla. (Di solito, non dovresti dividere nulla per il timestep in un tipico aggiornamento dello stato del gioco, quindi un timestep zero dovrebbe essere sicuro, ma consentirlo aggiunge una fonte aggiuntiva di potenziali errori per guadagno scarso o nullo, e quindi dovrebbe essere evitato.)
Ilmari Karonen il

5

Questo perché limiti la frequenza dei fotogrammi, ma esegui solo un aggiornamento per fotogramma. Supponiamo quindi che il gioco funzioni a 60 fps target, ottenendo 60 aggiornamenti logici al secondo. Se la frequenza dei fotogrammi scende a 15 fps, si otterrebbero solo 15 aggiornamenti logici al secondo.

Invece, prova ad accumulare il tempo di frame trascorso finora e quindi aggiorna la tua logica di gioco una volta per ogni periodo di tempo trascorso, ad esempio per eseguire la tua logica a 100 fps, eseguiresti l'aggiornamento una volta ogni 10 ms accumulati (e sottrai quelli dal contatore).

Aggiungi un'alternativa (migliore per gli oggetti visivi) aggiorna la tua logica in base al tempo trascorso.


1
cioè aggiornamento (elapsedSeconds);
Jon

2
E dentro, posizione + = velocità * trascorsi Secondi;
Jon
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.