球物理:在球静止时平滑最终的反弹


12

在我的小弹跳球游戏中,我遇到了另一个问题。

我的球反弹得很好,除了最后一刻即将停止。球的运动对于主体部分来说是平滑的,但是直到最后,当球落在屏幕底部时,它会突然抽动一段时间。

我能理解为什么会这样,但是我似乎无法解决。

如有任何建议,我将不胜感激。

我的更新代码是:

public void Update()
    {
        // Apply gravity if we're not already on the ground
        if(Position.Y < GraphicsViewport.Height - Texture.Height)
        {
            Velocity += Physics.Gravity.Force;
        }            
        Velocity *= Physics.Air.Resistance;
        Position += Velocity;

        if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
        {
            // We've hit a vertical (side) boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Concrete;

            // Invert velocity
            Velocity.X = -Velocity.X;
            Position.X = Position.X + Velocity.X;
        }

        if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
        {
            // We've hit a horizontal boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Grass;

            // Invert Velocity
            Velocity.Y = -Velocity.Y;
            Position.Y = Position.Y + Velocity.Y;
        }
    }

也许我还应该指出这一点GravityResistance Grass并且Concrete都是类型Vector2


只是为了确认这一点:当球击中表面时,您的“摩擦”值小于1,这基本上是恢复系数正确的吗?
豪尔赫·雷涛

@JCLeitão-正确。
2012年

当您授予赏金和正确答案时,请不要发誓遵守投票。寻求帮助您的一切。
aaaaaaaaaaaaa'5

这是处理赏金的不好方法,基本上,您是在说自己无法判断自己,所以让投票决定...无论如何,您遇到的是常见的碰撞抖动。可以通过设置最大互穿量,最小速度或任何其他形式的“极限”来解决,一旦达到该极限,将导致您的例行程序停止运动并使物体静止。您可能还想向对象添加静止状态,以避免无用的检查。
Darkwings,2012年

@Darkwings-在这种情况下,我认为社区比我更了解最佳答案。这就是为什么投票会影响我的决定的原因。显然,如果我用最多的票数尝试了该解决方案,但没有帮助,那么我将不会对此做出任何答覆。
2012年

Answers:


19

这是改善物理仿真循环所需的步骤。

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);
}

“时间必须出现在代码中的某个位置。” 您要宣传的是,在所有地方乘以1不仅是个好主意,还必须吗?当然,可调的时间步长是一个不错的功能,但肯定不是强制性的。
aaaaaaaaaaaa 2012年

@eBusiness:我的论点更多地是关于一致性和检测错误,而不是可调整的时间步长。我并不是说乘以1是必要的,我说的velocity += gravity是错误的,并且只有velocity += gravity * timestep道理。最终可能会得到相同的结果,但是如果没有评论说“我知道我在这里做什么”,那仍然意味着编码错误,草率的程序员,对物理学的了解不足,或者只是原型代码需要有待改进。
sam hocevar

当您想说的是不好的做法时,您就说错了。这是您对此事的主观意见,您可以表达自己的意见,但这是主观的,因为此方面的代码完全符合其意图。我要问的是,您要在帖子中明确主观和客观之间的区别。
aaaaaaaaaaaa 2012年

2
@eBusiness:说实话,任何理智的标准都是错误的。该代码根本没有“按其意图做”,因为1)增加速度和重力实际上没有任何意义;2)如果给出合理的结果,那是因为存储的值gravity实际上是…… 不是重力。但是我可以在帖子中更清楚地说明。
sam hocevar

相反,以任何理智的标准将其称为错误是错误的。没错,重力没有存储在名为gravity的变量中,而是存在一个数字,这就是将要存在的全部,它与物理学没有任何关系,超出了我们想象的关系,将其乘以另一个数字不会改变这一点。看起来所做的改变是您在代码与物理之间建立精神联系的能力和/或意愿。顺便说一下,一个相当有趣的心理观察。
aaaaaaaaaaaa 2012年

6

添加一个检查以最小的垂直速度停止反弹。然后,当您得到最小的弹跳时,将球放在地面上。

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}

3
我喜欢这种解决方案,但我不会将弹跳限制为Y轴。我将计算碰撞点处的对撞机的法线,并检查碰撞速度的大小是否大于反弹阈值。即使OP的世界只允许Y弹跳,其他用户也可能会发现更通用的解决方案很有帮助。(如果我不清楚,不妨考虑在一个随机点将两个球反弹在一起)
布兰登2012年

@brandon,太好了,应该可以正常使用。
2012年

1
@Zhen,如果您使用表面的法线,则可能会导致球最终粘在法线与重力方向不平行的表面上。如果可能的话,我会尝试在计算中考虑重力。
Nic Foster

