Thursday, October 10, 2019

xna - Ball Physics : Smoothing the final bounces as the ball comes to rest


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.



Answer



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

No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...