Warning: You should have a good understanding of the basic tutorials before venturing further.

Suspension calculation

Wheeled vehicles typically have damped spring suspension which means that the distance between the chassis and the wheel will tend toward a specific rest position, but is limited in how fast it can move there. This kind of movement is simulated very well by Box2D's wheel joint. The distance joint also has similar behaviour to the linear part of the wheel joint. If you want to keep two bodies in alignment and with a suspension-like movement you could use a prismatic joint and a distance joint together.

But how about if you don't want any joints at all between the two bodies? A good example of this is a 'hovercar' type behavior where a body is suspended above the ground as if on a dampened spring when it is close to the ground, but there is nothing to stop it from moving further upwards. This means that the body could be above anything so we don't really want to make any joints between it and the ground.

In this topic we'll look at what forces should be applied to a body to get this kind of movement. We'll use a raycast to find out what is below the body and how far away it is. Although the example will look at a 'hovercar' scenario it's worth noting that you can do the same thing for player characters where the character is made from two bodies joined by a prismatic joint, instead of using a distance joint. The nice thing about that method is you can alter the target height to get the character to crouch, and you can customize the way the height is changed (eg. make it quicker to crouch than to stand up, etc).

The basics

After finding out how far the body is from the ground, all we really need to do is apply a force of the right magnitude. When the body is close to its target height we don't want it to move much so the force will be very small - think of this as a spring at its rest length - and when the body is close to the ground the force should be large.

Hovercar suspension

A common way for calculating the force of a spring is to multiply the distance from the rest position by a 'spring constant' which specifies how strong the spring is. So to achieve this it's just a matter of calling ApplyForce with a force calculated from the distance below the target height. At this point let's do a little coding to get us that far for now.

// Create ground
const groundDef = b2DefaultBodyDef();
const groundId = b2CreateBody(worldId, groundDef);

const edgeShape = {
    points: [
        new b2Vec2(-20, 0),
        new b2Vec2(20, 0)
    ]
};

const groundShapeDef = b2DefaultShapeDef();
b2CreateSegmentShape(groundId, groundShapeDef, edgeShape);

// Create hovercar
const bodyDef = b2DefaultBodyDef();
bodyDef.type = DYNAMIC;
bodyDef.fixedRotation = true;
bodyDef.position = new b2Vec2(0, 10);

const hovercarId = b2CreateBody(worldId, bodyDef);

const shapeDef = b2DefaultShapeDef();
shapeDef.density = 1;

const boxShape = b2MakeBox(2, 0.5); // 4x1 box
b2CreatePolygonShape(hovercarId, shapeDef, boxShape);

Now in the Step function we can cast a ray downwards to find the ground and apply a force depending on how far away it is:

const targetHeight = 3;
const springConstant = 100;

// Make the ray at least as long as the target distance
const startOfRay = b2Body_GetPosition(hovercarId);
const endOfRay = new b2Vec2(startOfRay.x, startOfRay.y - 5);

const rayInput = {
    origin: startOfRay,
    translation: new b2Vec2(endOfRay.x - startOfRay.x, endOfRay.y - startOfRay.y)
};

const filter = b2DefaultQueryFilter();
const rayResult = b2World_CastRayClosest(worldId, rayInput.origin, rayInput.translation, filter);

if (rayResult.hit) {
    const distanceAboveGround = Math.sqrt(
        Math.pow(startOfRay.x - rayResult.point.x, 2) + 
        Math.pow(startOfRay.y - rayResult.point.y, 2)
    );

    // Don't do anything if too far above ground
    if (distanceAboveGround < targetHeight) {
        const distanceAwayFromTargetHeight = targetHeight - distanceAboveGround;
        b2Body_ApplyForceToCenter(
            hovercarId,
            new b2Vec2(0, springConstant * distanceAwayFromTargetHeight),
            true
        );
    }
}

Hovercar suspension

The extra touch

Well that's a good start, but you'll notice that with only a spring to affect the body, it keeps bouncing forever. This is because there is no damping on the body which means that even though we are reducing the force applied as it gets closer to the target height, all the accumulated force we've applied in the previous time steps has not yet been countered by gravity.

One way to fix this is to use SetLinearDamping on the body, but this would cause the motion to be affected all the time, even when the body is way above the ground. We could also manually apply a vertical damping to the velocity of the body when it is within range of the ground, but really what we're trying to do is scale down the force, not damp velocity.

A better way is the 'look-ahead' technique used in the rotate to angle topic, to take into account the current velocity of the body. This will cause the calculated force to scale down earlier as the rest position is approached. Let's try that.

// After checking if distanceAboveGround < targetHeight
// Replace distanceAboveGround with the 'look ahead' distance
// This will look ahead 0.25 seconds - longer gives more 'damping'
const velocity = b2Body_GetLinearVelocity(hovercarId);
distanceAboveGround += 0.25 * velocity.y;

Hovercar suspension

The finale

If you are very observant you might have noticed that the body does not come to rest at the height we wanted. This is because as we push up, gravity pushes down in a continual struggle. Rather than coming to rest at the position where the hover force we're applying would be zero, the body will come to rest at the position where our hover force and gravity are equal. So for the example above, rather than settling at a height of 3 units, the body will settle at a height of 2.6 units.

This could be countered by altering the spring constant, but to really get at the root of the problem it would be cleaner to cancel gravity for the body:

// After checking if distanceAboveGround < targetHeight
// Negate gravity
const gravity = b2World_GetGravity(worldId);
const mass = b2Body_GetMass(hovercarId);
b2Body_ApplyForceToCenter(
    hovercarId,
    new b2Vec2(-mass * gravity.x, -mass * gravity.y),
    true
);

Other considerations

With the simple example we have here, you'll find that the body is very sensitive to the surface contours, because there is only one ray. For example, in the situation below the ray can fit between the two boxes and is not seeing a good representation of the ground below. You would probably want to have more than one ray along the bottom of the body and take the average/minimum of them or something like that to avoid this problem.

Hovercar suspension

Credits

This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.