Recientemente leí este artículo sobre Game Loops: http://www.koonsolo.com/news/dewitters-gameloop/
Y la última implementación recomendada me está confundiendo profundamente. No entiendo cómo funciona, y parece un completo desastre.
Entiendo el principio: actualizar el juego a una velocidad constante, con lo que quede, renderice el juego tantas veces como sea posible.
Supongo que no puedes usar un:
- Obtenga información para 25 ticks
- Juego de render para 975 ticks
Enfoque ya que recibiría información para la primera parte de la segunda y esto se sentiría extraño? ¿O es eso lo que está pasando en el artículo?
Esencialmente:
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)
¿Cómo es eso válido?
Asumamos sus valores.
MAX_FRAMESKIP = 5
Supongamos next_game_tick, que se asignó momentos después de la inicialización, antes de que el bucle principal del juego diga ... 500.
Finalmente, como estoy usando SDL y OpenGL para mi juego, con OpenGL solo para renderizar, supongamos que GetTickCount()
devuelve el tiempo desde que se llamó a SDL_Init, lo que hace.
SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.
Fuente: http://www.libsdl.org/docs/html/sdlgetticks.html
El autor también asume esto:
DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started
Si ampliamos la while
declaración obtenemos:
while( ( 750 > 500 ) && ( 0 < 5 ) )
750 porque ha pasado el tiempo desde que next_game_tick
fue asignado. loops
es cero como puedes ver en el artículo.
Así que hemos ingresado al ciclo while, hagamos algo de lógica y aceptemos alguna entrada.
Bla bla bla.
Al final del ciclo while, que te recuerdo que está dentro de nuestro ciclo principal del juego es:
next_game_tick += SKIP_TICKS;
loops++;
Actualicemos cómo se ve la próxima iteración del código while
while( ( 1000 > 540 ) && ( 1 < 5 ) )
1000 porque ha transcurrido el tiempo de obtener entrada y hacer cosas antes de llegar a la siguiente ineteración del bucle, donde se recupera GetTickCount ().
540 porque, en el código 1000/25 = 40, por lo tanto, 500 + 40 = 540
1 porque nuestro ciclo ha iterado una vez
5 , ya sabes por qué.
Entonces, dado que este ciclo While depende CLARAMENTE de él MAX_FRAMESKIP
y no del TICKS_PER_SECOND = 25;
modo previsto, ¿cómo se supone que el juego se ejecuta correctamente?
No me sorprendió que cuando implementé esto en mi código, correctamente podría agregar, ya que simplemente cambié el nombre de mis funciones para manejar la entrada del usuario y dibujar el juego a lo que el autor del artículo tiene en su código de ejemplo, el juego no hizo nada .
Coloqué un fprintf( stderr, "Test\n" );
bucle while dentro que no se imprime hasta que termina el juego.
¿Cómo se ejecuta este ciclo de juego 25 veces por segundo, garantizado, mientras se procesa tan rápido como puede?
Para mí, a menos que me falte algo ENORME, parece ... nada.
¿Y no es esta estructura, de este ciclo while, supuestamente ejecutándose 25 veces por segundo y luego actualizando el juego exactamente lo que mencioné anteriormente al comienzo del artículo?
Si ese es el caso, ¿por qué no podríamos hacer algo simple como:
while( loops < 25 )
{
getInput();
performLogic();
loops++;
}
drawGame();
Y contar para la interpolación de otra manera.
Perdona mi pregunta extremadamente larga, pero este artículo me ha hecho más daño que bien. Ahora estoy muy confundido, y no tengo idea de cómo implementar un bucle de juego adecuado debido a todas estas preguntas surgidas.
fuente
Respuestas:
Creo que el autor cometió un pequeño error:
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)
debiera ser
while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)
Es decir: mientras todavía no sea hora de dibujar nuestro próximo fotograma y aunque no hemos omitido tantos fotogramas como MAX_FRAMESKIP, debemos esperar.
Tampoco entiendo por qué se actualiza
next_game_tick
en el bucle y supongo que es otro error. Dado que al comienzo de un cuadro puede determinar cuándo debe ser el siguiente cuadro (cuando se utiliza una velocidad de cuadro fija). Elnext game tick
no depende de la cantidad de tiempo que nos sobra después de la actualización y la representación.El autor también comete otro error común.
Esto significa renderizar el mismo marco varias veces. El autor incluso es consciente de eso:
Esto solo hace que la GPU haga un trabajo innecesario y si el procesamiento tarda más de lo esperado, podría hacer que comience a trabajar en su próximo marco más tarde de lo previsto, por lo que es mejor ceder al sistema operativo y esperar.
fuente
Quizás sea mejor si lo simplifico un poco:
while
loop inside mainloop se usa para ejecutar pasos de simulación desde donde estaba hasta donde debería estar ahora.update_game()
La función siempre debe suponer que soloSKIP_TICKS
ha pasado una cantidad de tiempo desde la última llamada. Esto mantendrá la física del juego funcionando a velocidad constante en hardware lento y rápido.Incrementando
next_game_tick
por la cantidad deSKIP_TICKS
movimientos lo acerca al tiempo actual. Cuando esto se hace más grande que la hora actual, se rompe (current > next_game_tick
) y mainloop continúa representando el cuadro actual.Después de renderizar, la siguiente llamada a
GetTickCount()
devolvería la nueva hora actual. Si este tiempo es mayor denext_game_tick
lo que significa que ya estamos detrás de los pasos 1-N en la simulación y debemos ponernos al día, ejecutando cada paso en la simulación a la misma velocidad constante. En este caso, si es más bajo, simplemente representará el mismo marco nuevamente (a menos que haya interpolación).El código original había limitado el número de bucles si nos quedamos demasiado lejos (
MAX_FRAMESKIP
). Esto solo hace que realmente muestre algo y no parezca estar encerrado si, por ejemplo, se reanuda desde la suspensión o el juegoGetTickCount()
se detiene en el depurador durante mucho tiempo (suponiendo que no se detenga durante ese tiempo) hasta que haya alcanzado el tiempo.Para deshacerse de la representación inútil del mismo marco si no está utilizando la interpolación en el interior
display_game()
, puede envolverla dentro si una declaración como:Este también es un buen artículo sobre esto: http://gafferongames.com/game-physics/fix-your-timestep/
Además, quizás la razón por la cual tus
fprintf
resultados cuando finaliza tu juego podría ser que no se enjuagó.Perdón por mi inglés.
fuente
Su código parece completamente válido.
Considere el
while
bucle del último conjunto:Tengo un sistema en el lugar que dice "mientras el juego se está ejecutando, verifique la hora actual, si es mayor que nuestra cuenta de velocidad de cuadros en ejecución, y hemos omitido dibujar menos de 5 cuadros, luego omita el dibujo y simplemente actualice la entrada y física: dibuja la escena y comienza la próxima iteración de actualización "
Cuando ocurre cada actualización, incrementa el tiempo "next_frame" en su framerate ideal. Luego revisas tu tiempo nuevamente. Si su hora actual es ahora menor que cuando debería actualizarse el siguiente marco, entonces omita la actualización y dibuje lo que tiene.
Si su current_time es mayor (imagine que el último proceso de extracción tomó mucho tiempo, porque hubo algún inconveniente en alguna parte, o un montón de recolección de basura en un lenguaje administrado, o una implementación de memoria administrada en C ++ o lo que sea), entonces se omite el sorteo y
next_frame
se actualiza con otro marco adicional que vale la pena, hasta que las actualizaciones lleguen a donde deberíamos estar en el reloj, o se nos salte el dibujo de suficientes cuadros que DEBEMOS dibujar uno, para que el jugador pueda ver Lo que están haciendo.Si su máquina es súper rápida o su juego es súper simple, entonces
current_time
podría ser menosnext_frame
frecuente, lo que significa que no está actualizando durante esos puntos.Ahí es donde entra la interpolación. Del mismo modo, podría tener un bool separado, declarado fuera de los bucles, para declarar el espacio "sucio".
Dentro del ciclo de actualización, establecería
dirty = true
, lo que significa que realmente ha realizado una actualización.Luego, en lugar de solo llamar
draw()
, diría:Entonces se está perdiendo cualquier interpolación para un movimiento suave, pero se está asegurando de que solo esté actualizando cuando haya actualizaciones realmente sucediendo (en lugar de interpolaciones entre estados).
Si eres valiente, hay un artículo llamado "Fix Your Timestep!" por GafferOnGames.
Aborda el problema de manera ligeramente diferente, pero considero que es una solución más bonita que hace casi lo mismo (dependiendo de las características de su idioma y cuánto le importan los cálculos físicos de su juego).
fuente