Ballphysik: Glättung der letzten Sprünge, wenn der Ball zur Ruhe kommt

12

Ich bin gegen ein anderes Problem in meinem kleinen springenden Ballspiel angekommen.

Mein Ball springt gut herum, bis auf die letzten Momente, in denen er zur Ruhe kommt. Die Bewegung des Balls ist für den Hauptteil glatt, aber gegen Ende ruckelt der Ball eine Weile, während er sich auf dem Boden des Bildschirms niederlässt.

Ich kann verstehen, warum das passiert, aber ich kann es nicht glätten.

Für Ratschläge wäre ich dankbar.

Mein Update-Code lautet:

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

Vielleicht sollte ich auch darauf hinweisen Gravity, Resistance Grassund Concretesind alle vom Typ Vector2.

Ste
quelle
Nur um dies zu bestätigen: Ihre "Reibung", wenn der Ball auf eine Oberfläche trifft, ist ein Wert <1, was im Grunde genommen der korrekte Wiedergutmachungskoeffizient ist ?
Jorge Leitao
@ JCLeitão - Richtig.
Ste
Bitte schwöre nicht, die Stimmen einzuhalten, wenn du Kopfgeld und die richtige Antwort gibst. Gehen Sie für das, was Ihnen geholfen hat.
aaaaaaaaaaa
Das ist eine schlechte Art, mit einem Kopfgeld umzugehen. Im Grunde sagen Sie, dass Sie sich nicht selbst beurteilen können, also lassen Sie die Aufwärtsstimmen entscheiden. Was Sie jedoch erleben, ist ein gewöhnlicher Kollisionsjitter. Dies kann gelöst werden, indem ein maximaler Durchdringungsbetrag, eine minimale Geschwindigkeit oder eine andere Form von 'Grenze' eingestellt wird, die einmal erreicht ist, und Ihre Routine veranlasst, die Bewegung anzuhalten und das Objekt zur Ruhe zu bringen. Sie können Ihren Objekten auch einen Ruhezustand hinzufügen, um unnötige Überprüfungen zu vermeiden.
Darkwings
@ Darkwings - Ich denke, die Community in diesem Szenario weiß besser als ich, was die beste Antwort ist. Deshalb werden die positiven Stimmen meine Entscheidung beeinflussen. Wenn ich die Lösung mit den meisten positiven Stimmen ausprobiert hätte und es mir nicht geholfen hätte, hätte ich diese Antwort natürlich nicht vergeben.
Ste

Antworten:

19

Hier sind die Schritte aufgeführt, die zur Verbesserung Ihrer Physiksimulationsschleife erforderlich sind.

1. Zeitschritt

Das Hauptproblem, das ich mit Ihrem Code sehen kann, ist, dass es die physikalische Schrittzeit nicht berücksichtigt. Es sollte offensichtlich sein, dass etwas nicht stimmt, Position += Velocity;da die Einheiten nicht übereinstimmen. Entweder Velocityist eigentlich keine Geschwindigkeit oder es fehlt etwas.

Auch wenn Ihre Geschwindigkeits- und Schwerkraftwerte so skaliert sind, dass jeder Frame in einer Zeiteinheit abläuft 1(was bedeutet, dass z. B. Velocity tatsächlich die zurückgelegte Distanz in einer Sekunde ist), muss die Zeit implizit irgendwo in Ihrem Code erscheinen (indem Sie die Variablen so festlegen, dass ihre Namen spiegeln wider, was sie wirklich speichern) oder explizit (durch Einführung eines Zeitschritts). Ich glaube, am einfachsten ist es, die Zeiteinheit anzugeben:

float TimeStep = 1.0;

Und verwenden Sie diesen Wert überall dort, wo er benötigt wird:

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

Beachten Sie, dass jeder anständige Compiler die Multiplikationen um ein Vielfaches vereinfacht 1.0, so dass der Teil die Dinge nicht langsamer macht.

Jetzt Position += Velocity * TimeStepist noch nicht ganz genau (siehe diese Frage, um zu verstehen, warum), aber es wird wahrscheinlich vorerst tun.

Dies muss auch Zeit in Betracht ziehen:

Velocity *= Physics.Air.Resistance;

Es ist etwas schwieriger zu beheben; Ein möglicher Weg ist:

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

