El movimiento parece depender de la velocidad de fotogramas, a pesar del uso de Time.deltaTime

13

Tengo el siguiente código para calcular la traducción requerida para mover un objeto de juego en Unity, que se llama LateUpdate. Por lo que entiendo, mi uso de Time.deltaTimedebería hacer que la velocidad de fotogramas de traducción final sea independiente (tenga en cuenta CollisionDetection.Move()que solo está realizando transmisiones de rayos).

public IMovementModel Move(IMovementModel model) {    
    this.model = model;

    targetSpeed = (model.HorizontalInput + model.VerticalInput) * model.Speed;

    model.CurrentSpeed = accelerateSpeed(model.CurrentSpeed, targetSpeed,
        model.Accel);

    if (model.IsJumping) {
        model.AmountToMove = new Vector3(model.AmountToMove.x,
            model.AmountToMove.y);
    } else if (CollisionDetection.OnGround) {
        model.AmountToMove = new Vector3(model.AmountToMove.x, 0);
    }

    model.FlipAnim = flipAnimation(targetSpeed);
    // If we're ignoring gravity, then just use the vertical input.
    // if it's 0, then we'll just float.
    gravity = model.IgnoreGravity ? model.VerticalInput : 40f;

    model.AmountToMove = new Vector3(model.CurrentSpeed, model.AmountToMove.y - gravity * Time.deltaTime);

    model.FinalTransform =
        CollisionDetection.Move(model.AmountToMove * Time.deltaTime,
            model.BoxCollider.gameObject, model.IgnorePlayerLayer);
    // Prevent the entity from moving too fast on the y-axis.
    model.FinalTransform = new Vector3(model.FinalTransform.x,
        Mathf.Clamp(model.FinalTransform.y, -1.0f, 1.0f),
        model.FinalTransform.z);

    return model;
}

private float accelerateSpeed(float currSpeed, float target, float accel) {
    if (currSpeed == target) {
        return currSpeed;
    }
    // Must currSpeed be increased or decreased to get closer to target
    float dir = Mathf.Sign(target - currSpeed);
    currSpeed += accel * Time.deltaTime * dir;
    // If currSpeed has now passed Target then return Target, otherwise return currSpeed
    return (dir == Mathf.Sign(target - currSpeed)) ? currSpeed : target;
}

private void OnMovementCalculated(IMovementModel model) {
    transform.Translate(model.FinalTransform);
}

Si bloqueo el framerate del juego a 60FPS, mis objetos se mueven como se esperaba. Sin embargo, si lo desbloqueo ( Application.targetFrameRate = -1;), algunos objetos se moverán a una velocidad mucho más lenta de lo que esperaría cuando logre ~ 200FPS en un monitor de 144hz. Esto solo parece suceder en una compilación independiente, y no dentro del editor de Unity.

GIF de movimiento de objetos dentro del editor, FPS desbloqueado

http://gfycat.com/SmugAnnualFugu

GIF de movimiento de objetos dentro de la construcción independiente, FPS desbloqueado

http://gfycat.com/OldAmpleJuliabutterfly

cobre
fuente
2
Deberías leer esto. ¡Lo que quieres es tiempo de bucketing y pasos de tiempo fijos! gafferongames.com/game-physics/fix-your-timestep
Alan Wolfe

Respuestas:

30

Las simulaciones basadas en marcos experimentarán errores cuando las actualizaciones no puedan compensar las tasas de cambio no lineales.

Por ejemplo, considere un objeto que comienza con valores de posición y velocidad cero experimentando una aceleración constante de uno.

Si aplicamos esta lógica de actualización:

velocity += acceleration * elapsedTime
position += velocity * elapsedTime

Podemos esperar estos resultados bajo diferentes velocidades de cuadros: ingrese la descripción de la imagen aquí

El error se produce al tratar la velocidad final como si se aplicara a todo el cuadro. Esto es similar a una suma de Riemann correcta y la cantidad de error varía con la velocidad de fotogramas (ilustrada en una función diferente):

Como señala MichaelS , este error se reducirá a la mitad cuando la duración del cuadro se reduzca a la mitad y puede volverse intrascendente a velocidades de cuadro altas. Por otro lado, cualquier juego que experimente picos de rendimiento o cuadros de larga duración puede encontrar que esto produce un comportamiento impredecible.


