Una buena forma de crear un bucle de juego en OpenGL


31

Actualmente estoy empezando a aprender OpenGL en la escuela, y el otro día empecé a hacer un juego simple (solo, no para la escuela). Estoy usando Freeglut, y lo estoy construyendo en C, por lo que para mi ciclo de juego realmente había estado usando una función que pasé glutIdleFuncpara actualizar todo el dibujo y la física en una sola pasada. Esto estaba bien para animaciones simples que no me importaban demasiado la velocidad de fotogramas, pero dado que el juego se basa principalmente en la física, realmente quiero (necesito) vincular la velocidad de actualización.

Entonces, mi primer intento fue tener mi función que paso a glutIdleFunc( myIdle()) para realizar un seguimiento de cuánto tiempo ha pasado desde la llamada anterior y actualizar la física (y los gráficos actuales) cada tantos milisegundos. Solía timeGetTime()hacer esto (usando <windows.h>). Y esto me hizo pensar, ¿usar la función inactiva es realmente una buena forma de recorrer el ciclo del juego?

Mi pregunta es, ¿cuál es una mejor manera de implementar el bucle del juego en OpenGL? ¿Debo evitar usar la función inactiva?



Siento que esta pregunta es más específica para OpenGL y si es aconsejable usar GLUT para el ciclo
Jeff

1
Aquí hombre , no uses GLUT para proyectos más grandes . Sin embargo, está bien para los más pequeños.
bobobobo

Respuestas:


29

La respuesta simple es no, no desea utilizar la devolución de llamada glutIdleFunc en un juego que tiene algún tipo de simulación. La razón de esto es que esta función divorcia la animación y dibuja el código del manejo de eventos de la ventana, pero no de forma asincrónica. En otras palabras, recibir y entregar código de sorteo de eventos de ventana (o lo que sea que coloque en esta devolución de llamada), esto está perfectamente bien para una aplicación interactiva (interactuar, luego responder), pero no para un juego donde la física o el estado del juego deben progresar independiente de la interacción o el tiempo de procesamiento.

Desea desacoplar completamente el manejo de entrada, el estado del juego y el código de sorteo. Hay una solución fácil y limpia para esto que no involucra la biblioteca de gráficos directamente (es decir, es portátil y fácil de visualizar); desea que todo el ciclo del juego produzca tiempo y que la simulación consuma el tiempo producido (en fragmentos). Sin embargo, la clave es integrar la cantidad de tiempo que su simulación consumió en su animación.

La mejor explicación y tutorial que he encontrado sobre esto es Fix Your Timestep de Glenn Fiedler

Este tutorial tiene el para tratamiento completo, sin embargo, si usted no tiene una verdadera simulación de la física, puede omitir la verdadera integración, sino el bucle básico todavía se reduce a (detallado en pseudo-código):

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

Al hacerlo de esta manera, los bloqueos en su código de procesamiento, manejo de entrada o sistema operativo no hacen que su estado de juego se quede atrás. Este método también es portátil e independiente de la biblioteca de gráficos.

GLUT es una buena biblioteca, sin embargo, está estrictamente basada en eventos. Usted registra devoluciones de llamada y dispara el bucle principal. Siempre entrega el control de su bucle principal usando GLUT. Hay trucos para evitarlo , también puede simular un bucle externo utilizando temporizadores y demás, pero otra biblioteca es probablemente una mejor manera (más fácil) de hacerlo. Hay muchas alternativas, aquí hay algunas (algunas con buena documentación y tutoriales rápidos):

  • GLFW que le brinda la capacidad de obtener eventos de entrada en línea (en su propio bucle principal).
  • SDL , sin embargo, su énfasis no es específicamente OpenGL.

Buen artículo. Entonces, para hacer esto en OpenGL, no usaría glutMainLoop, sino el mío. Si hiciera eso, ¿podría usar glutMouseFunc y otras funciones? Espero que tenga sentido, todavía soy bastante nuevo en todo esto
Jeff

Lamentablemente no. Todas las devoluciones de llamada registradas en GLUT se activan desde glutMainLoop a medida que se agota la cola de eventos y se repite. Agregué algunos enlaces a alternativas en el cuerpo de la respuesta.
charstar

1
+1 por deshacerse de GLUT por cualquier otra cosa. Por lo que sé, GLUT nunca fue diseñado para nada más que aplicaciones de prueba.
Jari Komppa

Sin embargo, todavía tiene que manejar eventos del sistema, ¿no? Comprendí que al usar glutIdleFunc, solo maneja el bucle GetMessage-TranslateMessage-DispatchMessage (en Windows) y llama a su función cada vez que no se recibe ningún mensaje. Lo haría usted mismo de todos modos, o sufriría la impaciencia del sistema operativo para marcar su aplicación como no responde, ¿verdad?
Ricket

@Ricket Técnicamente, glutMainLoopEvent () maneja los eventos entrantes llamando a las devoluciones de llamada registradas. glutMainLoop () es efectivamente {glutMainLoopEvent (); IdleCallback (); } Una consideración clave aquí es que, volver a dibujar también es una devolución de llamada manejada desde el bucle de eventos. Usted puede hacer una comportan biblioteca orientada a eventos como un uno por tiempo, pero Martillo de Maslow y todo lo que ...
charstar

4

Creo que glutIdleFuncestá bien; es esencialmente lo que terminarías haciendo a mano de todos modos, si no lo usaras. No importa lo que vaya a tener un ciclo cerrado que no se llama en un patrón fijo, y debe elegir si desea convertirlo en un ciclo de tiempo fijo o mantenerlo en un ciclo de tiempo variable y hacer Seguro que todas tus cuentas de matemáticas tienen tiempo de interpolación. O incluso una mezcla de estos, de alguna manera.

Ciertamente puede tener una myIdlefunción a la que pasa glutIdleFunc, y dentro de eso, medir el tiempo que ha pasado, dividirlo por su paso de tiempo fijo y llamar a otra función tantas veces.

Esto es lo que no debes hacer:

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

fixedTimestep no se llamaría cada 10 milisegundos. De hecho, sería más lento que el tiempo real, y podría terminar falsificando números para compensar cuando realmente la raíz del problema radica en la pérdida del resto cuando se divide timeDifferencepor 10.

En cambio, haz algo como esto:

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

Ahora, esta estructura de bucle garantiza que su fixedTimestepfunción se llame una vez cada 10 milisegundos, sin perder milisegundos como resto ni nada por el estilo.

Debido a la naturaleza multitarea de los sistemas operativos, no puede confiar en que una función se llame exactamente cada 10 milisegundos; pero el bucle anterior sucedería lo suficientemente rápido como para que sea lo suficientemente cerca, y puede suponer que cada llamada fixedTimestepincrementaría su simulación en 10 milisegundos.

Lea también esta respuesta y especialmente los enlaces proporcionados en la respuesta.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.