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 Velocity
ist 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 * TimeStep
ist 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 TimeStep
wä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);
}