Le mouvement semble dépendre de la fréquence d'images, malgré l'utilisation de Time.deltaTime

13

J'ai le code suivant pour calculer la traduction requise pour déplacer un objet de jeu dans Unity, qui est appelé LateUpdate. D'après ce que je comprends, mon utilisation de Time.deltaTimedevrait rendre la fréquence d'images finale de la traduction indépendante (veuillez noter qu'il CollisionDetection.Move()s'agit simplement de lancer des 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);
}

Si je verrouille la fréquence d'images du jeu à 60 images par seconde, mes objets se déplacent comme prévu. Cependant, si je le déverrouille ( Application.targetFrameRate = -1;), certains objets se déplaceront à un rythme beaucoup plus lent que ce à quoi je m'attendrais en atteignant ~ 200FPS sur un moniteur 144hz. Cela ne semble se produire que dans une version autonome, et non dans l'éditeur Unity.

GIF de mouvement d'objet dans l'éditeur, FPS déverrouillé

http://gfycat.com/SmugAnnualFugu

GIF de mouvement d'objet dans la version autonome, FPS déverrouillé

http://gfycat.com/OldAmpleJuliabutterfly

Tonnelier
la source
2
Vous devriez lire ceci. La répartition du temps est ce que vous voulez, et des pas de temps fixes! gafferongames.com/game-physics/fix-your-timestep
Alan Wolfe

Réponses:

30

Les simulations basées sur des trames rencontreront des erreurs lorsque les mises à jour ne compenseront pas les taux de changement non linéaires.

Par exemple, considérons un objet commençant par des valeurs de position et de vitesse de zéro subissant une accélération constante de un.

Si nous appliquons cette logique de mise à jour:

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

Nous pouvons nous attendre à ces résultats sous différentes fréquences d'images: entrez la description de l'image ici

L'erreur est causée par le traitement de la vitesse finale comme si elle s'appliquait à l'ensemble du cadre. Ceci est similaire à une somme de Riemann droite et la quantité d'erreur varie avec la fréquence d'images (illustrée sur une fonction différente):

Comme MichaelS le fait remarquer, cette erreur sera réduite de moitié lorsque la durée de la trame est réduite de moitié, et peut devenir sans conséquence à des fréquences de trame élevées. D'un autre côté, tous les jeux qui connaissent des pics de performances ou des cadres longs peuvent trouver que cela produit un comportement imprévisible.


Heureusement, la cinématique nous permet de calculer avec précision le déplacement provoqué par l'accélération linéaire:

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

Donc, si nous appliquons cette logique de mise à jour:

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

Nous aurons les résultats suivants:

entrez la description de l'image ici

Kelly Thomas
la source
2
Il s'agit d'informations utiles, mais comment traite-t-elle réellement le code en question? Tout d'abord, l'erreur diminue considérablement à mesure que la fréquence d'images augmente, de sorte que la différence entre 60 et 200 images par seconde est négligeable (8 images par seconde contre l'infini n'est déjà que de 12,5% trop élevé). Deuxièmement, une fois que le sprite est à pleine vitesse, la plus grande différence est de 0,5 unité d'avance. Cela ne devrait pas affecter la vitesse de marche réelle comme indiqué dans les .gifs joints. Quand ils se retournent, l'accélération est apparemment instantanée (peut-être plusieurs images à 60+ images par seconde, mais pas des secondes complètes).
MichaelS
2
C'est un problème Unity ou code alors, pas un problème mathématique. Une feuille de calcul rapide dit que si nous utilisons a = 1, vi = 0, di = 0, vmax = 1, nous devons atteindre vmax à t = 1, avec d = 0,5. En faisant cela sur 5 images (dt = 0,2), d (t = 1) = 0,6. Plus de 50 images (dt = 0,02), d (t = 1) = 0,51. Plus de 500 images (dt = 0,002), d (t = 1) = 0,501. Donc, 5 images par seconde est de 20% de haut, 50 images par seconde de 2% et 500 images par seconde de 0,2%. En général, l'erreur est de 100 / fps pour cent trop élevée. 50 fps est environ 1,8% supérieur à 500 fps. Et c'est juste pendant l'accélération. Une fois que la vélocité atteint le maximum, il ne devrait y avoir aucune différence. Avec a = 100 et vmax = 5, il devrait y avoir encore moins de différence.
MichaelS
2
En fait, je suis allé de l'avant et j'ai utilisé votre code dans une application VB.net (simulant le dt de 1/60 et 1/200), et j'ai obtenu Bounce: 5 à l'image 626 (10.433) secondes contre Bounce: 5 à l'image 2081 ( 10.405) secondes . 0,27% plus de temps à 60 ips.
MichaelS
2
C'est votre approche "cinématique" qui donne une différence de 10%. L'approche traditionnelle est la différence de 0,27%. Vous venez de les étiqueter incorrectement. Je pense que c'est parce que vous incluez incorrectement l'accélération lorsque la vitesse est maximale. Des cadences plus élevées ajoutent moins d'erreurs par trame, donc donnent un résultat plus précis. Vous en avez besoin if(velocity==vmax||velocity==-vmax){acceleration=0}. Ensuite, l'erreur diminue considérablement, bien qu'elle ne soit pas parfaite car nous ne savons pas exactement quelle partie de l'accélération du cadre s'est terminée.
MichaelS
6

Cela dépend d'où vous appelez votre étape. Si vous l'appelez depuis Update, votre mouvement sera en effet indépendant de la fréquence d'images si vous évoluez avec Time.deltaTime, mais si vous l'appelez depuis FixedUpdate, vous devez évoluer avec Time.fixedDeltaTime. Je suppose que vous appelez votre étape à partir de FixedUpdate, mais que vous évoluez avec Time.deltaTime, ce qui entraînerait une diminution de la vitesse apparente lorsque l'étape fixe d'Unity est plus lente que la boucle principale, ce qui se passe dans votre version autonome. Lorsque l'étape fixe est lente, fixedDeltaTime est grande.

Nox
la source
1
Il est appelé depuis LateUpdate. Je mettrai à jour ma question pour que ce soit clair. Bien que je pense Time.deltaTimequ'il utilisera toujours la valeur correcte quel que soit l'endroit où il est appelé (s'il est utilisé dans FixedUpdate, il utilisera fixedDeltaTime).
Cooper