Вот шаги, необходимые для улучшения вашего цикла симуляции физики.
1. Временной шаг
Основная проблема, которую я вижу в вашем коде, заключается в том, что он не учитывает время шага по физике. Должно быть очевидно, что здесь что-то не так, Position += Velocity;
потому что единицы не совпадают. Либо Velocity
это на самом деле не скорость, либо чего-то не хватает.
Даже если ваши значения скорости и гравитации масштабируются таким образом, чтобы каждый кадр происходил в единицу времени 1
(что означает, например, что Velocity
фактически означает расстояние, пройденное за одну секунду), время должно появляться где-то в вашем коде, либо неявно (путем фиксации переменных так, чтобы их имена отражают то, что они на самом деле хранят) или явно (путем введения временного шага). Я считаю, что проще всего объявить единицу времени:
float TimeStep = 1.0;
И используйте это значение везде, где это необходимо:
Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...
Обратите внимание, что любой приличный компилятор упростит умножения на 1.0
, так что часть не будет замедлять работу.
Сейчас Position += Velocity * TimeStep
все еще не совсем точно (см. Этот вопрос, чтобы понять, почему), но это, вероятно, будет делать пока.
Кроме того, это должно учитывать время:
Velocity *= Physics.Air.Resistance;
Это немного сложнее исправить; Один из возможных способов:
Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
Math.Pow(Physics.Air.Resistance.Y, TimeStep))
* Velocity;
2. Двойные обновления
Теперь проверьте, что вы делаете, когда подпрыгиваете (показан только соответствующий код):
Position += Velocity * TimeStep;
if (Position.Y < 0)
{
Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
Position.Y = Position.Y + Velocity.Y * TimeStep;
}
Вы можете видеть, что TimeStep
используется дважды во время отскока. Это в основном дает мячу вдвое больше времени для обновления. Вот что должно произойти вместо этого:
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. Гравитация
Проверьте эту часть кода сейчас:
if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
Velocity += Physics.Gravity.Force * TimeStep;
}
Вы добавляете гравитацию на всю продолжительность кадра. Но что, если мяч действительно отскакивает во время этого кадра? Тогда скорость будет инвертирована, но гравитация, которая была добавлена, заставит шар ускоряться от земли! Таким образом, избыточная гравитация должна быть удалена при подпрыгивании , а затем добавлена в правильном направлении.
Может случиться, что даже повторное добавление силы тяжести в правильном направлении приведет к слишком сильному ускорению скорости. Чтобы избежать этого, вы можете либо пропустить гравитационное прибавление (в конце концов, оно не так уж много, и оно длится только кадр), либо ограничить скорость до нуля.
4. Фиксированный код
А вот и полностью обновленный код:
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. Дальнейшие дополнения
Для еще большей стабильности симуляции вы можете решить запустить симуляцию физики на более высокой частоте. Это делается тривиально из-за вышеуказанных изменений TimeStep
, потому что вам просто нужно разбить ваш кадр на столько кусков, сколько вы хотите. Например:
public void Update()
{
float TimeStep = 1.0;
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
Update(TimeStep / 4);
}