¿Cuál es el punto de actualización de renderizado independiente en un bucle de juego?

74

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?

Sam
fuente
66
Personalmente, encontré gameprogrammingpatterns.com/game-loop.html una explicación útil
Niels
12
No todos los cambios en la representación se reflejan en el estado del juego. Y sospecho que no entiendes el punto de ese fragmento de código: te permite actualizar varias veces por render, no renderizar varias veces por actualización.
Luaan
13
Tenga en cuenta que el código lee while (isTimeForUpdate), no if (isTimeForUpdate). El objetivo principal no es render()cuando no ha habido un update(), sino update()repetidamente entre render()s. Independientemente, ambas situaciones tienen usos válidos. El primero sería válido si el estado puede cambiar fuera de su updatefunció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.
Thierry
Una pregunta más lógica sería "¿cuál es el punto del bucle del juego de renderizado dependiente de la actualización "
User1306322
1
¿Con qué frecuencia tiene una actualización que no hace nada? ¿Ni siquiera actualizas animaciones de fondo o relojes en pantalla?
pjc50

Respuestas:

113

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 deltaTimetrabajos 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 deltaTimeescala 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.

DMGregory
fuente
2
En los casos en que todo usará la misma velocidad de fotogramas, la sincronización de todo puede minimizar el retraso entre el momento en que se muestrea el controlador y cuando el estado del juego reacciona. Para muchos juegos en algunas máquinas más antiguas, el peor de los casos sería menos de 17 ms (los controles se leen al comienzo del espacio en blanco vertical, luego se aplican los cambios de estado del juego y se procesa el siguiente cuadro mientras el rayo se mueve hacia abajo de la pantalla) . Desacoplar las cosas a menudo resultará en un aumento significativo en el peor de los casos.
supercat
3
Si bien es cierto que las canalizaciones de actualización más complicadas facilitan la introducción involuntaria de latencia, no es una consecuencia necesaria del enfoque desacoplado cuando se implementa correctamente. De hecho, incluso podemos reducir la latencia. Tomemos un juego que rinde a 60 fps. Con un paso cerrado 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)-renderbucle 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. :)
DMGregory
3
En una nota al margen interesante sobre los juegos antiguos que no tienen en cuenta el tiempo real transcurrido, el arcade original de Space Invaders tenía un problema técnico causado por el poder de renderizado, donde a medida que el jugador destruía las naves enemigas, el renderizado y, por lo tanto, las actualizaciones del juego, se aceleraban, lo que resultaba, accidentalmente , en la curva de dificultad icónica del juego.
Oskuro
1
@DMGregory: si las cosas se hacen de forma asincrónica, entonces será posible que ocurra un cambio de control justo después de que el ciclo del juego sondee los controles, el ciclo del evento del juego después del actual finalice justo después de que el ciclo de representación tome el estado del juego, y para que el bucle de renderizado después del actual termine justo después de que el sistema de salida de video tome el búfer de cuadro actual, por lo que el peor de los casos termina siendo apenas dos tiempos de bucle de juego más dos tiempos de renderizado más dos períodos de cuadro. Sincronizar las cosas correctamente podría reducirlo a la mitad.
supercat
1
@Oskuro: Eso no fue un problema técnico. La velocidad del ciclo de actualización permanece constante independientemente de cuántos invasores haya en la pantalla, pero el juego no atrae a todos los invasores en cada ciclo de actualización.
supercat
8

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.

Waddles
fuente
55
Aquí se debe hacer una distinción entre renderizar (todo) solo cuando algo ha cambiado (sobre qué pregunta la pregunta) y renderizar solo las partes que han cambiado (lo que describe su respuesta).
DMGregory
Eso es cierto, y traté de hacer esa distinción entre el primer y el segundo párrafo. Es raro que un marco no cambie en absoluto, así que pensé que era importante tomar esta vista junto con su respuesta integral.
Waddles
Además de esto, señalaría que no hay razón para no renderizar. Usted sabe que siempre tiene tiempo para sus llamadas de renderizado en cada fotograma (¡es mejor que sepa que siempre tiene tiempo para sus llamadas de renderizado en cada fotograma!), Por lo que hay muy poco daño al hacer un renderizado 'innecesario', especialmente porque este caso esencialmente nunca aparece en la práctica.
Steven Stadnicki
@StevenStadnicki Es cierto que no hay un gran costo de tiempo para renderizar todo en cada fotograma (ya que necesita tener tiempo en su presupuesto para hacerlo de todos modos cada vez que cambian muchos estados a la vez). Pero para dispositivos portátiles como computadoras portátiles, tabletas, teléfonos o sistemas de juegos portátiles, podría ser considerado evitar el renderizado redundante para hacer un uso eficiente de la batería del reproductor . Esto se aplica principalmente a los juegos pesados ​​en la interfaz de usuario en los que grandes partes de la pantalla pueden permanecer sin cambios para los marcos entre las acciones del jugador, y no siempre será práctico de implementar dependiendo de la arquitectura del juego.
DMGregory
6

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.

tyjkenn
fuente
1
¿O, por ejemplo, medir el tiempo entre ticks y calcular qué tan lejos debe llegar el personaje en ese tiempo? ¡Los días de las animaciones de sprites fijos ya pasaron!
Graham
2
Los humanos no pueden percibir las cosas a más de 60 fps sin alias temporal, pero la velocidad de fotogramas necesaria para lograr un movimiento suave en ausencia de desenfoque de movimiento puede ser mucho mayor que eso. Más allá de cierta velocidad, una rueda giratoria debería aparecer como un desenfoque, pero si el software no aplica el desenfoque de movimiento y la rueda gira más de medio radio por cuadro, la rueda puede verse mal incluso si la velocidad de fotogramas fuera de 1000 fps.
supercat
1
@Graham, entonces te encuentras con el problema desde mi primer párrafo, donde cosas como la física se portan mal con el cambio más grande por tic. Si la velocidad de fotogramas baja lo suficiente, compensar con cambios más grandes podría hacer que un jugador corra a través de una pared, perdiendo por completo su cuadro de golpe.
tyjkenn
1
Los humanos generalmente no pueden percibir más rápido que 60 fps, por lo que no tiene sentido desperdiciar recursos en renderizar más rápido que eso. Estoy en desacuerdo con esa declaración. Los VR HMD se procesan a 90Hz, y está subiendo. Créame cuando le digo que CIERTAMENTE puede percibir la diferencia entre 90Hz y 60Hz en un auricular. Además, últimamente he visto tantos juegos vinculados a la CPU como a la GPU. Decir que "la representación suele ser el proceso más lento" no es necesariamente cierto.
3Dave
@DavidLively, por lo que "sin sentido" puede haber sido demasiado exagerado. Lo que quise decir es que el renderizado tiende a ser un cuello de botella, y la mayoría de los juegos se ven bien a 60 fps. Ciertamente, hay efectos que son importantes en algunos tipos de juegos, como con VR, que solo puedes obtener con velocidades de cuadro más rápidas, pero esos parecen ser la excepción en lugar de la norma. Si estuviera ejecutando un juego en una máquina más lenta, preferiría tener una física funcional que una velocidad de fotogramas rápida apenas notable.
tyjkenn
4

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 el updatemétodo, pero también podría diseñarlo para que el cambio de movimiento tenga lugar durante update. Con el último enfoque, a pesar de que su estado de juego en realidad no cambiará hasta el próximo update, 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.

Thomas
fuente
4

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.

Graham
fuente