¿Cómo manejan los juegos los problemas de los bucles de velocidad de fotogramas variables?

Tendré que estar un poco en desacuerdo contigo sobre que nada malo suceda cuando la velocidad de fotogramas disminuya porque he leído más de una crítica en la que las partes realmente activas de algunos juegos se vuelven menos receptivas y los elementos no se mueven como si fueran ‘ Se supone que debe moverse y la detección de colisión deja de funcionar correctamente. Sin mencionar que esto también puede causar problemas de entrada.

Monim Wains mencionó que hay dos bucles, uno para física y otro para gráficos, pero si el bucle de gráficos está separado o no del bucle de actualización depende del motor. Cuando los dos bucles están separados, un motor bien hecho debería soltar algunos de los fotogramas del bucle de gráficos para mantener el bucle de actualización actualizado. Realmente no puedo decir mucho sobre tu motor de juego, ya que en realidad no mencionas cuál estás usando.

Una debilidad con la función de bucle separado es el desarrollador del juego. Cuando estaba aprendiendo XNA, noté que parte del código que encontré en línea tenía código dentro del ciclo de actualización que solo era necesario para ejecutarse dentro del ciclo de extracción. Dejar caer cuadros de dibujo no ayuda tanto cuando comienzas a colocar el código de dibujo dentro del ciclo de actualización.

Dos de las respuestas anteriores han hablado sobre crear su propio tiempo delta y usarlo en sus cálculos de física. Estoy totalmente de acuerdo con ellos en la importancia de utilizar el tiempo delta al calcular el movimiento de los objetos del juego. No he jugado con muchos motores de juego, pero los que he jugado ya calcularán el tiempo delta para ti. El tiempo delta es realmente importante para tratar con velocidades de cuadro variables. Si conduce a 70 mph, ¿qué tan lejos ha conducido? No sé, cuánto tiempo llevas conduciendo.

Hay diferentes formas de usar el tiempo delta; algunas ecuaciones lo usarán para actualizar los vectores de velocidad multiplicando los vectores de aceleración por el tiempo delta para encontrar el cambio de velocidad, aplicando este cambio de velocidad a la velocidad anterior para crear una nueva velocidad, y luego promediando la nueva velocidad con la velocidad anterior , y usando la velocidad promedio junto con el tiempo delta para determinar el cambio de posición. Luego, hay algunas ecuaciones que simplemente multiplican la velocidad por el tiempo delta.

Uno de mis viejos videojuegos, desde 2005, funcionó bien en la primera computadora que construí. Cuando construí una nueva versión de juegos, instalé ese mismo juego y noté que algo estaba mal. Todo el juego estaba jugando en avance rápido. Me reí mucho y luego comencé a jugar un juego diferente.

Con el bucle de juego de un solo hilo más simple donde el juego corre lento en máquina lenta, rápido en máquina rápida, no tiene absolutamente ningún otro problema que correr lento y molestar a los jugadores. Aquí el tiempo delta del fotograma es irrelevante ya que la física del movimiento (y = y + vy, vy = vy + gravedad) o la detección de colisión de un objeto en movimiento (x = x + vx, verificar la colisión de x con otro objeto) cada fotograma es el mismo independientemente de la velocidad de fotogramas real en tiempo real. El problema surge cuando intenta escalar las variables en proporción a la velocidad de fotogramas real (como la gravedad y vx) para que el flujo de tiempo de su juego sea constante. Puede ver que vx se vuelve muy grande en la máquina lenta y omite un objeto para colisión o la variable de gravedad se vuelve demasiado grande y el movimiento se vuelve irreal debido a la destrucción. Si compensa las variables del juego como esta, el cálculo de la física se vuelve más complejo, ya que debe integrarse por falta de tiempo. Al igual que con la detección de colisión, no solo puede confiar en la verificación de límites, sino también utilizar la intersección de la línea de trayectoria de vuelo, sino que incluso los resultados no son consistentes.

