Ecco i passaggi necessari per migliorare il tuo ciclo di simulazione fisica.
1. Timestep
Il problema principale che posso vedere con il tuo codice è che non tiene conto del tempo di passaggio della fisica. Dovrebbe essere ovvio che c'è qualcosa che non va Position += Velocity;
perché le unità non corrispondono. O in Velocity
realtà non è una velocità o manca qualcosa.
Anche se i valori di velocità e gravità sono ridimensionati in modo tale che ciascun fotogramma si verifichi in un'unità di tempo 1
(il che significa che, ad esempio, Velocity
significa effettivamente la distanza percorsa in un secondo), il tempo deve apparire in qualche punto del codice, sia implicitamente (fissando le variabili in modo che i loro nomi riflettono ciò che realmente memorizzano) o esplicitamente (introducendo un timestep). Credo che la cosa più semplice da fare sia dichiarare l'unità di tempo:
float TimeStep = 1.0;
E usa quel valore ovunque sia necessario:
Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...
Nota che qualsiasi compilatore decente semplificherà le moltiplicazioni di 1.0
, in modo che quella parte non rallenti le cose.
Ora non Position += Velocity * TimeStep
è ancora del tutto esatto (vedi questa domanda per capire perché) ma probabilmente lo farà per ora.
Inoltre, questo deve prendere in considerazione il tempo:
Velocity *= Physics.Air.Resistance;
È un po 'più complicato da risolvere; un modo possibile è:
Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
Math.Pow(Physics.Air.Resistance.Y, TimeStep))
* Velocity;
2. Doppi aggiornamenti
Ora controlla cosa fai quando rimbalzi (mostrato solo il codice pertinente):
Position += Velocity * TimeStep;
if (Position.Y < 0)
{
Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
Position.Y = Position.Y + Velocity.Y * TimeStep;
}
Puoi vedere che TimeStep
viene usato due volte durante il rimbalzo. Questo in pratica dà alla palla il doppio del tempo per aggiornarsi. Questo è ciò che dovrebbe accadere invece:
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à
Controlla questa parte del codice ora:
if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
Velocity += Physics.Gravity.Force * TimeStep;
}
Aggiungete la gravità per l'intera durata del fotogramma. Ma cosa succede se la palla rimbalza effettivamente durante quel frame? Quindi la velocità verrà invertita, ma la gravità aggiunta farà accelerare la palla da terra! Quindi la gravità in eccesso dovrà essere rimossa durante il rimbalzo , quindi aggiunta nuovamente nella direzione corretta.
Può accadere che anche l'aggiunta di gravità nella direzione corretta provochi un'accelerazione eccessiva della velocità. Per evitarlo, puoi saltare l'aggiunta di gravità (dopotutto, non è molto e dura solo un frame) o bloccare la velocità a zero.
4. Codice fisso
Ed ecco il codice completamente aggiornato:
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. Ulteriori aggiunte
Per una stabilità della simulazione ancora migliore, puoi decidere di eseguire la simulazione fisica a una frequenza più elevata. Ciò è reso banale dalle modifiche di cui sopra che coinvolgono TimeStep
, perché devi solo dividere la cornice in tutti i blocchi che desideri. Per esempio:
public void Update()
{
float TimeStep = 1.0;
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
}