2. Doppelte Updates

Überprüfen Sie nun, was Sie beim Bouncen tun (nur relevanter Code wird angezeigt):

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

Sie können sehen, dass TimeStepwährend des Abprallens zweimal verwendet wird. Dies gibt dem Ball im Grunde doppelt so viel Zeit, um sich selbst zu aktualisieren. Dies sollte stattdessen geschehen:

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. Schwerkraft

Überprüfen Sie jetzt diesen Teil des Codes:

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

Sie fügen die Schwerkraft für die gesamte Dauer des Rahmens hinzu. Aber was ist, wenn der Ball während dieses Rahmens tatsächlich springt? Dann wird die Geschwindigkeit umgekehrt, aber die hinzugefügte Schwerkraft lässt den Ball vom Boden weg beschleunigen! Daher muss die überschüssige Schwerkraft beim Abprallen entfernt und in der richtigen Richtung wieder hinzugefügt werden.

Es kann vorkommen, dass bereits die erneute Hinzufügung der Schwerkraft in die richtige Richtung zu einer zu starken Beschleunigung der Geschwindigkeit führt. Um dies zu vermeiden, können Sie entweder die Gravitationsaddition überspringen (immerhin ist es nicht so viel und es dauert nur einen Frame) oder die Geschwindigkeit auf Null klemmen.

4. Festcode

Und hier ist der vollständig aktualisierte Code:

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. Weitere Ergänzungen

Um die Stabilität der Simulation noch weiter zu verbessern, können Sie Ihre Physiksimulation mit einer höheren Frequenz ausführen. Dies wird durch die obigen Änderungen, die mit sich bringen , trivial gemacht TimeStep, da Sie Ihren Rahmen nur in so viele Teile aufteilen müssen, wie Sie möchten. Zum Beispiel:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}
sam hocevar
quelle
msgstr "Die Zeit muss irgendwo in Ihrem Code stehen." Sie werben dafür, dass die Multiplikation mit 1 nicht nur eine gute, sondern eine obligatorische Idee ist? Sicher, ein einstellbarer Zeitschritt ist eine nette Funktion, aber sicherlich nicht zwingend erforderlich.
aaaaaaaaaaa
@eBusiness: In meinem Argument geht es viel mehr um Konsistenz und Fehlererkennung als um einstellbare Zeitschritte. Ich sage nicht, dass das Multiplizieren mit 1 notwendig ist, ich sage, dass velocity += gravityes falsch ist und nur velocity += gravity * timestepSinn ergibt . Es kann am Ende das gleiche Ergebnis liefern, aber ohne einen Kommentar mit der Aufschrift "Ich weiß, was ich hier tue" bedeutet es immer noch einen Codierungsfehler, einen schlampigen Programmierer, einen Mangel an Kenntnissen über Physik oder nur einen Prototyp-Code, der benötigt wird verbessert werden.
Sam Hocevar
Sie sagen, es ist falsch , wenn Sie angeblich sagen wollen, dass es sich um eine schlechte Praxis handelt. Es ist Ihre subjektive Meinung zu diesem Thema, und es ist in Ordnung, dass Sie es ausdrücken, aber es ist subjektiv, da der Code in dieser Hinsicht genau das tut, was er soll. Ich bitte Sie lediglich, in Ihrem Beitrag den Unterschied zwischen subjektiv und objektiv deutlich zu machen.
aaaaaaaaaaa
2
@eBusiness: Ehrlich gesagt ist es in jeder Hinsicht falsch. Der Code "tut gar nicht so, wie er soll", weil 1) das Hinzufügen von Geschwindigkeit und Schwerkraft eigentlich nichts bedeutet; und 2) wenn es ein vernünftiges Ergebnis liefert, liegt es daran, dass der in gespeicherte Wert gravitytatsächlich… nicht die Schwerkraft ist. Aber ich kann das in der Post klarer machen.
Sam Hocevar
Im Gegenteil, es falsch zu nennen, ist in jeder Hinsicht falsch. Sie haben Recht, dass die Schwerkraft nicht in der Variablen namens Schwerkraft gespeichert ist, sondern eine Zahl, und das ist alles, was es jemals geben wird. Sie hat keine Beziehung zur Physik, die über die von uns angenommene Beziehung hinausgeht und mit der sie multipliziert wird Eine andere Zahl ändert daran nichts. Was sich scheinbar ändert, ist Ihre Fähigkeit und / oder Bereitschaft, die mentale Verbindung zwischen Code und Physik herzustellen. Übrigens eine recht interessante psychologische Beobachtung.
aaaaaaaaaaa
6