Muchos juegos a principios de los 80 se escribieron así. ¡Cuando se ejecuta en máquinas rápidas de próxima generación, estos juegos se ejecutan demasiado rápido para ser jugables!

La solución más simple para la velocidad variable de la máquina es usar una velocidad de cuadro fija (generalmente 30 fps o 60 fps y ocasionalmente 50 fps es la velocidad más popular). Aquí solo ejecuta toda la lógica del juego (incluido el renderizado y todo lo demás) dentro de un tiempo delta fijo. Debe elegir los requisitos mínimos del sistema aquí. Lo bueno de este método es que incluso en un sistema por debajo del mínimo, el juego funcionará bien pero más lento. En máquinas más rápidas, el juego seguirá funcionando a una velocidad fija. Un pequeño inconveniente (o al alza depende de su situación) de este método es que el sistema mínimo se comporta igual que las máquinas de gama alta.

El siguiente método más simple es usar un bucle lógico de juego fijo y un hilo de renderizado asíncrono. Aquí ejecutas la lógica, la física y la colisión del juego a una velocidad fija (30, 60 o 50 fps), pero actualizas el renderizado cada vez que se termina el renderizado. Este era un método popular para los juegos 3D FPS en los días de DOS (usando un controlador de interrupción de temporizador) cuando sus velocidades de fotogramas apenas superaban los 10 fotogramas por segundo. Lo malo de esto es que incluso en máquinas realmente rápidas, su velocidad de renderizado no puede superar su velocidad lógica de juego (a 30, 60 o 50 fps).

Nota: En los sistemas modernos con API de representación asincrónica (DirectX u OpenGL … etc.), los 2 métodos anteriores son básicamente lo mismo.

El método más avanzado es una versión ligeramente mejorada del método anterior donde la lógica de su juego se ejecuta a una velocidad fija pero el procesamiento se ejecuta tan rápido como lo permite su sistema. Aquí, incluso si su renderizado se realiza mucho antes de la próxima lógica del juego, el renderizado se actualiza con coordenadas interpoladas para que pueda beneficiarse de un movimiento más suave en máquinas más rápidas.

Hay un método aún más avanzado en el que todo se escala linealmente a un intervalo de tiempo variable, pero no lo recomendaría. La integración de la física del movimiento vuelve a ser un desastre con un intervalo de tiempo variable.

Idealmente, desea que la lógica de su juego se ejecute al mismo tiempo en cada máquina (esto hace que la física, la colisión y el juego general sean consistentes en todas las máquinas).

Esto es para juegos de un solo jugador y las cosas se vuelven un poco más complicadas para los juegos de varios jugadores …

Hay diferentes soluciones a este problema.

Con algunos equipos (PC más antiguas, algunos dispositivos móviles), el rendimiento varía enormemente. Por lo tanto, se hace difícil ofrecer una experiencia consistente.

Estas son algunas soluciones.

  1. Se corrigió Delta T. (trate cada cuadro como si fuera el mismo intervalo de tiempo, incluso si no lo es)
  2. Variable Delta T. (mida el intervalo de tiempo y los deltas de simulación de escala en consecuencia)
  3. Asincrónico. Se corrigió Delta T. (ejecute toda la simulación en pasos de tiempo fijos, ejecute gráficos de forma asincrónica)

La solución 1 da como resultado una experiencia que podría convertirse en cámara lenta bajo cargas pesadas. Y bajo cargas ligeras, el juego corre demasiado rápido. La condición “demasiado rápida” puede remediarse haciendo una pausa.

Solución 2 : medimos el intervalo de velocidad de fotogramas que estamos obteniendo (40 fps, 20 fps, etc.) y luego encontramos la duración del intervalo de fotogramas y usamos ese valor como el incremento t. Entonces, por ejemplo, las velocidades se multiplican por este valor.

Confieso que hice esto en el pasado. Es una idea terrible. Significa que el juego nunca es determinista. Resulta en una gran cantidad de errores, muchos de los cuales son difíciles de recrear.

