Physique de la balle: lissage des rebonds finaux lorsque la balle s'arrête

12

Je me suis heurté à un autre problème dans mon petit jeu de balle rebondissante.

Ma balle rebondit très bien, sauf pour les derniers moments où elle est sur le point de se reposer. Le mouvement de la balle est fluide pour la partie principale mais, vers la fin, la balle se branle pendant un certain temps alors qu'elle s'installe au bas de l'écran.

Je peux comprendre pourquoi cela se produit mais je n'arrive pas à le lisser.

Je serais reconnaissant pour tout conseil qui pourrait être offert.

Mon code de mise à jour est:

public void Update()
    {
        // Apply gravity if we're not already on the ground
        if(Position.Y < GraphicsViewport.Height - Texture.Height)
        {
            Velocity += Physics.Gravity.Force;
        }            
        Velocity *= Physics.Air.Resistance;
        Position += Velocity;

        if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
        {
            // We've hit a vertical (side) boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Concrete;

            // Invert velocity
            Velocity.X = -Velocity.X;
            Position.X = Position.X + Velocity.X;
        }

        if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
        {
            // We've hit a horizontal boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Grass;

            // Invert Velocity
            Velocity.Y = -Velocity.Y;
            Position.Y = Position.Y + Velocity.Y;
        }
    }

Je devrais peut-être aussi le souligner Gravity, Resistance Grasset ils Concretesont tous du genre Vector2.

Ste
la source
Juste pour le confirmer: votre "friction" lorsque la balle touche une surface est une valeur <1, ​​quel est le coefficient de restitution correct?
Jorge Leitao
@ JCLeitão - Correct.
Ste
Veuillez ne pas jurer de respecter les votes lorsque vous accordez une prime et une réponse correcte. Optez pour tout ce qui vous a aidé.
aaaaaaaaaaaa
C'est une mauvaise façon de gérer une prime, fondamentalement, vous dites que vous ne pouvez pas vous juger vous-même, donc laissez les votes positifs décider ... Quoi qu'il en soit, ce que vous vivez est une gigue de collision courante. Cela peut être résolu en définissant une quantité d'interpénétration maximale, une vitesse minimale ou toute autre forme de «limite» qui, une fois atteinte, entraînera votre routine pour arrêter le mouvement et mettre l'objet au repos. Vous pouvez également ajouter un statut de repos à vos objets pour éviter les vérifications inutiles.
Darkwings
@Darkwings - Je pense que la communauté dans ce scénario sait mieux que moi quelle est la meilleure réponse. C'est pourquoi les votes positifs vont influencer ma décision. Évidemment, si j'essayais la solution avec le plus de votes positifs et que cela ne m'aidait pas , je ne donnerais pas cette réponse.
Ste

Réponses:

19

Voici les étapes nécessaires pour améliorer votre boucle de simulation physique.

1. Timestep

Le principal problème que je peux voir avec votre code est qu'il ne tient pas compte du temps de pas physique. Il devrait être évident qu'il y a quelque chose qui ne va pas Position += Velocity;parce que les unités ne correspondent pas. Soit ce Velocityn'est pas une vitesse, soit quelque chose manque.

Même si vos valeurs de vitesse et de gravité sont mises à l'échelle de sorte que chaque image se produise à une unité de temps 1(ce qui signifie par exemple que Velocitysignifie réellement la distance parcourue en une seconde), le temps doit apparaître quelque part dans votre code, soit implicitement (en fixant les variables de sorte que leurs noms reflètent ce qu'ils stockent vraiment) ou explicitement (en introduisant un pas de temps). Je pense que la chose la plus simple à faire est de déclarer l'unité de temps:

float TimeStep = 1.0;

Et utilisez cette valeur partout où vous en avez besoin:

Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...

Notez que tout compilateur décent simplifiera les multiplications 1.0, de sorte que cette partie ne ralentira pas les choses.

Maintenant, ce Position += Velocity * TimeStepn'est pas encore tout à fait exact (voir cette question pour comprendre pourquoi), mais il le fera probablement pour l'instant.

En outre, cela doit prendre en compte le temps:

Velocity *= Physics.Air.Resistance;

C'est un peu plus difficile à corriger; une façon possible est:

Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
                    Math.Pow(Physics.Air.Resistance.Y, TimeStep))
          * Velocity;

2. Double mise à jour

Vérifiez maintenant ce que vous faites lorsque vous rebondissez (seul le code pertinent est affiché):

Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Position.Y + Velocity.Y * TimeStep;
}