Afortunadamente, la cinemática nos permite calcular con precisión el desplazamiento causado por la aceleración lineal:

d =  vᵢ*t + (a*t²)/2

where:
  d  = displacement
  v = initial velocity
  a  = acceleration
  t  = elapsed time

breakdown:
  vᵢ*t     = movement due to the initial velocity
  (a*t²)/2 = change in movement due to acceleration throughout the frame

Entonces, si aplicamos esta lógica de actualización:

position += (velocity * elapsedTime) + (acceleration * elapsedTime * elapsedTime / 2)
velocity += acceleration * elapsedTime

Tendremos los siguientes resultados:

ingrese la descripción de la imagen aquí

Kelly Thomas
fuente
2
Esta es información útil, pero ¿cómo aborda realmente el código en cuestión? Primero, el error disminuye dramáticamente a medida que aumenta la velocidad de fotogramas, por lo que la diferencia entre 60 y 200 fps es insignificante (8 fps vs infinito ya es solo un 12.5% ​​demasiado alto). En segundo lugar, una vez que el sprite está a toda velocidad, la mayor diferencia es estar 0.5 unidades por delante. No debería afectar la velocidad real de caminata como se muestra en los archivos adjuntos. Cuando giran, la aceleración es aparentemente instantánea (posiblemente varios cuadros a más de 60 fps, pero no segundos completos).
MichaelS
2
Ese es un problema de Unity o código, no un problema de matemáticas. Una hoja de cálculo rápida dice que si usamos a = 1, vi = 0, di = 0, vmax = 1, deberíamos golpear vmax en t = 1, con d = 0.5. Al hacer eso en 5 cuadros (dt = 0.2), d (t = 1) = 0.6. Más de 50 cuadros (dt = 0.02), d (t = 1) = 0.51. Más de 500 cuadros (dt = 0.002), d (t = 1) = 0.501. Entonces 5 fps es 20% alto, 50 fps es 2% alto y 500 fps es 0.2% alto. En general, el error es 100 / fps por ciento demasiado alto. 50 fps es aproximadamente 1.8% más alto que 500 fps. Y eso es solo durante la aceleración. Una vez que la velocidad alcanza el máximo, debería haber una diferencia cero. Con a = 100 y vmax = 5, debería haber incluso menos diferencia.
MichaelS
2
De hecho, seguí adelante y usé su código en una aplicación VB.net (simulando dt de 1/60 y 1/200), y obtuve Bounce: 5 en el cuadro 626 (10.433) segundos vs. Bounce: 5 en el cuadro 2081 ( 10.405) segundos . 0.27% más de tiempo a 60 fps.
MichaelS
2
Es su enfoque "cinemático" el que da una diferencia del 10%. El enfoque tradicional es la diferencia de 0.27%. Acabas de etiquetarlos incorrectamente. Creo que es porque incluyes incorrectamente la aceleración cuando la velocidad está al máximo. Las velocidades de fotogramas más altas agregan menos errores por fotograma, por lo que se obtiene un resultado más preciso. Es necesario if(velocity==vmax||velocity==-vmax){acceleration=0}. Luego, el error cae sustancialmente, aunque no es perfecto ya que no descubrimos exactamente qué parte de la aceleración del cuadro finalizó.
MichaelS
6

Depende de dónde llames tu paso. Si lo está llamando desde Update, su movimiento será independiente de la velocidad de fotogramas si escala con Time.deltaTime, pero si lo llama desde FixedUpdate, necesita escalar con Time.fixedDeltaTime. Me imagino que estás llamando a tu paso desde FixedUpdate, pero escalando con Time.deltaTime, lo que daría como resultado una velocidad aparente disminuida cuando el paso fijo de Unity es más lento que el bucle principal, que es lo que está sucediendo en tu construcción independiente. Cuando el paso fijo es lento, fixedDeltaTime es grande.

Nox
fuente
1
Se llama desde LateUpdate. Actualizaré mi pregunta para que quede claro. Aunque creo Time.deltaTimeque aún usará el valor correcto independientemente de dónde se llame (si se usa en FixedUpdate, usará fixedDeltaTime).
Cooper