Fügen Sie ein Häkchen hinzu, um das Abprallen mit einer minimalen vertikalen Geschwindigkeit zu stoppen. Und wenn Sie den minimalen Sprung bekommen, legen Sie den Ball in den Boden.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}
Zhen
quelle
3
Diese Lösung gefällt mir, aber ich würde den Sprung nicht auf die Y-Achse beschränken. Ich würde die Normalen des Kolliders am Kollisionspunkt berechnen und prüfen, ob die Größe der Kollisionsgeschwindigkeit größer als die Sprungschwelle ist. Auch wenn die Welt des OP nur Y-Bounces zulässt, können andere Benutzer eine allgemeinere Lösung hilfreich finden. (Wenn ich unklar bin, denke daran, zwei Kugeln an einem zufälligen Punkt zusammen zu hüpfen.)
Brandon,
@brandon, toll, es sollte mit normal besser funktionieren.
Zhen
1
@Zhen, wenn Sie die Normale der Oberfläche verwenden, besteht die Möglichkeit, dass der Ball an einer Oberfläche haftet, deren Normale nicht parallel zur Schwerkraft ist. Ich würde versuchen, die Schwerkraft in die Berechnung einzubeziehen, wenn dies möglich ist.
Nic Foster
Keine dieser Lösungen sollten alle Geschwindigkeiten auf 0 gesetzt Sie begrenzen nur die Reflexion über den normalen des Vektors auf der Absprungschwelle abhängig
brandon
1

Ich denke also, das Problem, warum dies passiert, ist, dass sich Ihr Ball einem Limit nähert. Mathematisch gesehen bleibt der Ball niemals auf der Oberfläche stehen, er nähert sich der Oberfläche.