Vous pouvez voir qu'il TimeStepest utilisé deux fois pendant le rebond. Cela donne au ballon deux fois plus de temps pour se mettre à jour. C'est ce qui devrait arriver à la place:

Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    /* First, stop at Y = 0 and count how much time is left */
    float RemainingTime = -Position.Y / Velocity.Y;
    Position.Y = 0;

    /* Then, start from Y = 0 and only use how much time was left */
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Velocity.Y * RemainingTime;
}

3. Gravité

Vérifiez cette partie du code maintenant:

if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
    Velocity += Physics.Gravity.Force * TimeStep;
}            

Vous ajoutez de la gravité pendant toute la durée du cadre. Mais que se passe-t-il si la balle rebondit réellement pendant cette image? La vitesse sera alors inversée, mais la gravité ajoutée fera alors accélérer le ballon loin du sol! Ainsi, l' excès de gravité devra être retiré lors du rebond , puis rajouté dans la bonne direction.

Il peut arriver que même en rajoutant de la gravité dans la bonne direction, la vitesse s'accélère trop. Pour éviter cela, vous pouvez soit ignorer l'ajout de gravité (après tout, ce n'est pas tant que ça et cela ne dure qu'une image), soit fixer la vitesse à zéro.

4. Code fixe

Et voici le code entièrement mis à jour:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep);
}

public void Update(float TimeStep)
{
    float RemainingTime;

    // Apply gravity if we're not already on the ground
    if(Position.Y < GraphicsViewport.Height - Texture.Height)
    {
        Velocity += Physics.Gravity.Force * TimeStep;
    }
    Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, RemainingTime),
                        Math.Pow(Physics.Air.Resistance.Y, RemainingTime))
              * Velocity;
    Position += Velocity * TimeStep;

    if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
    {
        // We've hit a vertical (side) boundary
        if (Position.X < 0)
        {
            RemainingTime = -Position.X / Velocity.X;
            Position.X = 0;
        }
        else
        {
            RemainingTime = (Position.X - (GraphicsViewport.Width - Texture.Width)) / Velocity.X;
            Position.X = GraphicsViewport.Width - Texture.Width;
        }

        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Concrete.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Concrete.Y, RemainingTime))
                  * Velocity;

        // Invert velocity
        Velocity.X = -Velocity.X;
        Position.X = Position.X + Velocity.X * RemainingTime;
    }

    if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
    {
        // We've hit a horizontal boundary
        if (Position.Y < 0)
        {
            RemainingTime = -Position.Y / Velocity.Y;
            Position.Y = 0;
        }
        else
        {
            RemainingTime = (Position.Y - (GraphicsViewport.Height - Texture.Height)) / Velocity.Y;
            Position.Y = GraphicsViewport.Height - Texture.Height;
        }

        // Remove excess gravity
        Velocity.Y -= RemainingTime * Physics.Gravity.Force;

        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Grass.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Grass.Y, RemainingTime))
                  * Velocity;

        // Invert velocity
        Velocity.Y = -Velocity.Y;

        // Re-add excess gravity
        float OldVelocityY = Velocity.Y;
        Velocity.Y += RemainingTime * Physics.Gravity.Force;
        // If velocity changed sign again, clamp it to zero
        if (Velocity.Y * OldVelocityY <= 0)
            Velocity.Y = 0;

        Position.Y = Position.Y + Velocity.Y * RemainingTime;
    }
}

5. Ajouts supplémentaires