Solución 3 : es la mejor en mi opinión. Ejecutamos todo el código de simulación a una velocidad de tic regular. Esto podría ser 30 hz o 60 hz. Esto está bloqueado en el tiempo real y da como resultado un juego determinista. No se saltan nunca las garrapatas. El tiempo de tic es un número entero que aumentó en uno cada actualización.

Las mismas entradas siempre producen los mismos resultados.

Ejecutamos el código gráfico de forma asincrónica. Por lo tanto, el bucle de gráficos puede ejecutarse más lento o más rápido que la marca de simulación. Pero usamos la interpolación para suavizar los pasos de simulación. El tiempo de renderizado es un valor flotante que se mezcla entre los ticks adyacentes.

En hardware muy lento, esto crea un poco de retraso, pero las mismas entradas en máquinas lentas producen los mismos resultados que las máquinas rápidas.

Para el código de red, esta tercera solución es aún más importante. Un modelo cliente / servidor significa que el servidor tiene que funcionar independientemente de los gráficos.

¡Solo me gustaría enfatizar la importancia de hacer física (actualizar posiciones, velocidades y aceleraciones) usando pasos de tiempo de tamaño fijo ! Si no haces eso, obtendrás un comportamiento inconsistente , y eso es todo malo (de lo que has notado algunos pocos). Si hay algo a favor, es la consistencia.

Supongamos que ejecutamos su simulación en las computadoras A y B; Si A tarda 5 segundos y B tarda 5 nanosegundos para calcular cada paso de física, aún deben proporcionar exactamente el mismo comportamiento físico. Es decir, para un cierto punto de partida, el número de cuadro 53 debería ser el mismo en A y B, independientemente de si tardó 5 * 53 segundos o 5 * 53 nanosegundos en llegar.

Entonces, con respecto al paso de cálculo: por cualquier razón, no use la hora del sistema (como al consultar la fecha del sistema o similar, consulte algún temporizador de alta precisión). Tampoco uses un delta variable. Tiene sentido usar un bucle para la física y otro para dibujar, manejar entradas y similares. Pero al final del día, asegúrese de que la física se realice con el mismo paso de tiempo en cada cuadro.

Si desea una lectura más profunda, le sugiero este sitio, que también tiene ejemplos de código: ¡Arregle su Timestep!

Y si sigue ese enlace, léalo hasta el final, para que no tenga la idea de que el paso de tiempo variable o el paso de tiempo semi-fijo son sugerencias finales . Buena suerte con tu programación. Una sugerencia más con respecto al contenido vinculado: de todos modos, no se moleste con la interpolación como primer paso, el desgarro visual no es tan malo como la física variable.

La forma en que debe manejarse es la siguiente:

Los desarrolladores SIEMPRE deben separar TODAS las actualizaciones de estado (lógica, física, jugabilidad, animación, etc.) del renderizado. NO se debe modificar el estado en la función de dibujo.

Las cosas sensibles al tiempo, como la física, deben ejecutarse en un cierto paso de tiempo FIJO. Esto puede suceder varias veces por cuadro si el sistema está retrasado.

Una gran cantidad de simulaciones físicas a medida utilizan una integración explícita de primer orden muy rápida, pero bastante imprecisa, que a menudo puede explotar si el intervalo de tiempo es demasiado grande. Esta es la causa de fallas en las que una muñeca de trapo se disparará al aire después de una colisión.

El renderizado debe realizarse lo más rápido posible, con o sin vsync. Idealmente, debería interpolar entre los pasos de tiempo del estado, pero sobre todo solo representa el estado actual de velocidad y simplicidad.

Debe obtener el tiempo desde el último fotograma y realizar su simulación de paso fijo tantas veces como sea necesario para satisfacer ese intervalo de tiempo. A veces, ejecutará la simulación varias veces por cuadro (si su sistema está retrasado) y, a veces, querrá omitir uno o dos cuadros si su juego se está ejecutando increíblemente rápido.

