ESTREMAMENTE confuso sul loop di gioco "Velocità di gioco costante FPS massimo"


12

Di recente ho letto questo articolo su Game Loops: http://www.koonsolo.com/news/dewitters-gameloop/

E l'ultima implementazione consigliata mi sta confondendo profondamente. Non capisco come funzioni e sembra un casino completo.

Comprendo il principio: aggiorna il gioco a una velocità costante, con tutto ciò che rimane renderlo il gioco il più volte possibile.

Presumo che tu non possa usare un:

  • Ottieni input per 25 tick
  • Rendering del gioco per 975 tick

Approccio poiché otterresti input per la prima parte della seconda e questo sembrerebbe strano? O è quello che sta succedendo nell'articolo?


Essenzialmente:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

Com'è valido?

Assumiamo i suoi valori.

MAX_FRAMESKIP = 5

Supponiamo next_game_tick, che è stato assegnato pochi istanti dopo l'inizializzazione, prima che il ciclo di gioco principale sia ... 500.

Infine, poiché sto usando SDL e OpenGL per il mio gioco, con OpenGL usato solo per il rendering, supponiamo che GetTickCount()ritorni il tempo da quando è stato chiamato SDL_Init, cosa che fa.

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

Fonte: http://www.libsdl.org/docs/html/sdlgetticks.html

L'autore assume anche questo:

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

Se espandiamo la whiledichiarazione otteniamo:

while( ( 750 > 500 ) && ( 0 < 5 ) )

750 perché il tempo è passato da quando è next_game_tickstato assegnato. loopsè zero come puoi vedere nell'articolo.

Quindi abbiamo inserito il ciclo while, facciamo un po 'di logica e accettiamo alcuni input.

Yadayadayada.

Alla fine del ciclo while, che ti ricordo che è all'interno del nostro ciclo di gioco principale è:

next_game_tick += SKIP_TICKS;
loops++;

Aggiorniamo l'aspetto della prossima iterazione del codice while

while( ( 1000 > 540 ) && ( 1 < 5 ) )

1000 perché è trascorso il tempo a ottenere input e fare cose prima che raggiungessimo la successiva ineterazione del loop, in cui viene richiamato GetTickCount ().

540 perché, nel codice 1000/25 = 40, quindi, 500 + 40 = 540

1 perché il nostro loop è stato ripetuto una volta

5 , sai perché.


Quindi, dal momento che questo ciclo While è chiaramente Dipendente MAX_FRAMESKIPe non intenzionale, TICKS_PER_SECOND = 25;come dovrebbe funzionare correttamente il gioco?

Non è stata una sorpresa per me che quando l'ho implementato nel mio codice, potrei aggiungere correttamente, semplicemente rinominando le mie funzioni per gestire l'input dell'utente e disegnare il gioco su ciò che l'autore dell'articolo ha nel suo codice di esempio, il gioco non ha fatto nulla .

Ho inserito un fprintf( stderr, "Test\n" );ciclo while che non viene stampato fino alla fine del gioco.

In che modo questo ciclo di gioco viene eseguito 25 volte al secondo, garantito, mentre il rendering è il più veloce possibile?

Per me, a meno che non mi manchi qualcosa di ENORME, sembra ... niente.

E non è questa struttura, di questo ciclo while, presumibilmente in esecuzione 25 volte al secondo e quindi aggiornare il gioco esattamente quello che ho menzionato prima all'inizio dell'articolo?

In questo caso, perché non potremmo fare qualcosa di semplice come:

while( loops < 25 )
{
    getInput();
    performLogic();

    loops++;
}

drawGame();

E conta per l'interpolazione in qualche altro modo.

Perdona la mia domanda estremamente lunga, ma questo articolo ha fatto più male che bene a me. Sono gravemente confuso ora - e non ho idea di come implementare un ciclo di gioco adeguato a causa di tutte queste domande sorte.


1
Gli sfoghi sono meglio indirizzati verso l'autore dell'articolo. Quale parte è la tua domanda obiettiva ?
Anko,

3
Questo ciclo di gioco è anche valido, qualcuno spiega. Dai miei test non ha la struttura corretta per essere eseguita 25 volte al secondo. Spiega al mio perché lo fa. Anche questo non è un rant, questa è una serie di domande. Devo usare le emoticon, sembro arrabbiato?
Tsujp,

2
Dal momento che la tua domanda si riduce a "Cosa non capisco di questo ciclo di gioco", e hai molte parole in grassetto, viene fuori almeno come esasperato.
Kirbinator,