Pour une stabilité de simulation encore améliorée, vous pouvez décider d'exécuter votre simulation physique à une fréquence plus élevée. Cela est rendu trivial par les changements ci-dessus TimeStep, car il vous suffit de diviser votre cadre en autant de morceaux que vous le souhaitez. Par exemple:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}
sam hocevar
la source
"le temps doit apparaître quelque part dans votre code." Vous faites de la publicité que multiplier par 1 partout n'est pas seulement une bonne idée, c'est obligatoire? Bien sûr, un pas de temps réglable est une fonctionnalité intéressante, mais ce n'est certainement pas obligatoire.
aaaaaaaaaaaa
@eBusiness: mon argument est beaucoup plus sur la cohérence et la détection des erreurs que sur les pas de temps réglables. Je ne dis pas que multiplier par 1 est nécessaire, je dis que velocity += gravityc'est faux et n'a de velocity += gravity * timestepsens que. Cela peut donner le même résultat à la fin, mais sans un commentaire disant "je sais ce que je fais ici" cela signifie toujours une erreur de codage, un programmeur bâclé, un manque de connaissances sur la physique, ou juste un code prototype qui doit Soyez améliorés.
sam hocevar
Vous dites que c'est faux , alors que ce que vous voulez dire, c'est que c'est une mauvaise pratique. C'est votre opinion subjective sur la question, et c'est bien que vous l'exprimiez, mais elle EST subjective car le code à cet égard fait exactement ce qu'il est censé faire. Tout ce que je demande, c'est que vous fassiez la différence entre le subjectif et l'objectif dans votre message.
aaaaaaaaaaaa
2
@eBusiness: Honnêtement, il est faux par aucune norme saine d' esprit. Le code ne fait pas du tout ce qu'il est censé faire, car 1) ajouter de la vitesse et de la gravité ne signifie rien du tout; et 2) si cela donne un résultat raisonnable, c'est parce que la valeur stockée dans gravityest en fait… pas la gravité. Mais je peux clarifier cela dans le post.
sam hocevar
Au contraire, le qualifier de mauvais est faux à tous points de vue. Vous avez raison de dire que la gravité n'est pas stockée dans la variable nommée gravité, mais plutôt un nombre, et c'est tout ce qu'il y aura jamais, elle n'a aucun rapport avec la physique au-delà de la relation que nous imaginons avoir, en la multipliant par un autre nombre ne change pas cela. Ce que cela change apparemment, c'est votre capacité et / ou votre volonté de faire le lien mental entre le code et la physique. Au fait une observation psychologique assez intéressante.
aaaaaaaaaaaa
6

Ajoutez une coche pour arrêter le rebond, en utilisant une vitesse verticale minimale. Et lorsque vous obtenez le rebond minimal, placez la balle dans le sol.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}
Zhen
la source
3
J'aime cette solution, mais je ne limiterais pas le rebond à l'axe Y. Je calculerais la normale du collisionneur au point de collision et vérifierais si l'amplitude de la vitesse de collision est supérieure au seuil de rebond. Même si le monde de l'OP n'autorise que les rebonds Y, d'autres utilisateurs peuvent trouver une solution plus générale utile. (Si je ne suis pas clair, pensez à faire rebondir deux sphères ensemble à un moment aléatoire)
brandon
@brandon, super, ça devrait mieux fonctionner avec la normale.
Zhen
1
@Zhen, si vous utilisez la normale de la surface, vous avez la possibilité que la balle finisse par coller à une surface dont la normale n'est pas parallèle à celle de la gravité. J'essaierais de prendre en compte la gravité dans le calcul si possible.
Nic Foster
Aucune de ces solutions ne devrait régler les vitesses à 0. Vous ne limitez la réflexion à travers la normale du vecteur qu'en fonction du seuil de rebond
brandon
1

Donc, je pense que le problème de pourquoi cela se produit est que votre balle approche d'une limite. Mathématiquement, la balle ne s'arrête jamais à la surface, elle s'approche de la surface.

Cependant, votre jeu n'utilise pas de temps continu. Il s'agit d'une carte qui utilise une approximation de l'équation différentielle. Et cette approximation n'est pas valable dans cette situation limite (vous pouvez, mais vous devrez prendre des pas de temps plus petits et plus petits, ce qui, je suppose, n'est pas faisable.

Physiquement parlant, ce qui se passe, c'est que lorsque la balle est très proche de la surface, elle y adhère si la force totale est inférieure à un seuil donné.

La réponse @Zhen serait bien si votre système est homogène, ce qui n'est pas le cas. Il a une certaine gravité sur l'axe y.

Donc, je dirais que la solution ne serait pas que la vitesse soit inférieure à un seuil donné, mais la force totale appliquée sur la balle après la mise à jour devrait être inférieure à un seuil donné.

Cette force est la contribution de la force exercée par le mur sur le ballon + la gravité.

La condition devrait alors être quelque chose comme

if (newVelocity + Physics.Gravity.Force <seuil)

notez que newVelocity.y est une quantité positive si le rebond est sur la paroi du botton et que la gravité est une quantité négative.

Notez également que newVelocity et Physics.Gravity.Force n'ont pas les mêmes dimensions, comme vous l'avez écrit dans

Velocity += Physics.Gravity.Force;

ce qui signifie que, comme vous, je suppose que delta_time = 1 et ballMass = 1.

J'espère que cela t'aides

Jorge Leitao
la source
1

