La velocidad de fotogramas afecta la velocidad del objeto

9

Estoy experimentando con la construcción de un motor de juego desde cero en Java, y tengo un par de preguntas. Mi bucle principal del juego se ve así:

        int FPS = 60;
        while(isRunning){
            /* Current time, before frame update */
            long time = System.currentTimeMillis();
            update();
            draw();
            /* How long each frame should last - time it took for one frame */
            long delay = (1000 / FPS) - (System.currentTimeMillis() - time);
            if(delay > 0){
                try{
                    Thread.sleep(delay);
                }catch(Exception e){};
            }
        }

Como puede ver, he establecido la velocidad de fotogramas en 60FPS, que se utiliza en el delaycálculo. El retraso asegura que cada cuadro tome la misma cantidad de tiempo antes de procesar el siguiente. En mi update()función hago lo x++que aumenta el valor horizontal de un objeto gráfico que dibujo con lo siguiente:

bbg.drawOval(x,40,20,20);

Lo que me confunde es la velocidad. cuando configuré FPS150, el círculo procesado atraviesa la velocidad realmente rápido, mientras que establece FPS30 movimientos en la pantalla a la mitad de la velocidad. ¿La velocidad de fotogramas no solo afecta la "suavidad" del renderizado y no la velocidad de los objetos que se renderizan? Creo que me falta una gran parte, me encantaría algunas aclaraciones.

Carpetfizz
fuente
44
Aquí hay un buen artículo sobre el bucle del juego: arregle su paso de tiempo
Kostya Regent
2
Como nota al margen, generalmente intentamos poner cosas que no se deben realizar en cada bucle fuera de los bucles. En su código, su 1000 / FPSdivisión podría hacerse y el resultado asignado a una variable antes de su while(isRunning)ciclo. Esto ayuda a ahorrar un par de instrucciones de CPU para hacer algo más de una vez inútilmente.
Vaillancourt

Respuestas:

21

Estás moviendo el círculo un píxel por fotograma. No debería ser una gran sorpresa que, si su ciclo de renderizado se ejecuta a 30 FPS, su círculo se moverá 30 a píxeles por segundo.

Básicamente tiene tres formas posibles de tratar este problema:

  1. Simplemente elija una velocidad de fotogramas y manténgala. Eso es lo que hicieron muchos juegos de la vieja escuela: se ejecutaban a una velocidad fija de 50 o 60 FPS, generalmente sincronizados con la frecuencia de actualización de la pantalla, y simplemente diseñaban su lógica de juego para hacer todo lo necesario dentro de ese intervalo de tiempo fijo. Si, por alguna razón, eso no sucediera, el juego solo tendría que saltarse un cuadro (o posiblemente bloquearse), disminuyendo efectivamente tanto el dibujo como la física del juego a la mitad de la velocidad.

    En particular, los juegos que las funciones más utilizadas, como la detección de colisiones de sprites de hardware más o menos tenían que obra como esta, porque su lógica del juego estaba íntimamente ligada a la prestación, que se hizo en el hardware a una tasa fija.

  2. Usa un paso de tiempo variable para la física de tu juego. Básicamente, esto significa reescribir su ciclo de juego para que se vea así:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        float timestep = 0.001 * (time - lastTime);  // in seconds
        if (timestep <= 0 || timestep > 1.0) {
            timestep = 0.001;  // avoid absurd time steps
        }
        update(timestep);
        draw();
        // ... sleep until next frame ...
        lastTime = time;
    }

    y, dentro update(), ajustando las fórmulas físicas para tener en cuenta el paso de tiempo variable, por ejemplo, así:

    speed += timestep * acceleration;
    position += timestep * (speed - 0.5 * timestep * acceleration);

    Un problema con este método es que puede ser complicado mantener la física (en su mayoría) independiente del paso de tiempo ; realmente no quieres que la distancia que los jugadores pueden saltar dependa de su velocidad de fotogramas. La fórmula que mostré anteriormente funciona bien para una aceleración constante, por ejemplo, bajo gravedad (y la que está en la publicación vinculada funciona bastante bien incluso si la aceleración varía con el tiempo), pero incluso con las fórmulas físicas más perfectas posibles, es probable que trabajar con flotadores produce un poco de "ruido numérico" que, en particular, puede hacer que las repeticiones exactas sean imposibles. Si eso es algo que cree que podría desear, puede preferir los otros métodos.

  3. Desacople la actualización y dibuje los pasos. Aquí, la idea es que actualices el estado de tu juego usando un paso de tiempo fijo, pero ejecutes un número variable de actualizaciones entre cada cuadro. Es decir, su ciclo de juego podría verse así:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        if (time - lastTime > 1000) {
            lastTime = time;  // we're too far behind, catch up
        }
        int updatesNeeded = (time - lastTime) / updateInterval;
        for (int i = 0; i < updatesNeeded; i++) {
            update();
            lastTime += updateInterval;
        }
        draw();
        // ... sleep until next frame ...
    }

    Para hacer que el movimiento percibido sea más suave, es posible que también desee que su draw()método interpole cosas como las posiciones de los objetos sin problemas entre los estados del juego anterior y siguiente. Esto significa que debe pasar el desplazamiento de interpolación correcto al draw()método, por ejemplo, así:

        int remainder = (time - lastTime) % updateInterval;
        draw( (float)remainder / updateInterval );  // scale to 0.0 - 1.0

    También necesitaría que su update()método realmente calcule el estado del juego un paso adelante (o posiblemente varios, si desea hacer una interpolación de splines de orden superior), y que guarde las posiciones de objetos anteriores antes de actualizarlas, para que el draw()método pueda interpolar entre ellos. (También es posible extrapolar las posiciones pronosticadas en función de las velocidades y aceleraciones de los objetos, pero esto puede parecer desigual, especialmente si los objetos se mueven de manera complicada, lo que hace que las predicciones a menudo fallen).

    Una ventaja de la interpolación es que, para algunos tipos de juegos, puede permitirle reducir significativamente la tasa de actualización de la lógica del juego, mientras mantiene una ilusión de movimiento suave. Por ejemplo, es posible que pueda actualizar su estado de juego solo, por ejemplo, 5 veces por segundo, mientras sigue dibujando de 30 a 60 cuadros interpolados por segundo. Al hacer esto, también puede considerar intercalar la lógica de su juego con el dibujo (es decir, tener un parámetro para su update()método que le indique que solo ejecute x % de una actualización completa antes de regresar), y / o ejecutar la física del juego / lógica y el código de renderizado en hilos separados (¡cuidado con las fallas de sincronización!).