@Kirbinator Posso apprezzarlo, ma stavo cercando di fondare tutto ciò che trovo insolito in questo articolo, quindi non è una domanda superficiale e vuota. Non penso che sia per antonomasia che, comunque, mi piacerebbe pensare di avere alcuni punti validi - dopo tutto quello che è un articolo che cerca di insegnare, ma non sta facendo un buon lavoro.
Tsujp,

7
Non fraintendetemi, è una buona domanda, potrebbe essere più breve dell'80%.
Kirbinator,

Risposte:


8

Penso che l'autore abbia fatto un piccolo errore:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

dovrebbe essere

while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)

Cioè: fintanto che non è ancora il momento di disegnare il nostro prossimo fotogramma e mentre non abbiamo saltato tanti fotogrammi come MAX_FRAMESKIP, dovremmo aspettare.

Inoltre non capisco perché si aggiorni next_game_ticknel ciclo e presumo che sia un altro errore. Poiché all'inizio di un frame è possibile determinare quando dovrebbe essere il frame successivo (quando si utilizza un frame rate fisso). Il next game ticknon dipende da quanto tempo ci rimane dopo che l'aggiornamento e il rendering.

L'autore commette anche un altro errore comune

con ciò che resta, render il gioco il più volte possibile.

Questo significa rendere lo stesso frame più volte. L'autore è persino a conoscenza di ciò:

Il gioco verrà aggiornato a una velocità costante di 50 volte al secondo e il rendering verrà eseguito il più rapidamente possibile. Si noti che quando il rendering viene eseguito più di 50 volte al secondo, alcuni fotogrammi successivi saranno gli stessi, quindi i fotogrammi visivi effettivi verranno visualizzati ad un massimo di 50 fotogrammi al secondo.

Questo fa sì che la GPU faccia un lavoro non necessario e se il rendering richiede più tempo del previsto, potrebbe iniziare a lavorare sul frame successivo più tardi del previsto, quindi è meglio cedere al sistema operativo e attendere.


+ Voto. Mmmm. Questo in effetti mi lascia perplesso su cosa fare allora. Ti ringrazio per la tua comprensione. Probabilmente avrò un gioco, il problema è davvero la conoscenza di limitare FPS o di avere FPS impostato in modo dinamico e come farlo. Nella mia mente è necessaria una gestione degli input fissi, quindi il gioco funziona allo stesso ritmo per tutti. Questo è solo un semplice MMO 2D Platformer (a lungo termine)
tsujp,

