EXTREMADAMENTE confundido sobre el bucle de juego "Velocidad constante del juego FPS máximo"

12

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 whiledeclaración obtenemos:

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

750 porque ha pasado el tiempo desde que next_game_tickfue asignado. loopses 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_FRAMESKIPy 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.

tsujp
fuente
1
Las diatribas están mejor dirigidas hacia el autor del artículo. ¿Qué parte es tu pregunta objetiva ?
Anko
3
¿Es este bucle de juego incluso válido ?, explica alguien. Según mis pruebas, no tiene la estructura correcta para ejecutarse 25 veces por segundo. Explica a mi por qué lo hace. Además, esto no es una queja, esta es una serie de preguntas. ¿Debo usar emoticones, me veo enojado?
tsujp
2
Dado que su pregunta se reduce a "¿Qué no estoy entendiendo sobre este bucle del juego", y tiene muchas palabras en negrita, parece que al menos está exasperado.
Kirbinator
@Kirbinator Puedo apreciar eso, pero estaba tratando de fundamentar todo lo que encuentro inusual en este artículo, por lo que no es una pregunta superficial y vacía. No creo que sea por excelencia que, de todos modos, me gustaría pensar que tengo algunos puntos válidos; después de todo, es un artículo que intenta enseñar, pero no hace un buen trabajo.
tsujp
77
No me malinterpreten, es una buena pregunta, podría ser un 80% más corta.
Kirbinator

Respuestas:

8

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_ticken 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). El next game tickno 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.

con lo que quede, renderiza el juego tantas veces como sea posible.

Esto significa renderizar el mismo marco varias veces. El autor incluso es consciente de eso:

El juego se actualizará a un ritmo constante de 50 veces por segundo, y el renderizado se realiza lo más rápido posible. Tenga en cuenta que cuando el renderizado se realiza más de 50 veces por segundo, algunos fotogramas posteriores serán los mismos, por lo que los fotogramas visuales reales se mostrarán a un máximo de 50 fotogramas por segundo.

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.

Roy T.
fuente
+ Voto. Mmmm De hecho, esto me deja desconcertado sobre qué hacer entonces. Te agradezco tu perspicacia. Probablemente jugaré, el problema es realmente el conocimiento sobre limitar FPS o tener FPS configurado dinámicamente, y cómo hacerlo. En mi opinión, se requiere un manejo de entrada fijo para que el juego funcione al mismo ritmo para todos. Este es solo un simple MMO de Plataformas 2D (a la larga)
tsujp
Si bien el bucle parece correcto e incrementar next_game_tick está ahí para eso. Está ahí para mantener la simulación ejecutándose a velocidad constante en hardware cada vez más lento. Ejecutar el código de representación "lo más rápido posible" (o más rápido que la física de todos modos) tiene sentido solo cuando hay una interpolación en el código de representación para que sea más fluido para objetos rápidos, etc. (final del artículo), pero eso es solo energía desperdiciada si está generando más de lo que cualquier dispositivo de salida (pantalla) puede mostrar.
Dotti
Entonces su código es una implementación válida, ahora que es. ¿Y solo tenemos que vivir con este desperdicio? ¿O hay métodos que puedo buscar para esto?
tsujp
Ceder ante el sistema operativo y esperar es solo una buena idea si tiene una buena idea de cuándo el sistema operativo le devolverá el control, lo que puede no ser tan pronto como lo desee.
Kylotan
No puedo votar esta respuesta. Se está perdiendo por completo el material de interpolación, lo que hace que toda la segunda mitad de la respuesta sea inválida.
AlbeyAmakiir
4

Quizás sea mejor si lo simplifico un poco:

while( game_is_running ) {

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

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whileloop 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 solo SKIP_TICKSha 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_tickpor la cantidad de SKIP_TICKSmovimientos 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 de next_game_ticklo 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 juego GetTickCount()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:

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

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 fprintfresultados cuando finaliza tu juego podría ser que no se enjuagó.

Perdón por mi inglés.

Dotti
fuente
4

Su código parece completamente válido.

Considere el whilebucle del último conjunto:

// 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();
}

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_framese 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_timepodría ser menos next_framefrecuente, 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:

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

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

Norguard
fuente