12
\$\begingroup\$

I've come against another issue in my little bouncing ball game.

My ball is bouncing around fine except for the last moments when it is about to come to rest. The movement of the ball is smooth for the main part but, towards the end, the ball jerks for a while as it settles on the bottom of the screen.

I can understand why this is happening but I can't seem to smooth it.

I'd be grateful for any advice that can be offered.

My update code is:

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

Perhaps I should also point out that Gravity, Resistance Grass and Concrete are all of the type Vector2.

\$\endgroup\$
8
  • \$\begingroup\$ Just to confirm this: your "friction" when the ball hits a surface is a value <1, which is basically the coefficient of restitution correct? \$\endgroup\$ May 18, 2012 at 8:39
  • \$\begingroup\$ @J.C.Leitão - Correct. \$\endgroup\$
    – Ste
    May 18, 2012 at 8:42
  • \$\begingroup\$ Please do not swear to abide the votes when you award bounty and correct answer. Go for whatever helped you. \$\endgroup\$ May 21, 2012 at 9:19
  • \$\begingroup\$ That's a bad way to handle a bounty, basically you're saying that you can't judge yourself so you let the upvotes decide... Anyway, what you are experiencing is a common collision jitter. That can be solved setting a maximum interpenetration amount, a minimum velocity or any other form of 'limit' that once reached will cause your routine to stop the movement and put the object to rest. You may also want to add a resting status to your objects to avoid useless checks. \$\endgroup\$
    – Darkwings
    May 24, 2012 at 13:05
  • \$\begingroup\$ @Darkwings - I think the community in this scenario know better than me as to what is the best answer. That is why the upvotes will influence my decision. Obviously, if I tried the solution with the most upvotes and it didn't help me, then I wouldn't award to that answer. \$\endgroup\$
    – Ste
    May 24, 2012 at 13:16

6 Answers 6

19
+100
\$\begingroup\$

Here a the steps required for improving your physics simulation loop.

1. Timestep

The main problem I can see with your code is that it does not account for the physics step time. It should be obvious that there is something wrong with Position += Velocity; because the units do not match. Either Velocity is actually not a velocity, or something is missing.

Even if your velocity and gravity values are scaled such that each frame happens at a time unit 1 (meaning that eg. Velocity actually means the distance traveled in one second), time must appear somewhere in your code, either implicitly (by fixing the variables so that their names reflect what they really store) or explicitly (by introducing a timestep). I believe the easiest thing to do is to declare the time unit:

float TimeStep = 1.0;

And use that value everywhere it is needed:

Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...

Note that any decent compiler will simplify away the multiplications by 1.0, so that part will not make things slower.

Now Position += Velocity * TimeStep is still not quite exact (see this question to understand why) but it will probably do for now.

Also, this needs to take time into account:

Velocity *= Physics.Air.Resistance;

It is a bit trickier to fix; one possible way is:

Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
                    Math.Pow(Physics.Air.Resistance.Y, TimeStep))
          * Velocity;

2. Double updates

Now check what you do when bouncing (only relevant code shown):

Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Position.Y + Velocity.Y * TimeStep;
}

You can see that TimeStep is used twice during the bounce. This is basically giving the ball twice as much time to update itself. This is what should happen instead:

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. Gravity

Check this part of the code now:

if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
    Velocity += Physics.Gravity.Force * TimeStep;
}            

You add gravity for the whole duration of the frame. But what if the ball actually bounces during that frame? Then velocity will be inverted, but the gravity that was added will then make the ball accelerate away from the ground! So excess gravity will have to be removed when bouncing, then re-added in the correct direction.

It may happen that even re-adding gravity in the correct direction will cause velocity to accelerate too much. To avoid this, you can either skip the gravity addition (after all, it's not that much and it only lasts a frame) or clamp velocity to zero.

4. Fixed code

And here is the fully updated code:

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. Further additions

For even improved simulation stability, you may decide to run your physics simulation at a higher frequency. This is made trivial by the above changes involving TimeStep, because you just need to split your frame in as many chunks as you wish. For instance:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}
\$\endgroup\$
12
  • \$\begingroup\$ "time must appear somewhere in your code." You are advertising that multiplying by 1 all over the place is not just a good idea, it's mandatory? Sure an adjustable timestep is a nice feature, but it most certainly ain't mandatory. \$\endgroup\$ May 22, 2012 at 15:08
  • \$\begingroup\$ @eBusiness: my argument is a lot more about consistency and detecting errors than about adjustable timesteps. I'm not saying multiplying by 1 is necessary, I'm saying velocity += gravity is wrong and only velocity += gravity * timestep makes sense. It may give the same result in the end, but without a comment saying "I know what I'm doing here" it still means a coding error, a sloppy programmer, a lack of knowledge about physics, or just prototype code that needs to be improved. \$\endgroup\$ May 22, 2012 at 15:31
  • \$\begingroup\$ You say it's wrong, when what you supposedly mean to say is that it's bad practice. It's your subjective opinion on the matter, and it's fine that you express it, but it IS subjective as the code in this regard does exactly as it's meant to. All I ask is that you make the difference between the subjective and the objective clear in your post. \$\endgroup\$ May 22, 2012 at 19:41
  • 2
    \$\begingroup\$ @eBusiness: honestly, it is wrong by any sane standard. The code doesn’t ”do as it's meant to” at all, because 1) adding velocity and gravity doesn’t actually mean anything; and 2) if it gives a reasonable result it’s because the value stored in gravity is actually… not gravity. But I can make that clearer in the post. \$\endgroup\$ May 22, 2012 at 20:44
  • \$\begingroup\$ On the contrary, calling it wrong is wrong by any sane standard. You are right that gravity is not stored in the variable named gravity, instead there is a number, and that is all there is ever going to be, it does not have any relation to physics beyond what relation we imagine it has, multiplying it by another number does not change that. What it seemingly does change is your ability and/or willingness to make the mental connection between the code and physics. By the way a rather interesting psychological observation. \$\endgroup\$ May 22, 2012 at 21:21