Mentre il ciclo sembra corretto e l'incremento di next_game_tick è lì per questo. È lì per far funzionare la simulazione a velocità costante su hardware sempre più lento. L'esecuzione del codice di rendering "il più velocemente possibile" (o comunque più veloce della fisica) ha senso solo quando c'è un'interpolazione nel codice di rendering per renderlo più fluido per oggetti veloci ecc. (Fine dell'articolo), ma è solo uno spreco di energia se sta eseguendo il rendering più di ciò che qualunque dispositivo di output (schermo) sia in grado di mostrare.
Dotti,

Quindi il suo codice è un'implementazione valida, ora lo è. E non ci resta che vivere con questi rifiuti? O ci sono metodi che posso cercare per questo.
Tsujp,

Cedere al sistema operativo e attendere è una buona idea solo se hai una buona idea di quando il sistema operativo ti restituirà il controllo, il che potrebbe non avvenire non appena lo desideri.
Kylotan,

Non posso votare questa risposta. Manca del tutto l'interpolazione, il che rende invalida l'intera seconda metà della risposta.
AlbeyAmakiir,

4

Forse è meglio se lo semplificassi un po ':

while( game_is_running ) {

    current = GetTickCount();
    while(current > next_game_tick) {
        update_game();

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whileloop all'interno di mainloop viene utilizzato per eseguire passaggi di simulazione da qualsiasi luogo si trovasse, a dove dovrebbe essere ora. update_game()La funzione dovrebbe sempre presumere che SKIP_TICKSsia trascorso solo il tempo trascorso dall'ultima chiamata. Ciò manterrà la fisica del gioco a velocità costante su hardware lento e veloce.

Incrementando next_game_tickdella quantità di SKIP_TICKSspostamenti si avvicina all'ora corrente. Quando questo diventa più grande del tempo corrente, si rompe ( current > next_game_tick) e mainloop continua a visualizzare il fotogramma corrente.

Dopo il rendering, la prossima chiamata a GetTickCount()restituirà la nuova ora corrente. Se questo tempo è superiore next_game_tick, significa che siamo già dietro le fasi 1-N nella simulazione e dovremmo recuperare il ritardo, eseguendo ogni fase della simulazione alla stessa velocità costante. In questo caso, se è inferiore, renderebbe nuovamente lo stesso frame (a meno che non ci sia interpolazione).

Il codice originale aveva limitato il numero di loop se rimanevamo troppo lontani ( MAX_FRAMESKIP). Questo lo rende effettivamente mostrare qualcosa e non sembra essere bloccato se, ad esempio, riprende dalla sospensione o il gioco viene sospeso nel debugger per lungo tempo (supponendo che GetTickCount()non si fermi durante quel periodo) fino a quando non ha raggiunto il tempo.

Per sbarazzarsi del rendering dello stesso frame inutile se non si utilizza l'interpolazione all'interno display_game(), è possibile inserirlo all'interno se un'istruzione come:

while (game_is_running) {
    current = GetTickCount();
    if (current > next_game_tick) {
        while(current > next_game_tick) {
            update_game();

            next_game_tick += SKIP_TICKS;
        }
    display_game();
    }
    else {
    // could even sleep here
    }
}

Questo è anche un buon articolo su questo: http://gafferongames.com/game-physics/fix-your-timestep/

Inoltre, forse il motivo per cui le tue fprintfuscite alla fine del gioco potrebbe essere solo che non è stato scaricato.

Mi dispiace per il mio inglese.


4

Il suo codice sembra del tutto valido.

Considera il whileloop dell'ultimo set:

// JS / pseudocode
var current_time = function () { return Date.now(); }, // in ms
    framerate = 1000/30, // 30fps
    next_frame = current_time(),

    max_updates_per_draw = 5,

    iterations;

while (game_running) {

    iterations = 0;

    while (current_time() > next_frame && iterations < max_updates_per_draw) {
        update_game(); // input, physics, audio, etc

        next_frame += framerate;
        iterations += 1;
    }

    draw();
}

Ho un sistema in atto che dice "mentre il gioco è in esecuzione, controlla l'ora corrente - se è maggiore del nostro conteggio framerate in esecuzione, e abbiamo saltato il disegno meno di 5 fotogrammi, quindi salta il disegno e aggiorna l'input e fisica: altrimenti disegna la scena e avvia la prossima iterazione di aggiornamento "

Quando si verifica ogni aggiornamento, si incrementa il tempo "next_frame" in base al framerate ideale. Quindi controlli di nuovo il tuo tempo. Se il tuo orario attuale è ora inferiore a quando il next_frame deve essere aggiornato, salta sopra l'aggiornamento e disegna ciò che hai.

Se il tuo current_time è maggiore (immagina che l'ultimo processo di disegno abbia richiesto molto tempo, perché c'era qualche singhiozzo da qualche parte, o un mucchio di garbage collection in una lingua gestita, o un'implementazione di memoria gestita in C ++ o altro), allora draw viene saltato e next_frameviene aggiornato con un altro frame di tempo in più, fino a quando gli aggiornamenti raggiungono il punto in cui dovremmo essere sul clock, oppure abbiamo saltato disegnando abbastanza frame che DEVE assolutamente disegnarne uno, in modo che il giocatore possa vedere cosa stanno facendo.

Se la tua macchina è super-veloce o il tuo gioco è super-semplice, current_timepotrebbe essere meno che next_framefrequente, il che significa che non ti stai aggiornando durante quei punti.

Ecco dove entra in gioco l'interpolazione. Allo stesso modo, potresti avere un bool separato, dichiarato al di fuori dei loop, per dichiarare lo spazio "sporco".

All'interno del ciclo di aggiornamento, avresti impostato dirty = true, indicando che hai effettivamente eseguito un aggiornamento.

Quindi, invece di chiamare draw(), diresti:

if (is_dirty) {
    draw(); 
    is_dirty = false;
}

Quindi ti stai perdendo qualsiasi interpolazione per un movimento fluido, ma ti stai assicurando che stai aggiornando solo quando hai effettivamente degli aggiornamenti (piuttosto che interpolazioni tra stati).

Se sei coraggioso, c'è un articolo chiamato "Fix Your Timestep!" di GafferOnGames.
Affronta il problema in modo leggermente diverso, ma lo considero una soluzione più carina che fa principalmente la stessa cosa (a seconda delle caratteristiche della tua lingua e di quanto ti interessi ai calcoli della fisica del tuo gioco).

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.