Por supuesto, también es posible combinar estos métodos de varias maneras. Por ejemplo, en un juego multijugador cliente-servidor, es posible que el servidor (que no necesita dibujar nada) ejecute sus actualizaciones en un paso de tiempo fijo (para una física consistente y una capacidad de reproducción exacta), mientras hace que el cliente realice actualizaciones predictivas (para ser anulado por el servidor, en caso de desacuerdo) en un intervalo de tiempo variable para un mejor rendimiento. También es posible mezclar de manera útil la interpolación y las actualizaciones de paso de tiempo variable; por ejemplo, en el escenario cliente-servidor que se acaba de describir, realmente no tiene mucho sentido que el cliente use tiempos de actualización más cortos que el servidor, por lo que puede establecer un límite inferior en el paso de tiempo del cliente e interpolar en la etapa de dibujo para permitir mayores FPS

(Editar: Código agregado para evitar intervalos / recuentos de actualizaciones absurdos, en caso de que, por ejemplo, la computadora esté temporalmente suspendida o congelada durante más de un segundo mientras se ejecuta el ciclo del juego. Gracias a Mooing Duck por recordarme la necesidad de eso .)

Ilmari Karonen
fuente
1
Muchas gracias por tomarse el tiempo para responder mi pregunta, realmente lo aprecio. Realmente me gusta el enfoque # 3, tiene más sentido para mí. Dos preguntas, ¿cuál es el intervalo de actualización definido por y por qué se divide por él?
Carpetfizz
1
@Carpetfizz: updateIntervales solo la cantidad de milisegundos que desea entre las actualizaciones de estado del juego. Para, digamos, 10 actualizaciones por segundo, establecerías updateInterval = (1000 / 10) = 100.
Ilmari Karonen
1
currentTimeMillisNo es un reloj monótono. En su nanoTimelugar, úselo, a menos que desee que la sincronización de tiempo de la red altere la velocidad de las cosas en su juego.
user253751
@MooingDuck: Bien visto. Ya lo arreglé, creo. ¡Gracias!
Ilmari Karonen
@IlmariKaronen: En realidad, mirando el código, podría ser más simple simplemente while(lastTime+=updateInterval <= time). Sin embargo, eso es solo un pensamiento, no una corrección.
Mooing Duck el
7

Su código se está ejecutando cada vez que se procesa un marco. Si la velocidad de fotogramas es mayor o menor que la velocidad de fotogramas especificada, sus resultados cambiarían ya que las actualizaciones no tienen el mismo tiempo.

Para resolver esto, debe consultar Delta Timing .

El propósito de Delta Timing es eliminar los efectos del retraso en las computadoras que intentan manejar gráficos complejos o una gran cantidad de código, sumando la velocidad de los objetos para que eventualmente se muevan a la misma velocidad, independientemente del retraso.

Para hacer esto:

Se realiza llamando a un temporizador cada fotograma por segundo que contiene el tiempo entre ahora y la última llamada en milisegundos.

Luego deberá multiplicar el tiempo delta por el valor que desea cambiar por tiempo. Por ejemplo:

distanceTravelledSinceLastFrame = Speed * DeltaTime
Estático
fuente
3
Además, ponga límites a los plazos mínimos y máximos. Si la computadora hiberna y luego se reanuda, no desea que las cosas se inicien fuera de la pantalla. Si aparece un milagro y time()devuelve el mismo dos veces, no desea errores div / 0 y procesamiento desperdiciado.
Mooing Duck
@MooingDuck: Ese es un muy buen punto. He editado mi propia respuesta para reflejarlo. (Por lo general, no deberías dividir nada por el paso de tiempo en una actualización de estado de juego típica, por lo que un paso de tiempo cero debería ser seguro, pero permitirlo agrega una fuente adicional de errores potenciales para poca o ninguna ganancia, y por lo tanto debería ser evitado.)
Ilmari Karonen
5

Esto se debe a que limita su velocidad de fotogramas, pero solo realiza una actualización por fotograma. Así que supongamos que el juego se ejecuta en el objetivo de 60 fps, obtienes 60 actualizaciones lógicas por segundo. Si la velocidad de cuadros baja a 15 fps, solo tendría 15 actualizaciones lógicas por segundo.

En su lugar, intente acumular el tiempo de fotograma pasado hasta ahora y luego actualice la lógica de su juego una vez por cada intervalo de tiempo que haya pasado, por ejemplo, para ejecutar su lógica a 100 fps, ejecutaría la actualización una vez por cada 10 ms acumulados (y reste esos mostrador).

Agregue una alternativa (mejor para imágenes) actualice su lógica en función del tiempo transcurrido.

Mario
fuente
1
es decir, actualización (elapsedSeconds);
Jon
2
Y dentro, posición + = velocidad * elapsedSeconds;
Jon