O movimento parece depender da taxa de quadros, apesar do uso de Time.deltaTime

13

Eu tenho o código a seguir para calcular a tradução necessária para mover um objeto de jogo no Unity, que é chamado LateUpdate. Pelo que entendi, meu uso de Time.deltaTimedeve tornar a taxa de quadros de tradução final independente (observe CollisionDetection.Move()apenas o desempenho de raycasts).

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);
}

Se eu bloquear a taxa de quadros do jogo para 60FPS, meus objetos se moverão conforme o esperado. No entanto, se eu o desbloquear ( Application.targetFrameRate = -1;), alguns objetos se moverão a uma taxa muito mais lenta do que eu esperaria ao atingir ~ 200FPS em um monitor de 144 hz. Isso parece acontecer apenas em uma construção independente, e não no editor do Unity.

GIF do movimento do objeto dentro do editor, FPS desbloqueado

http://gfycat.com/SmugAnnualFugu

GIF de movimento de objetos na construção autônoma, FPS desbloqueado

http://gfycat.com/OldAmpleJuliabutterfly

Tanoeiro
fonte
2
Você deve ler isso. O intervalo de tempo é o que você deseja e etapas de tempo fixas! gafferongames.com/game-physics/fix-your-timestep #
Alan Wolfe

Respostas:

30

As simulações baseadas em quadros sofrerão erros quando as atualizações falharem em compensar taxas de mudança não lineares.

Por exemplo, considere um objeto começando com valores de posição e velocidade de zero experimentando uma aceleração constante de um.

Se aplicarmos essa lógica de atualização:

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

Podemos esperar estes resultados sob diferentes taxas de quadros: insira a descrição da imagem aqui

O erro é causado pelo tratamento da velocidade final como se ela fosse aplicada a todo o quadro. Isso é semelhante a uma soma direita de Riemann e a quantidade de erro varia com a taxa de quadros (ilustrada em uma função diferente):

Como o MichaelS aponta, esse erro será reduzido pela metade quando a duração do quadro for reduzida pela metade e poderá se tornar inconseqüente em altas taxas de quadros. Por outro lado, qualquer jogo com picos de desempenho ou quadros de execução prolongada pode achar que isso produz um comportamento imprevisível.


Felizmente, a cinemática nos permite calcular com precisão o deslocamento causado pela aceleração linear:

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

Portanto, se aplicarmos essa lógica de atualização:

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

Teremos os seguintes resultados:

insira a descrição da imagem aqui

Kelly Thomas
fonte
2
Essa é uma informação útil, mas como ela realmente trata o código em questão? Primeiro, o erro diminui drasticamente à medida que a taxa de quadros aumenta, portanto, a diferença entre 60 e 200 qps é insignificante (8 qps vs infinito já é apenas 12,5% alto demais). Segundo, quando o sprite estiver a toda velocidade, a maior diferença estará 0,5 unidades à frente. Não deve afetar a velocidade real de caminhada, conforme mostrado nos gifs anexados. Quando eles se viram, a aceleração é aparentemente instantânea (possivelmente vários quadros a mais de 60 qps, mas não segundos completos).
26616 MichaelS
2
Isso é uma questão de unidade ou código, não uma questão de matemática. Uma planilha rápida diz que, se usarmos a = 1, vi = 0, di = 0, vmax = 1, devemos atingir vmax em t = 1, com d = 0,5. Fazendo isso mais de 5 quadros (dt = 0,2), d (t = 1) = 0,6. Mais de 50 quadros (dt = 0,02), d (t = 1) = 0,51. Mais de 500 quadros (dt = 0,002), d (t = 1) = 0,501. Portanto, 5 fps tem 20% de altura, 50 fps é 2% de altura e 500 fps é 0,2% de altura. Em geral, o erro é 100 / fps por cento alto demais. 50 fps é cerca de 1,8% superior a 500 fps. E isso é apenas durante a aceleração. Quando a velocidade atingir o máximo, deve haver zero diferença. Com a = 100 evmax = 5, deve haver ainda menos diferença.
26616 MichaelS
2
Na verdade, fui adiante e usei seu código em um aplicativo VB.net (simulando dt de 1/60 e 1/200) e obtive Bounce: 5 no quadro 626 (10.433) segundos vs. Bounce: 5 no quadro 2081 ( 10,405) segundos . 0,27% mais tempo a 60 qps.
26616 MichaelS
2
É a sua abordagem "cinemática" que dá uma diferença de 10%. A abordagem tradicional é a diferença de 0,27%. Você apenas os rotulou incorretamente. Eu acho que é porque você está incluindo incorretamente a aceleração quando a velocidade é máxima. Maiores taxas de quadros adicionam menos erros por quadro, para fornecer um resultado mais preciso. Você precisa if(velocity==vmax||velocity==-vmax){acceleration=0}. Então, o erro diminui substancialmente, embora não seja perfeito, pois não descobrimos exatamente qual parte da aceleração do quadro terminou.
26616 MichaelS
6

Depende de onde você está chamando o seu passo. Se você está chamando de Atualização, seu movimento será de fato independente da taxa de quadros se você escalar com Time.deltaTime, mas se você estiver chamando de FixedUpdate, precisará escalar com Time.fixedDeltaTime. Eu acho que você está chamando seu passo de FixedUpdate, mas escalando com Time.deltaTime, o que resultaria em menor velocidade aparente quando o passo fixo do Unity for mais lento que o loop principal, que é o que está acontecendo em sua compilação independente. Quando a etapa fixa é lenta, fixedDeltaTime é grande.

Nox
fonte
1
Está sendo chamado de LateUpdate. Vou atualizar minha pergunta para deixar isso claro. Embora eu acredite Time.deltaTimeque ainda usará o valor correto, independentemente de onde é chamado (se usado em FixedUpdate, ele usará FixedDeltaTime).
Cooper