Aquí los pasos necesarios para mejorar su ciclo de simulación física.
1. Timestep
El principal problema que puedo ver con su código es que no tiene en cuenta el tiempo del paso de física. Debería ser obvio que hay algo mal Position += Velocity;
porque las unidades no coinciden. O en Velocity
realidad no es una velocidad, o falta algo.
Incluso si sus valores de velocidad y gravedad están escalados de manera que cada cuadro ocurra en una unidad de tiempo 1
(lo que significa que, por ejemplo, en Velocity
realidad significa la distancia recorrida en un segundo), el tiempo debe aparecer en algún lugar de su código, ya sea implícitamente (arreglando las variables de manera que sus nombres reflejan lo que realmente almacenan) o explícitamente (al introducir un paso de tiempo). Creo que lo más fácil es declarar la unidad de tiempo:
float TimeStep = 1.0;
Y use ese valor donde sea necesario:
Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...
Tenga en cuenta que cualquier compilador decente simplificará las multiplicaciones 1.0
, por lo que esa parte no hará las cosas más lentas.
Ahora Position += Velocity * TimeStep
todavía no es del todo exacto (vea esta pregunta para entender por qué), pero probablemente lo hará por ahora.
Además, esto debe tener en cuenta el tiempo:
Velocity *= Physics.Air.Resistance;
Es un poco más complicado de arreglar; Una forma posible es:
Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
Math.Pow(Physics.Air.Resistance.Y, TimeStep))
* Velocity;
2. actualizaciones dobles
Ahora verifique lo que hace cuando rebota (solo se muestra el código relevante):
Position += Velocity * TimeStep;
if (Position.Y < 0)
{
Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
Position.Y = Position.Y + Velocity.Y * TimeStep;
}
Puede ver que TimeStep
se usa dos veces durante el rebote. Básicamente, esto le da a la pelota el doble de tiempo para actualizarse. Esto es lo que debería suceder en su lugar:
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. Gravedad
Verifique esta parte del código ahora:
if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
Velocity += Physics.Gravity.Force * TimeStep;
}
Agrega gravedad durante toda la duración del cuadro. Pero, ¿qué pasa si la pelota realmente rebota durante ese cuadro? ¡Entonces la velocidad se invertirá, pero la gravedad que se agregó hará que la bola acelere lejos del suelo! Por lo tanto , se deberá eliminar el exceso de gravedad al rebotar y luego volver a agregarlo en la dirección correcta.
Puede suceder que incluso volver a agregar la gravedad en la dirección correcta provoque que la velocidad se acelere demasiado. Para evitar esto, puede omitir la adición de gravedad (después de todo, no es tanto y solo dura un fotograma) o fijar la velocidad a cero.
4. Código fijo
Y aquí está el código completamente actualizado:
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. Adiciones adicionales
Para mejorar aún más la estabilidad de la simulación, puede decidir ejecutar su simulación física a una frecuencia más alta. Esto se hace trivial por los cambios anteriores que involucran TimeStep
, porque solo necesita dividir su marco en tantos trozos como desee. Por ejemplo:
public void Update()
{
float TimeStep = 1.0;
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
}