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 Velocity
n'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 Velocity
signifie 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 * TimeStep
n'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 TimeStep
est 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);
}