Ось кроки, необхідні для вдосконалення циклу моделювання фізики.
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);
}