Un buon modo per creare un loop di gioco in OpenGL


31

Attualmente sto iniziando a studiare OpenGL a scuola e l'altro giorno ho iniziato a creare un gioco (da solo, non per la scuola). Sto usando Freeglut, e lo sto costruendo in C, quindi per il mio loop di gioco avevo appena usato una funzione che avevo passato glutIdleFuncper aggiornare tutto il disegno e la fisica in un unico passaggio. Questo andava bene per le semplici animazioni che non mi interessavano troppo della frequenza dei fotogrammi, ma poiché il gioco è principalmente basato sulla fisica, voglio davvero (ho bisogno di) limitare la velocità di aggiornamento.

Quindi il mio primo tentativo è stato quello di avere la mia funzione a cui passo glutIdleFunc( myIdle()) per tenere traccia di quanto tempo è trascorso dalla precedente chiamata e aggiornare la fisica (e attualmente la grafica) ogni millisecondi. Lo facevo timeGetTime()(usando <windows.h>). E questo mi ha fatto pensare: usare la funzione inattiva è davvero un buon modo di fare il giro del gioco?

La mia domanda è: qual è il modo migliore per implementare il loop di gioco in OpenGL? Devo evitare di utilizzare la funzione inattiva?



Sento che questa domanda è più specifica per OpenGL e se è consigliabile usare GLUT per il loop
Jeff,

1
Qui amico , non usare GLUT per progetti più grandi . Va bene per quelli più piccoli, però.
bobobobo,

Risposte:


29

La semplice risposta è no, non vuoi usare il callback glutIdleFunc in un gioco che ha una sorta di simulazione. La ragione di ciò è che questa funzione separa l'animazione e disegna il codice dalla gestione degli eventi della finestra ma non in modo asincrono. In altre parole, ricevere e consegnare gli eventi della finestra in stallo disegna il codice (o qualsiasi cosa tu abbia inserito in questo callback), questo è perfettamente adatto per un'applicazione interattiva (interagisci, quindi risposta), ma non per un gioco in cui la fisica o lo stato del gioco devono progredire indipendentemente dall'interazione o dal tempo di rendering.

Vuoi disaccoppiare completamente la gestione dell'input, lo stato del gioco e disegnare il codice. Esiste una soluzione semplice e chiara a ciò che non coinvolge direttamente la libreria grafica (ovvero è portatile e facile da visualizzare); vuoi che l'intero ciclo di gioco produca tempo e che la simulazione consumi il tempo prodotto (in blocchi). La chiave tuttavia è quindi integrare il tempo impiegato dalla simulazione nell'animazione.

La migliore spiegazione e tutorial che ho trovato su questo è Fix Your Timestep di Glenn Fiedler

Questo tutorial ha il pieno trattamento, se non si dispone di un vero e proprio simulazione fisica, è possibile saltare la vera integrazione, ma il ciclo di base bolle ancora giù fino a (in verbose pseudo-codice):

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

In questo modo, le bancarelle nel codice di rendering, nella gestione degli input o nel sistema operativo non causano ritardi nello stato del gioco. Questo metodo è anche portatile e indipendente dalla libreria grafica.

GLUT è una libreria eccellente ma è strettamente guidata dagli eventi. Registri callback e spegni il loop principale. Consegni sempre il controllo del tuo loop principale usando GLUT. Ci sono hack per aggirarlo , puoi anche falsificare un loop esterno usando timer e simili, ma un'altra libreria è probabilmente un modo migliore (più semplice) per andare. Ci sono molte alternative, eccone alcune (quelle con una buona documentazione e brevi tutorial):

  • GLFW che ti dà la possibilità di ottenere eventi di input in linea (nel tuo ciclo principale).
  • SDL , tuttavia la sua enfasi non è specificamente OpenGL.

Buon articolo Quindi per farlo in OpenGL, non userei glutMainLoop, ma invece il mio? Se lo facessi, sarei in grado di utilizzare glutMouseFunc e altre funzioni? Spero che abbia un senso, sono ancora abbastanza nuovo in tutto questo
Jeff,

Sfortunatamente no. Tutti i callback registrati in GLUT vengono espulsi da glutMainLoop quando la coda degli eventi viene svuotata e viene ripetuta. Ho aggiunto alcuni collegamenti ad alternative nel corpo della risposta.
Charstar,

1
+1 per abbandonare GLUT per qualsiasi altra cosa. Per quanto ne so, GLUT non è mai stato pensato per nient'altro che applicazioni di test.
Jari Komppa,

Devi comunque gestire gli eventi di sistema, no? La mia comprensione è stata che usando glutIdleFunc, gestisce semplicemente il ciclo (in Windows) GetMessage-TranslateMessage-DispatchMessage e chiama la tua funzione ogni volta che non ci sono messaggi da ricevere. Lo faresti tu stesso, oppure subiresti l'impazienza del sistema operativo nel contrassegnare la tua applicazione come non rispondente, giusto?
Ricket,

@Ricket Tecnicamente, glutMainLoopEvent () gestisce gli eventi in arrivo chiamando i callback registrati. glutMainLoop () è effettivamente {glutMainLoopEvent (); IdleCallback (); } Una considerazione chiave qui è che il redrawing è anche un callback gestito all'interno del loop degli eventi. È possibile effettuare una comportarsi libreria event-driven come una sola volta-driven, ma Martello di Maslow e tutto il resto ...
charstar

4

Penso che glutIdleFuncvada bene; è essenzialmente quello che faresti comunque a mano, se non lo usassi. Indipendentemente da cosa avrai un ciclo stretto che non viene chiamato in un modello fisso, e devi scegliere se vuoi convertirlo in un ciclo a tempo fisso o mantenerlo in un ciclo a tempo variabile e creare sicuramente tutti i tuoi conti matematici per l'interpolazione del tempo. O anche un mix di questi, in qualche modo.

Puoi certamente avere una myIdlefunzione a cui passi glutIdleFunc, e al suo interno, misurare il tempo che è trascorso, dividerlo per il tuo timestep fisso e chiamare un'altra funzione che molte volte.

Ecco cosa non fare:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

fixedTimestep non verrebbe chiamato ogni 10 millisecondi. In effetti sarebbe più lento del tempo reale, e potresti finire per confondere i numeri per compensare quando in realtà la radice del problema risiede nella perdita del resto quando dividi timeDifferenceper 10.

Invece, fai qualcosa del genere:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

Ora questa struttura ad anello assicura che la tua fixedTimestepfunzione venga chiamata una volta ogni 10 millisecondi, senza perdere millisecondi come resto o qualcosa del genere.

A causa della natura multitasking dei sistemi operativi, non è possibile fare affidamento su una funzione chiamata esattamente ogni 10 millisecondi; ma il ciclo sopra sarebbe abbastanza veloce da sperare che fosse abbastanza vicino, e puoi supporre che ogni chiamata di fixedTimestepincrementerebbe la tua simulazione di 10 millisecondi.

Leggere anche questa risposta e in particolare i collegamenti forniti nella risposta.

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.