TL; DR Debe separar sus funciones de actualización y dibujo y ejecutarlas de forma independiente.

Esta es una buena pregunta y mientras no sea un juego multijugador, tengo una respuesta rápida.

Por lo tanto, no base su cálculo en el marco en el que se encuentra. (Cuadro 1, Cuadro 2, etc.) Obtendrá los problemas que está describiendo anteriormente. Base todo en el tiempo del sistema .

¿Entonces que significa eso? Para explicar esto, permítanme describir un caso simple.

Entonces, digamos que queremos mover un sprite 2D a través de la pantalla. Si codifica el objeto al mover 1 píxel por fotograma a través de la pantalla, el movimiento del sprite se verá “irregular” mientras se anima a través de la pantalla. Esto se debe a que los cuadros se pueden dibujar en cualquier momento y el sistema operativo no le garantiza que sus rutinas de animación se llamarán en el intervalo especificado.

Pero el sistema operativo no afecta la hora del sistema. Puede consultar la hora del sistema y no será ‘incorrecto’ porque tiene un proceso ocupado. Cuando calcules dónde debe estar el sprite, usa el tiempo del sistema para calcular la nueva posición.

Nuevamente, use el tiempo del sistema para calcular la posición cada vez que dibuja el objeto en la pantalla.

Las colisiones son una cosa diferente en conjunto. Hay una serie de problemas que pueden suceder. Pero en su mayor parte, la solución es casi la misma. Debe calcular si el objeto ha chocado con el otro objeto utilizando la última posición de esos objetos.

¡Espero que esto ayude!

Esto fue especialmente un problema para los juegos más antiguos, pero no era tan notable porque los diseñadores sabían las especificaciones exactas del hardware en el que se jugaba el juego (máquinas arcade), por lo que se conocía el tiempo entre cuadros.

Para los juegos modernos, debido a la variación en el hardware, los marcos no pueden ser estáticos. La solución moderna es tener 2 bucles diferentes que separen la física y los marcos . Un bucle debe ocuparse de todas las salidas a la pantalla. Esto puede variar en términos de marcos.

El segundo ciclo debe ocuparse de la física y otros cálculos de juego. Este bucle debe tener una pausa constante al final para que sea confiable y se ejecute a una velocidad constante. Esta velocidad debe determinarse por la pausa constante, no por la velocidad de cuadros.

En general, desea que su juego (y, por lo tanto, las actualizaciones de su juego) sean predecibles por encima de todo.

La forma más fácil de hacerlo es simplemente actualizar a un FPS constante, y si no puede manejarlo, disminuya el tiempo de juego. Es mejor tener un juego más lento que un juego que se juega de forma irregular. Por otro lado, está perfectamente bien omitir los renderizados de cuadros en una máquina más lenta en comparación con una más rápida.

Es por eso que los motores de juego a menudo desacoplan el renderizado de las actualizaciones.

Conceptualmente se vería algo así como:

game_init ();
mientras corre) {
current_tick = GetTickCount ();
game_draw (current_tick – last_draw_tick);
last_draw_tick = current_tick;
if (current_tick – last_update_tick> = 1000 / UPDATE_FPS) {
last_update_tick = current_tick;
actualización del juego (1000 / UPDATE_FPS);
}
Sueño (10);
}

En realidad, es más simple usar un bucle de velocidad de cuadro variable que un bucle fps fijo.

En su ciclo de actualización (C / C ++):

largo largo lastNow = 0;
updateLoop () {
lastNow = getTimeInNanoseconds ();
while (gameRunningFlagSet) {
long long now = getTimeInNanoseconds ();
long long deltaTime = now – lastNow;
lastNow = ahora;
// TODO: lógica del juego aquí
// Use deltaTime en sus cálculos en lugar de asumir 16.667 ms para 60 FPS fijos
}
}

Reemplace getTineInNanoseconds con la función adecuada para su plataforma y motor.

No necesita protegerse contra desbordamientos de tiempo porque reducirán el rendimiento y no sucederán durante algunos siglos.