Ihr Spiel verwendet jedoch keine kontinuierliche Zeit. Es ist eine Karte, die eine Annäherung an die Differentialgleichung verwendet. Und diese Annäherung ist in dieser einschränkenden Situation nicht gültig (Sie können, aber Sie müssten kleinere und kleinere Zeitschritte machen, was meiner Meinung nach nicht machbar ist.

Physikalisch gesehen ist es so, dass der Ball, wenn er sich sehr nahe an der Oberfläche befindet, daran haftet, wenn die Gesamtkraft unter einer bestimmten Schwelle liegt.

@Zhen Antwort wäre in Ordnung, wenn Ihr System homogen ist, was nicht ist. Es hat eine gewisse Schwerkraft auf der y-Achse.

Daher würde ich sagen, dass die Lösung nicht darin bestehen sollte, dass die Geschwindigkeit unter einem bestimmten Schwellenwert liegt, sondern dass die nach der Aktualisierung auf den Ball ausgeübte Gesamtkraft unter einem bestimmten Schwellenwert liegt.

Diese Kraft ist der Beitrag der von der Wand auf den Ball ausgeübten Kraft + der Schwerkraft.

Die Bedingung sollte dann so ähnlich sein

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

Beachten Sie, dass newVelocity.y eine positive Größe ist, wenn sich der Sprung auf der Bodenwand befindet, und die Schwerkraft eine negative Größe ist.

Beachten Sie auch, dass newVelocity und Physics.Gravity.Force nicht dieselben Dimensionen haben, wie Sie geschrieben haben

Velocity += Physics.Gravity.Force;

Das heißt, ich gehe wie Sie von delta_time = 1 und ballMass = 1 aus.

Hoffe das hilft

Jorge Leitao
quelle
1

Sie haben eine Positionsaktualisierung in Ihrer Kollisionsprüfung, sie ist redundant und falsch. Und es fügt dem Ball Energie hinzu und hilft ihm so möglicherweise, sich ständig zu bewegen. Zusammen mit der Schwerkraft, die bei einigen Bildern nicht angewendet wird, entsteht eine merkwürdige Bewegung. Entfernen Sie es.

Jetzt sehen Sie möglicherweise ein anderes Problem: Der Ball bleibt außerhalb des festgelegten Bereichs "stecken" und springt ständig vor und zurück.

Eine einfache Möglichkeit, dieses Problem zu lösen, besteht darin, zu überprüfen, ob sich der Ball in die richtige Richtung bewegt, bevor Sie ihn ändern.

Also solltest du machen:

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

In:

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

Ähnliches gilt für die Y-Richtung.

Damit der Ball gut anhält, muss die Schwerkraft irgendwann gestoppt werden. Ihre aktuelle Implementierung stellt sicher, dass der Ball immer wieder auftaucht, da die Schwerkraft ihn nicht bremst, solange er sich im Untergrund befindet. Sie sollten immer die Schwerkraft anwenden. Dies führt jedoch dazu, dass der Ball nach dem Absetzen langsam in den Boden sinkt. Eine schnelle Lösung hierfür ist, nach dem Anwenden der Schwerkraft den Ball anzuhalten, wenn er sich unter der Oberfläche befindet und sich nach unten bewegt:

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

Diese Änderungen insgesamt sollten Ihnen eine anständige Simulation geben. Beachten Sie jedoch, dass es sich immer noch um eine sehr einfache Simulation handelt.

aaaaaaaaaaa
quelle
0

Haben Sie eine Mutator-Methode für alle Geschwindigkeitsänderungen, dann können Sie innerhalb dieser Methode die aktualisierte Geschwindigkeit überprüfen, um festzustellen, ob sie sich langsam genug bewegt, um sie in den Ruhezustand zu versetzen. Die meisten mir bekannten physikalischen Systeme bezeichnen dies als "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;

Bei der obigen Methode begrenzen wir das Abprallen, wenn es sich entlang derselben Achse wie die Schwerkraft befindet.

Eine weitere Überlegung wäre, festzustellen, wann ein Ball auf den Boden gestoßen ist, und die Geschwindigkeit entlang der Schwerkraftachse auf Null zu setzen, wenn er sich zum Zeitpunkt der Kollision relativ langsam bewegt.

Nic Foster
quelle
Ich werde nicht abstimmen, weil dies gültig ist, aber die Frage ist nach Sprungschwellen, nicht nach Geschwindigkeitsschwellen. Diese sind nach meiner Erfahrung fast immer getrennt, da der Effekt des Zitterns während des Springens im Allgemeinen von dem Effekt der weiteren Berechnung der Geschwindigkeit getrennt ist, wenn sie visuell in Ruhe ist.
Brandon
Sie sind eins im selben. Physik-Engines wie Havok oder PhysX und JigLibX basieren auf der linearen Geschwindigkeit (und der Winkelgeschwindigkeit). Diese Methode sollte für alle Bewegungen des Balls geeignet sein, einschließlich des Springens. Tatsächlich verwendete das letzte Projekt, an dem ich beteiligt war (LEGO Universe), eine fast identische Methode, um das Prellen von Münzen zu stoppen, sobald sie sich verlangsamt hatten. In diesem Fall verwendeten wir keine dynamische Physik, also mussten wir das manuell machen, anstatt Havok das für uns erledigen zu lassen.
Nic Foster
@NicFoster: Ich bin verwirrt, da sich ein Objekt meiner Meinung nach horizontal und vertikal sehr schnell bewegen könnte. In diesem Fall würde Ihre Methode nicht ausgelöst. Ich denke, das OP möchte, dass der vertikale Abstand trotz der hohen Geschwindigkeitslänge auf Null gesetzt wird.
George Duckett
@ GeorgeDuckett: Ah danke, ich habe die ursprüngliche Frage falsch verstanden. Der OP möchte nicht, dass der Ball aufhört, sich zu bewegen. Stoppen Sie einfach die vertikale Bewegung. Ich habe die Antwort aktualisiert, um nur die Sprunggeschwindigkeit zu berücksichtigen.
Nic Foster
0

Eine andere Sache: Sie multiplizieren mit einer Reibungskonstante. Ändern Sie das - senken Sie die Reibungskonstante, fügen Sie jedoch eine feste Energieabsorption für einen Sprung hinzu. Dadurch werden die letzten Sprünge viel schneller gedämpft.

Loren Pechtel
quelle