这些解决方案均不应将任何速度设置为0。您只能根据反弹阈值来限制矢量法线上的反射
Brandon

1

所以,我认为为什么发生这种情况的问题是您的球快要接近极限了。从数学上讲,球永远不会停在表面上,而是会接近表面。

但是,您的游戏不是连续使用时间。这是一张地图,它使用的是微分方程的近似值。而且这种近似在这种局限的情况下是无效的(您可以,但是您将不得不采取更小更小的时间步长,我认为这是不可行的。

从物理上讲,发生的情况是,当球非常靠近表面时,如果总力低于给定阈值,则球会粘附在表面上。

如果您的系统是同构的,则@Zhen答案会很好,而事实并非如此。它在y轴上有一些重力。

因此,我想说的解决方案不是将速度控制在给定阈值以下,而是将更新后施加在球上的总力控制在给定阈值以下。

该力是壁施加在球上的力加上重力的贡献。

条件应该是这样的

如果(newVelocity + Physics.Gravity.Force <阈值)

请注意,如果弹跳在波顿壁上,则newVelocity.y为正数,而重力为负数。

还要注意,newVelocity和Physics.Gravity.Force的尺寸与您编写的尺寸不同

Velocity += Physics.Gravity.Force;

这意味着像您一样,我假设delta_time = 1且ballMass = 1。

希望这可以帮助


1

您在碰撞检查中有位置更新,这是多余的,而且是错误的。而且它为球增加了能量,因此有可能帮助其永久移动。再加上某些框架上没有施加重力,这给您带来奇怪的运动。去掉它。

现在,您可能会看到一个不同的问题,球被“卡住”在指定区域之外,并不断地来回弹跳。

解决此问题的一种简单方法是在更换球之前检查球是否朝正确的方向移动。

因此,您应该:

if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)

进入:

if ((Position.X < 0 && Velocity.X < 0) || (Position.X > GraphicsViewport.Width - Texture.Width && Velocity.X > 0))

与Y方向相似。

为了使球平稳停止,您需要在某些时候停止重力。您当前的实现方式可以确保球始终会重新浮出水面,因为只要重力在地下,它就不会制动。您应该更改为始终应用重力。但是,这导致球在沉降后缓慢沉入地面。快速解决方法是在施加重力之后,如果球低于表面水平并向下移动,请停止它:

Velocity += Physics.Gravity.Force;
if(Position.Y > GraphicsViewport.Height - Texture.Height && Velocity.Y > 0)
{
    Velocity.Y = 0;
}

总的来说,这些变化将为您提供不错的模拟效果。但是请注意,它仍然是一个非常简单的模拟。


0

对于任何和所有速度变化,都有一个变幅器方法,然后您可以在该方法中检查更新的速度,以确定它的移动速度是否足够慢以使其静止。我所知道的大多数物理系统都称为“复原”。

public Vector3 Velocity
{
    public get { return velocity; }
    public set
    {
        velocity = value;

        // We get the direction that gravity pulls in
        Vector3 GravityDirection = gravity;
        GravityDirection.Normalize();

        Vector3 VelocityDirection = velocity;
        VelocityDirection.Normalize();

        if ((velocity * GravityDirection).SquaredLength() < 0.25f)
        {
            velocity.Y = 0.0f;
        }            
    }
}
private Vector3 velocity;

在上述方法中,我们限制了与重力在同一轴上的弹跳。

还需要考虑的其他事项是,只要球与地面碰撞,并且在碰撞时球的移动速度相当缓慢时,将沿重力轴的速度设置为零即可。


我不会拒绝投票,因为这是有效的,但问题是询问反弹阈值,而不是速度阈值。在我的经验中,这些几乎总是分开的,因为弹跳期间的抖动效果通常与在视觉上静止后继续计算速度的效果是分开的。
布兰登2012年

他们是同一个人。物理引擎,例如Havok或PhysX,以及JigLibX,都是基于线速度(和角速度)进行恢复的。此方法应适用于球的任何和所有运动,包括弹跳。实际上,我参与的最后一个项目(LEGO Universe)使用的方法几乎与此相同,一旦硬币放慢速度,它们便停止弹跳。在那种情况下,我们没有使用动态物理学,因此我们必须手动进行操作,而不是让Havok为我们处理它。
尼克·福斯特

@NicFoster:我很困惑,因为我认为一个对象可能在水平方向上非常快地移动,而在垂直方向上几乎没有移动,在这种情况下您的方法不会触发。我认为尽管速度长度很高,OP还是希望将垂直距离设置为零。
乔治·达基特

@GeorgeDuckett:谢谢你,我误解了原来的问题。OP不想让球停止运动,只需停止垂直运动即可。我已经更新了答案,只考虑了弹跳速度。
Nic Foster 2012年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.