Vous avez une mise à jour de position dans votre contrôle de collision, elle est redondante et erronée. Et il ajoute de l'énergie à la balle, l'aidant ainsi potentiellement à se déplacer perpétuellement. Avec la gravité non appliquée à certains cadres, cela donne un mouvement étrange. Retirez-le.

Maintenant, vous pouvez voir un problème différent, le fait que la balle se "bloque" en dehors de la zone désignée, rebondissant perpétuellement d'avant en arrière.

Un moyen simple de résoudre ce problème consiste à vérifier que la balle se déplace dans la bonne direction avant de la changer.

Vous devez donc faire:

if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)

Dans:

if ((Position.X < 0 && Velocity.X < 0) || (Position.X > GraphicsViewport.Width - Texture.Width && Velocity.X > 0))

Et similaire pour la direction Y.

Pour que le ballon s'arrête bien, vous devez arrêter la gravité à un moment donné. Votre implémentation actuelle garantit que la balle refera toujours surface car la gravité ne la freine pas tant qu'elle est souterraine. Vous devez changer pour toujours appliquer la gravité. Cela conduit cependant à la balle s'enfoncer lentement dans le sol après s'être installée. Une solution rapide pour cela est, après avoir appliqué la gravité, si la balle est en dessous du niveau de la surface et se déplace vers le bas, arrêtez-la:

Velocity += Physics.Gravity.Force;
if(Position.Y > GraphicsViewport.Height - Texture.Height && Velocity.Y > 0)
{
    Velocity.Y = 0;
}

Ces changements au total devraient vous donner une simulation décente. Mais notez qu'il s'agit toujours d'une simulation très simple.

aaaaaaaaaaaa
la source
0

Avoir une méthode de mutation pour tous les changements de vitesse, puis dans cette méthode, vous pouvez vérifier la vitesse mise à jour pour déterminer si elle se déplace assez lentement pour la mettre au repos. La plupart des systèmes de physique que je connais appellent cela «restitution».

public Vector3 Velocity
{
    public get { return velocity; }
    public set
    {
        velocity = value;

        // We get the direction that gravity pulls in
        Vector3 GravityDirection = gravity;
        GravityDirection.Normalize();

        Vector3 VelocityDirection = velocity;
        VelocityDirection.Normalize();

        if ((velocity * GravityDirection).SquaredLength() < 0.25f)
        {
            velocity.Y = 0.0f;
        }            
    }
}
private Vector3 velocity;

Dans la méthode ci-dessus, nous limitons le rebond à chaque fois qu'il est sur le même axe que la gravité.

Une autre chose à considérer serait de détecter chaque fois qu'une balle est entrée en collision avec le sol, et si elle se déplace assez lentement au moment de la collision, réglez la vitesse le long de l'axe de gravité sur zéro.

Nic Foster
la source
Je ne vais pas dévaloriser parce que cela est valide, mais la question concerne les seuils de rebond, pas les seuils de vitesse. Ceux-ci sont presque toujours séparés dans mon expérience, car l'effet de la gigue pendant le rebond est généralement distinct de l'effet de continuer à calculer la vitesse une fois visuellement au repos.
brandon
Ils sont un dans le même. Les moteurs physiques, comme Havok, ou PhysX, et JigLibX basent la restitution sur la vitesse linéaire (et la vitesse angulaire). Cette méthode devrait fonctionner pour tous les mouvements de la balle, y compris les rebonds. En fait, le dernier projet sur lequel j'étais (LEGO Universe) a utilisé une méthode presque identique à celle-ci pour arrêter le rebond des pièces une fois qu'elles avaient ralenti. Dans ce cas, nous n'utilisions pas la physique dynamique, nous avons donc dû le faire manuellement plutôt que de laisser Havok s'en occuper pour nous.
Nic Foster
@NicFoster: Je suis confus, car selon moi, un objet pourrait se déplacer très rapidement horizontalement et presque pas verticalement, auquel cas votre méthode ne se déclencherait pas. Je pense que l'OP voudrait que la distance verticale soit mise à zéro malgré la longueur de la vitesse étant élevée.
George Duckett
@GeorgeDuckett: Ah merci, j'ai mal compris la question d'origine. L'OP ne veut pas que la balle cesse de bouger, arrêtez simplement le mouvement vertical. J'ai mis à jour la réponse pour ne tenir compte que de la vitesse de rebond.
Nic Foster
0

Autre chose: vous multipliez par une constante de friction. Changez cela - abaissez la constante de frottement mais ajoutez une absorption d'énergie fixe sur un rebond. Cela amortira ces derniers rebonds beaucoup plus rapidement.

Loren Pechtel
la source