5
\$\begingroup\$

Add a check to stop the bounce, using a minimal vertical velocity. And when you get the minimal bounce, set the ball in the ground.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}
\$\endgroup\$
4
  • 3
    \$\begingroup\$ I like this solution, but I wouldn't limit bounce to the Y axis. I would calculate the normal of the collider at the collision point and check if the magnitude of the collision velocity is greater than the bounce threshold. Even if the OP's world only allows Y bounces, other users may find a more general solution helpful. (If I'm being unclear, think of bouncing two spheres together at a random point) \$\endgroup\$
    – brandon
    May 9, 2012 at 15:31
  • \$\begingroup\$ @brandon, great, it should work better with normal. \$\endgroup\$
    – Zhen
    May 9, 2012 at 16:49
  • 1
    \$\begingroup\$ @Zhen, if you use the normal of the surface you have the chance that you may have the ball end up sticking to a surface that has a normal that isn't parallel to that of gravity. I would try and factor gravity into the calculation if possible. \$\endgroup\$
    – Nic Foster
    May 9, 2012 at 17:59
  • \$\begingroup\$ None of these solutions should set any velocities to 0. You only limit the reflection across the normal of the vector depending on the bounce threshold \$\endgroup\$
    – brandon
    May 9, 2012 at 18:15
1
\$\begingroup\$

So, I think the problem of why this happening is that your ball is approaching a limit. Mathematically, the ball never stops on the surface, it approaches the surface.

However, your game is not using a continuous time. It is a map, which is using an approximation to the differential equation. And that approximation is not valid in this limiting situation (you can, but you would have to take smaler and smaller time steps, which I assume is not feasible.

Physically speaking, what happens is that when the ball is very close to the surface it sticks to it if the total force is below a given threshold.

@Zhen answer would be fine if your system is homogeneous, which is not. It has some gravity on the y axis.

So, I would say that the solution would not be that the velocity should be bellow a given threshold, but the total force applied on the ball after the update should be bellow a given threshold.

That force is the contribution of the force exerted by the wall on the ball + the gravity.

The condition should then be something like

if (newVelocity + Physics.Gravity.Force < threshold)

notice that newVelocity.y is a positive quantity if the bounce is on the botton wall, and gravity is a negative quantity.

Also notice that newVelocity and Physics.Gravity.Force do not have the same dimensions, as you've written in

Velocity += Physics.Gravity.Force;

meaning that, like you, I'm assuming that delta_time = 1 and ballMass = 1.

Hope this helps

\$\endgroup\$
1
\$\begingroup\$

You have a position update inside your collision check, it is redundant, and wrong. And it adds energy to the ball thus potentially helping it move perpetually. Along with the gravity not being applied at some frames this gives your strange movement. Remove it.

Now you may see a different issue, that the ball gets "stuck" outside the designated area, perpetually bouncing back and forth.

A simple way of solving this issue is to check that the ball moves in the correct direction before changing it.

Thus you should make:

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

Into:

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

And similar for Y direction.

In order for the ball to stop nicely you need to stop the gravity at some point. Your current implementation ensures that the ball will always resurface as gravity does not brake it as long as it is underground. You should change to always applying gravity. This however leads to the ball slowly sinking into the ground after settling. A quick fix for this is, after applying gravity, if the ball is below surface level and moving downwards, stop it:

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

These changes in total should give you a decent simulation. But do note that it is still a very simple simulation.

\$\endgroup\$
0
\$\begingroup\$

Have a mutator method for any and all velocity changes, then within that method you can check the updated velocity to determine if it is moving slow enough to put it at rest. Most physics systems that I know of call this 'restitution'.

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;

In the above method we limit bouncing whenever it's along the same axis as gravity.

Something else to consider would be detecting whenever a ball has collided with the ground, and if it's moving fairly slow at the time of collision, set the velocity along the axis of gravity to zero.

\$\endgroup\$
4
  • \$\begingroup\$ I won't downvote because this is valid, but the question is asking about bounce thresholds, not velocity thresholds. These are almost always separate in my experience because the effect of jittering during bouncing is generally separate from the effect of continuing to calculate velocity once it's visually at rest. \$\endgroup\$
    – brandon
    May 9, 2012 at 15:30
  • \$\begingroup\$ They're one in the same. Physics engines, like Havok, or PhysX, and JigLibX base restitution on linear velocity (and angular velocity). This method should work for any and all movement of the ball, including bouncing. In fact, the last project I was on (LEGO Universe) used a method almost identical to this to stop the bouncing of coins once they had slowed down. In that case we were not using dynamic physics so we had to do it manually rather than letting Havok take care of it for us. \$\endgroup\$
    – Nic Foster
    May 9, 2012 at 15:45
  • \$\begingroup\$ @NicFoster: I'm confused, as to my mind an object could be moving very fast horizontally and hardly at all vertically in which case your method wouldn't trigger. I think the OP would want the vertical distance to be set to zero despite the velocity length being high. \$\endgroup\$ May 9, 2012 at 15:56
  • \$\begingroup\$ @GeorgeDuckett: Ah thank you, I misunderstood the original question. The OP doesn't want the ball to stop moving, just stop the vertical movement. I've updated the answer to account for only bouncing velocity. \$\endgroup\$
    – Nic Foster
    May 9, 2012 at 16:00
0
\$\begingroup\$

Another thing: You're multiplying by a friction constant. Change that--lower the friction constant but add a fixed energy absorption on a bounce. This will damp those last bounces much faster.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .