Hay docenas de artículos, libros y debates sobre bucles de juegos. Sin embargo, a menudo me encuentro con algo como esto:
while(running)
{
processInput();
while(isTimeForUpdate)
{
update();
}
render();
}
Lo que básicamente me molesta de este enfoque es la representación "independiente de la actualización", por ejemplo, renderizar un marco cuando no hay ningún cambio. Entonces mi pregunta es ¿por qué este enfoque a menudo se enseña?
while (isTimeForUpdate)
, noif (isTimeForUpdate)
. El objetivo principal no esrender()
cuando no ha habido unupdate()
, sinoupdate()
repetidamente entrerender()
s. Independientemente, ambas situaciones tienen usos válidos. El primero sería válido si el estado puede cambiar fuera de suupdate
función, por ejemplo, cambiar lo que se representa en función del estado implícito, como la hora actual. Este último es válido porque le da a su motor de física la posibilidad de hacer muchas actualizaciones pequeñas y precisas, lo que, por ejemplo, reduce la posibilidad de 'deformación' a través de obstáculos.Respuestas:
Hay una larga historia de cómo llegamos a esta convención común, con muchos desafíos fascinantes en el camino, así que intentaré motivarlo por etapas:
1. Problema: los dispositivos funcionan a diferentes velocidades
¿Alguna vez trataste de jugar un viejo juego de DOS en una PC moderna y se ejecuta de manera increíblemente rápida?
Muchos juegos antiguos tenían un ciclo de actualización muy ingenuo: recolectaban información, actualizaban el estado del juego y renderizaban lo más rápido que permitía el hardware, sin tener en cuenta cuánto tiempo había transcurrido. Lo que significa que tan pronto como cambia el hardware, cambia la jugabilidad.
En general, queremos que nuestros jugadores tengan una experiencia consistente y una sensación de juego en una variedad de dispositivos (siempre que cumplan con algunas especificaciones mínimas) ya sea que estén usando el teléfono del año pasado o el modelo más nuevo, una computadora de escritorio de juegos de alta gama o un Portátil de nivel medio.
En particular, para los juegos que son competitivos (ya sea multijugador o tablas de clasificación) no queremos que los jugadores que se ejecutan en un dispositivo en particular tengan una ventaja sobre otros porque pueden correr más rápido o tener más tiempo para reaccionar.
La solución segura aquí es bloquear la velocidad a la que hacemos actualizaciones de estado de juego. De esa manera podemos garantizar que los resultados siempre serán los mismos.
2. Entonces, ¿por qué no simplemente bloquear la velocidad de fotogramas (p. Ej., Usando VSync) y aún ejecutar las actualizaciones de estado del juego y la representación en bloque?
Esto puede funcionar, pero no siempre es agradable al público. Hubo mucho tiempo en que correr a 30 fps fue considerado el estándar de oro para los juegos. Ahora, los jugadores esperan habitualmente 60 fps como la barra mínima, especialmente en juegos de acción multijugador, y algunos títulos más antiguos ahora se ven notablemente entrecortados ya que nuestras expectativas han cambiado. También hay un grupo vocal de jugadores de PC en particular que se oponen a los bloqueos de velocidad de fotogramas. Pagaron mucho por su hardware de última generación y quieren poder usar ese músculo informático para obtener el renderizado más suave y de mayor fidelidad que sea capaz.
En realidad, en realidad, la velocidad de fotogramas es el rey, y el estándar sigue aumentando progresivamente. Al principio del reciente resurgimiento de la realidad virtual, los juegos a menudo corrían alrededor de 60 fps. Ahora 90 es más estándar, y el hardware como el PSVR está comenzando a admitir 120. Esto puede seguir aumentando aún. Entonces, si un juego de realidad virtual limita su velocidad de cuadros a lo que es factible y aceptado hoy, es probable que se quede atrás a medida que el hardware y las expectativas se desarrollen aún más.
(Como regla, tenga cuidado cuando le digan "los jugadores no pueden percibir nada más rápido que XXX", ya que generalmente se basa en un tipo particular de "percepción", como reconocer un cuadro en secuencia. La percepción de la continuidad del movimiento es generalmente mucho más sensible. )
El último problema aquí es que un juego que usa una velocidad de fotogramas bloqueada también debe ser conservador: si alguna vez encuentras un momento en el juego en el que estás actualizando y mostrando una cantidad inusualmente alta de objetos, no querrás perderte tu fotograma. fecha límite y causar un tartamudeo o enganche notable. Por lo tanto, debe establecer sus presupuestos de contenido lo suficientemente bajos como para dejar margen de maniobra, o invertir en funciones de ajuste de calidad dinámica más complicadas para evitar vincular toda la experiencia de juego al rendimiento en el peor de los casos en hardware de especificaciones mínimas.
Esto puede ser especialmente problemático si los problemas de rendimiento aparecen tarde en el desarrollo, cuando todos sus sistemas existentes se construyen y ajustan suponiendo una velocidad de fotogramas de representación que ahora no siempre se puede alcanzar. El desacoplamiento de las tasas de actualización y representación brinda más flexibilidad para lidiar con la variabilidad del rendimiento.
3. ¿La actualización en un intervalo de tiempo fijo no tiene los mismos problemas que (2)?
Creo que este es el meollo de la pregunta original: si desacoplamos nuestras actualizaciones y, a veces, procesamos dos cuadros sin actualizaciones de estado del juego en el medio, entonces no es lo mismo que la representación de bloqueo a una velocidad de cuadros más baja, ya que no hay un cambio visible en ¿la pantalla?
En realidad, hay varias maneras diferentes en las que los juegos usan el desacoplamiento de estas actualizaciones con buenos resultados:
a) La velocidad de actualización puede ser más rápida que la velocidad de fotogramas renderizada
Como señala tyjkenn en otra respuesta, la física en particular a menudo se escalona a una frecuencia más alta que la representación, lo que ayuda a minimizar los errores de integración y proporciona colisiones más precisas. Entonces, en lugar de tener 0 o 1 actualizaciones entre cuadros renderizados, puede tener 5 o 10 o 50.
Ahora, el reproductor que renderiza a 120 fps puede obtener 2 actualizaciones por cuadro, mientras que el jugador con un rendimiento de hardware de menor especificación a 30 fps obtiene 8 actualizaciones por cuadro, y ambos juegos se ejecutan a la misma velocidad de juego-ticks-por-tiempo real-segundo. El mejor hardware hace que parezca más fluido, pero no altera radicalmente el funcionamiento del juego.
Aquí existe el riesgo de que, si la velocidad de actualización no coincide con la velocidad de fotogramas, puede obtener una "frecuencia de latido" entre los dos . P.ej. En la mayoría de los cuadros tenemos tiempo suficiente para 4 actualizaciones de estado del juego y un poco de sobra, y de vez en cuando tenemos suficiente guardado para hacer 5 actualizaciones en un cuadro, haciendo un pequeño salto o tartamudeo en el movimiento. Esto puede ser abordado por ...
b) Interpolar (o extrapolar) el estado del juego entre actualizaciones
Aquí a menudo dejaremos que el estado del juego viva un paso de tiempo fijo en el futuro, y almacenaremos suficiente información de los 2 estados más recientes para que podamos representar un punto arbitrario entre ellos. Luego, cuando estamos listos para mostrar un nuevo cuadro en pantalla, nos mezclamos con el momento apropiado solo para mostrar (es decir, no modificamos el estado de juego subyacente aquí)
Cuando se hace bien, esto hace que el movimiento se sienta suave e incluso ayuda a enmascarar alguna fluctuación en la velocidad de fotogramas, siempre que no bajemos demasiado .
c) Agregar suavidad a los cambios de estado que no son del juego
Incluso sin interpolar el estado del juego, aún podemos obtener algunas victorias de suavidad.
Los cambios puramente visuales como la animación de personajes, los sistemas de partículas o VFX, y los elementos de la interfaz de usuario como HUD, a menudo se actualizan por separado del paso de tiempo fijo del estado de juego. Esto significa que si estamos marcando nuestro estado de juego varias veces por cuadro, no estamos pagando su costo con cada marca, solo en el pase de render final. En cambio, escalamos la velocidad de reproducción de estos efectos para que coincida con la longitud del cuadro, para que se reproduzcan tan suavemente como lo permita la velocidad de fotogramas de renderizado, sin afectar la velocidad del juego o la equidad como se discutió en (1).
El movimiento de la cámara también puede hacer esto, especialmente en VR, a veces mostraremos el mismo cuadro más de una vez, pero lo reproyectaremos para tener en cuenta el movimiento de la cabeza del jugador en el medio , para que podamos mejorar la latencia y la comodidad percibidas, incluso si podemos No renderizar todo de forma nativa tan rápido. Algunos sistemas de transmisión de juegos (donde el juego se ejecuta en un servidor y el jugador solo ejecuta un cliente ligero) también usan una versión de este.
4. ¿Por qué no usar ese estilo (c) para todo? Si funciona para la animación y la interfaz de usuario, ¿no podemos simplemente escalar nuestras actualizaciones de estado de juego para que coincidan con la velocidad de fotogramas actual?
Sí * esto es posible, pero no, no es simple.
Esta respuesta ya es un poco larga, así que no entraré en todos los detalles sangrientos, solo un resumen rápido:
Multiplicar por
deltaTime
trabajos para ajustarse a actualizaciones de longitud variable para un cambio lineal (por ejemplo, movimiento con velocidad constante, cuenta regresiva de un temporizador o progreso a lo largo de una línea de tiempo de animación)Desafortunadamente, muchos aspectos de los juegos no son lineales . Incluso algo tan simple como la gravedad exige técnicas de integración más sofisticadas o subpasos de mayor resolución para evitar resultados divergentes con velocidades de cuadro variables. La entrada y el control del jugador es en sí misma una gran fuente de no linealidad.
En particular, los resultados de la detección y resolución discretas de colisiones dependen de la velocidad de actualización, lo que lleva a errores de tunelado y fluctuación de fase si los cuadros se alargan demasiado. Por lo tanto, una velocidad de fotogramas variable nos obliga a usar métodos de detección de colisión continua más complejos / costosos en más de nuestro contenido, o tolerar la variabilidad en nuestra física. Incluso la detección de colisión continua se enfrenta a desafíos cuando los objetos se mueven en arcos, lo que requiere pasos de tiempo más cortos ...
Por lo tanto, en el caso general de un juego de complejidad media, mantener un comportamiento coherente y equitativo por completo a través de la
deltaTime
escala es algo muy difícil y de mantenimiento intensivo a absoluto inviable.La estandarización de una tasa de actualización nos permite garantizar un comportamiento más consistente en una variedad de condiciones , a menudo con un código más simple.
Mantener esa tasa de actualización desacoplada del renderizado nos da flexibilidad para controlar la suavidad y el rendimiento de la experiencia sin alterar la lógica del juego .
Incluso entonces , nunca obtenemos una independencia de framerate verdaderamente "perfecta", pero al igual que muchos enfoques en los juegos, nos da un método controlable para marcar "lo suficientemente bueno" para las necesidades de un juego determinado. Es por eso que comúnmente se enseña como un punto de partida útil.
fuente
read-update-render
, nuestra peor latencia es de 17 ms (ignorando la canalización de gráficos y la latencia de visualización por ahora). Con un(read-update)x(n>1)-render
bucle desacoplado a la misma velocidad de cuadro, nuestra latencia en el peor de los casos solo puede ser igual o mejor porque verificamos y actuamos en la entrada con tanta frecuencia o más. :)Las otras respuestas son buenas y hablan de por qué existe el bucle del juego y deben separarse del bucle de renderizado. Sin embargo, en cuanto al ejemplo específico de "¿Por qué renderizar un marco cuando no ha habido ningún cambio?" Realmente se reduce a hardware y complejidad.
Las tarjetas de video son máquinas de estado y son realmente buenas para hacer lo mismo una y otra vez. Si solo representa cosas que han cambiado, en realidad es más costoso, no menos. En la mayoría de los escenarios, no hay mucho de nada que sea estático, si te mueves ligeramente a la izquierda en un juego de FPS, has cambiado los datos de píxeles del 98% de las cosas en la pantalla, también podrías renderizar todo el cuadro.
Pero principalmente, complejidad. Hacer un seguimiento de todo lo que ha cambiado mientras se realiza una actualización es mucho más costoso porque debe reelaborar todo o realizar un seguimiento del resultado anterior de algún algoritmo, compararlo con el nuevo resultado y solo renderizar ese píxel si el cambio es diferente. Depende del sistema.
El diseño del hardware, etc., está optimizado en gran medida para las convenciones actuales, y una máquina de estado ha sido un buen modelo para comenzar.
fuente
La representación suele ser el proceso más lento en el ciclo del juego. Los humanos no notan fácilmente una diferencia en una velocidad de fotogramas superior a 60, por lo que a menudo es menos importante perder tiempo en renderizar más rápido que eso. Sin embargo, hay otros procesos que se beneficiarían más de una tasa más rápida. La física es una. Un cambio demasiado grande en un bucle puede hacer que los objetos pasen por encima de las paredes. Puede haber formas de evitar simples errores de colisión en incrementos más grandes, pero para muchas interacciones físicas complejas, simplemente no obtendrá la misma precisión. Sin embargo, si el bucle de física se ejecuta con mayor frecuencia, hay menos posibilidades de fallas, ya que los objetos se pueden mover en incrementos más pequeños sin renderizarse cada vez. Se destinan más recursos al motor de física sensible y se desperdicia menos en dibujar más cuadros que el usuario no puede ver.
Esto es especialmente importante en los juegos más intensivos en gráficos. Si hubiera un render para cada bucle de juego, y un jugador no tuviera la máquina más poderosa, puede haber puntos en el juego en los que el fps caiga a 30 o 40. Si bien esta sería una velocidad de fotogramas no del todo horrible, el juego comenzaría a ser bastante lento si intentáramos mantener cada cambio de física razonablemente pequeño para evitar problemas técnicos. Al jugador le molestaría que su personaje camine solo la mitad de la velocidad normal. Sin embargo, si la velocidad de renderizado fuera independiente del resto del ciclo, el jugador podría mantenerse a una velocidad de caminata fija a pesar de la caída en la velocidad de fotogramas.
fuente
Una construcción como la de su pregunta puede tener sentido si el subsistema de representación tiene alguna noción de "tiempo transcurrido desde la última representación" .
Considere, por ejemplo, un enfoque en el que la posición de un objeto en el mundo del juego se representa a través de
(x,y,z)
coordenadas fijas con un enfoque que además almacena el vector de movimiento actual(dx,dy,dz)
. Ahora, podría escribir su ciclo de juego para que el cambio de posición tenga que ocurrir en elupdate
método, pero también podría diseñarlo para que el cambio de movimiento tenga lugar duranteupdate
. Con el último enfoque, a pesar de que su estado de juego en realidad no cambiará hasta el próximoupdate
, unrender
-función que se llama a una frecuencia más alta ya podría dibujar el objeto en una posición ligeramente actualizada. Si bien esto técnicamente conduce a una discrepancia entre lo que ves y lo que se representa internamente, la diferencia es lo suficientemente pequeña como para no importar la mayoría de los aspectos prácticos, pero permite que las animaciones se vean mucho más suaves.Predecir "el futuro" de su estado de juego (a pesar del riesgo de estar equivocado) puede ser una buena idea cuando tiene en cuenta, por ejemplo, las latencias de entrada de red.
fuente
Además de otras respuestas ...
Verificar el cambio de estado requiere un procesamiento significativo. Si se necesita un tiempo de procesamiento similar (¡o más!) Para verificar los cambios, en comparación con el procesamiento real, realmente no ha mejorado la situación. En el caso de renderizar una imagen, como dice @Waddles, una tarjeta de video es realmente buena para hacer lo mismo una y otra vez, y es más costoso verificar cada fragmento de datos en busca de cambios que simplemente transferirlo. a la tarjeta de video para su procesamiento. Además, si el renderizado es un juego, es muy poco probable que la pantalla no haya cambiado en el último tic.
También está asumiendo que el procesamiento requiere un tiempo de procesador significativo. Esto depende mucho de su procesador y tarjeta gráfica. Durante muchos años, la atención se ha centrado en descargar progresivamente un trabajo de renderizado cada vez más sofisticado en la tarjeta gráfica y reducir la entrada de renderizado necesaria del procesador. Idealmente, la
render()
llamada del procesador debería simplemente configurar una transferencia DMA y eso es todo. Luego, la transferencia de datos a la tarjeta gráfica se delega al controlador de memoria, y la producción de la imagen se delega a la tarjeta gráfica. Pueden hacerlo en su propio tiempo, mientras que el procesador en paralelocontinúa con la física, el motor de juego y todas las demás cosas que un procesador hace mejor. Obviamente, la realidad es mucho más complicada que eso, pero poder descargar el trabajo a otras partes del sistema también es un factor importante.fuente