ここでは、物理シミュレーションループを改善するために必要な手順を説明します。
1.タイムステップ
コードで見られる主な問題は、物理ステップ時間を考慮していないことです。Position += Velocity;
単位が一致しないため、何か問題があることは明らかです。どちらかVelocity
実際の速度ではないか、何かが欠けています。
あなたの速度と重力の値は、各フレームは、時間単位で行われるようにスケーリングされていても1
(つまり例えば、 Velocity
実際に意味の距離、1秒で移動)時間がどこに表示されなければならないのいずれかで、コード内で暗黙のように変数を固定することにより(それらの名前は、実際に保存されているものを反映している)、または明示的に(タイムステップを導入することにより)。最も簡単なことは、時間単位を宣言することだと思います。
float TimeStep = 1.0;
そして、その値を必要なすべての場所で使用します。
Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...
適切なコンパイラーは、による乗算を単純化する1.0
ので、その部分が遅くなることはありません。
今Position += Velocity * TimeStep
はまだ完全に正確ではありません(理由を理解するためにこの質問を参照してください)が、おそらく今のところはそうでしょう。
また、これには時間を考慮する必要があります。
Velocity *= Physics.Air.Resistance;
修正するのは少し難しいです。可能な方法の1つは次のとおりです。
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
バウンス中に2回使用されることがわかります。これは基本的に、ボールを更新する時間を2倍にしています。これは代わりに起こるべきことです:
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);
}