这是改善物理仿真循环所需的步骤。
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);
}