Oto kroki niezbędne do ulepszenia pętli symulacji fizyki.
1. Timepep
Główny problem, jaki widzę w twoim kodzie, polega na tym, że nie uwzględnia on kroku kroku fizyki. Powinno być oczywiste, że coś jest nie tak, Position += Velocity;
ponieważ jednostki się nie zgadzają. Albo Velocity
faktycznie nie jest prędkością, albo czegoś brakuje.
Nawet jeśli twoje wartości prędkości i grawitacji są skalowane w taki sposób, że każda klatka dzieje się w jednostce czasu 1
(co oznacza, że np. Velocity
Faktycznie oznacza odległość przebytą w ciągu jednej sekundy), czas musi pojawić się gdzieś w kodzie, niejawnie (poprzez ustalenie zmiennych tak, aby ich nazwy odzwierciedlają to, co naprawdę przechowują) lub jawnie (poprzez wprowadzenie pomiaru czasu). Uważam, że najłatwiej jest zadeklarować jednostkę czasu:
float TimeStep = 1.0;
I używaj tej wartości wszędzie tam, gdzie jest to potrzebne:
Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...
Zauważ, że każdy porządny kompilator uprości mnożenie 1.0
, dzięki czemu część nie spowolni.
Teraz Position += Velocity * TimeStep
wciąż nie jest całkiem dokładne (zobacz to pytanie, aby zrozumieć, dlaczego), ale prawdopodobnie tak się stanie.
Ponadto należy wziąć pod uwagę czas:
Velocity *= Physics.Air.Resistance;
Trochę trudniej jest to naprawić; jednym z możliwych sposobów jest:
Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
Math.Pow(Physics.Air.Resistance.Y, TimeStep))
* Velocity;
2. Podwójne aktualizacje
Teraz sprawdź, co robisz podczas odbijania (pokazano tylko odpowiedni kod):
Position += Velocity * TimeStep;
if (Position.Y < 0)
{
Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
Position.Y = Position.Y + Velocity.Y * TimeStep;
}
Możesz zobaczyć, że TimeStep
jest używane dwukrotnie podczas odrzuceń. Zasadniczo daje to kuli dwa razy więcej czasu na aktualizację. Oto co powinno się stać:
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. Grawitacja
Sprawdź teraz tę część kodu:
if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
Velocity += Physics.Gravity.Force * TimeStep;
}
Dodajesz grawitację przez cały czas trwania ramki. Ale co jeśli piłka faktycznie odbije się podczas tej klatki? Wtedy prędkość zostanie odwrócona, ale dodana grawitacja sprawi, że piłka przyspieszy od ziemi! Tak więc nadmiar grawitacji będą musiały być usuwane, kiedy odbijania , a następnie ponownie dodaje się w odpowiednim kierunku.
Może się zdarzyć, że nawet ponowne dodanie grawitacji we właściwym kierunku spowoduje zbyt duże przyspieszenie prędkości. Aby tego uniknąć, możesz albo pominąć dodawanie grawitacji (w końcu to nie jest tak dużo i trwa tylko rama), albo prędkość klamry do zera.
4. Naprawiono kod
A oto w pełni zaktualizowany kod:
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. Dalsze uzupełnienia
Aby uzyskać jeszcze lepszą stabilność symulacji, możesz zdecydować się na przeprowadzenie symulacji fizyki z większą częstotliwością. Jest to trywialne z uwagi na powyższe zmiany TimeStep
, ponieważ wystarczy podzielić ramkę na tyle części, ile chcesz. Na przykład:
public void Update()
{
float TimeStep = 1.0;
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
}