Aqui estão as etapas necessárias para melhorar seu loop de simulação física.
1. Timestep
O principal problema que vejo no seu código é que ele não leva em consideração o tempo da etapa da física. Deveria ser óbvio que há algo errado, Position += Velocity;
porque as unidades não coincidem. Ou Velocity
não é realmente uma velocidade ou algo está faltando.
Mesmo se seus valores de velocidade e gravidade são escalados de modo que cada quadro acontece em uma unidade de tempo 1
(o que significa que , por exemplo. Velocity
Na verdade significa a distância percorrida em um segundo), o tempo deve aparecer em algum lugar no seu código, quer implicitamente (fixando as variáveis de modo a que seus nomes refletem o que eles realmente armazenam) ou explicitamente (introduzindo um timestep). Acredito que a coisa mais fácil a fazer é declarar a unidade de tempo:
float TimeStep = 1.0;
E use esse valor em qualquer lugar que for necessário:
Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...
Observe que qualquer compilador decente simplificará as multiplicações 1.0
, para que essa parte não torne as coisas mais lentas.
Agora Position += Velocity * TimeStep
ainda não é bem exato (veja esta pergunta para entender o porquê), mas provavelmente o fará por enquanto.
Além disso, isso precisa levar tempo em consideração:
Velocity *= Physics.Air.Resistance;
É um pouco mais difícil de corrigir; uma maneira possível é:
Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
Math.Pow(Physics.Air.Resistance.Y, TimeStep))
* Velocity;
2. Atualizações duplas
Agora verifique o que você faz ao pular (apenas o código relevante é mostrado):
Position += Velocity * TimeStep;
if (Position.Y < 0)
{
Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
Position.Y = Position.Y + Velocity.Y * TimeStep;
}
Você pode ver que TimeStep
é usado duas vezes durante o salto. Isso basicamente dá à bola o dobro do tempo para se atualizar. Isto é o que deveria acontecer:
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. Gravidade
Verifique esta parte do código agora:
if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
Velocity += Physics.Gravity.Force * TimeStep;
}
Você adiciona gravidade por toda a duração do quadro. Mas e se a bola realmente saltar durante esse quadro? Então a velocidade será invertida, mas a gravidade adicionada fará com que a bola acelere para longe do chão! Portanto, o excesso de gravidade terá que ser removido ao saltar e , em seguida, adicionado novamente na direção correta.
Pode acontecer que mesmo adicionar novamente a gravidade na direção correta faça com que a velocidade acelere demais. Para evitar isso, você pode pular a adição de gravidade (afinal, não é muito e dura apenas um quadro) ou a velocidade da pinça para zero.
4. código fixo
E aqui está o código totalmente atualizado:
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. Adições adicionais
Para uma estabilidade de simulação ainda melhor, você pode decidir executar sua simulação de física em uma frequência mais alta. Isso é trivial pelas alterações acima TimeStep
, porque você só precisa dividir seu quadro em quantos blocos desejar. Por exemplo:
public void Update()
{
float TimeStep = 1.0;
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
}