Halt! You should have a good understanding of the basic tutorials before venturing further.

Here's another question which comes up quite frequently regarding projectile motion in Box2D - "When a body is thrown or launched in the air, how can I tell...

  • what path it will follow?
  • the maximum height it will reach?
  • how fast to launch it to reach a desired height?
  • what it will hit?
  • how it will bounce?

The reason for wanting to know this is usually either when the player is controlling the projectile and they will be shown a visual indicator of how the motion will play out, or when the computer is in charge and it needs to figure out how to launch something so it can pretend to be clever.

There are two ways this information can be found. One is to use a basic projectile motion formula and plug in the variables you know to find the one you don't. The other way is to fully simulate the motion of the object by running the Box2D world step to see how it will move. This topic will cover the first of these methods. Let's first take a quick look at the pros and cons of both the 'plugin formula' and the 'fully simulated world' methods.

The advantage of the plugin formula method is obviously that it requires less processing time, and is much easier to set up because there is no tricky management of world state required. The disadvantage is that if there are other dynamic objects in the world, the plugin formula method cannot actually answer the last two of the questions above: what will it hit and how will it bounce. Since we are looking into the future, in order to know what the projectile will hit we would also need to know the future positions of all the other things moving around in the scene too.

On the other hand, the fully simulated world method can answer all of the questions above because it does know the future positions of all the other objects in the scene... but this information comes at a cost. Firstly it could be quite a processing intensive task - as the user moves the projected trajectory around, the mouse move updates (or whatever input is being used) could be generated dozens of times per second. If you want show a projected trajectory for the next one second at 60fps this would mean you need to copy the entire world state and step it forwards 60 times, for every mouse movement. If your world is not too complex this is probably still fine, but there is also the extra work involved in making a copy of the world to use for this stepping forwards. This would require some kind of serialization process to go through the current world and make a duplicate of all the bodies and shapes into a new temporary world (if you are interested in this method you may find the b2djson loader helpful) but due to Box2D's warm starting optimizations there is no guarantee that the copied world and the original will behave exactly the same. Finally, this method does not give you a direct formulaic answer to questions 2 and 3 above - for example if you want to know the maximum height reached, or how high the projectile will be at time x, you have to run all the world steps up to that point in order to find out.

So that is the reasoning for going with the plugin formula method for this topic. Just remember that if you really do need full knowledge of how the projectile will interact with other dynamic objects, you will need to use the fully simulated world approach. But in most cases I'm guessing it's enough to show the user a predicted path or let the 'AI' do something sensible.

One more thing before we get started - you cannot predict anything if you are using a variable length time step. To use the plugin formula method to make predictions about the trajectory you will need to be using the same time step size for every step. Yeah that should go without saying but I said it anyway.

The Formula

To make a long story short, the formula we'll use to find a point at a given time along the trajectory path is as follows. (Click here if you're interested in the long story.)

projected trajectory diagram

where:

  • p(n) is the position at the nth time step
  • p0 is the starting position (0th time step)
  • v is the starting velocity per time step
  • a is the acceleration due to gravity per time step

I have seen a few people wondering why their trajectory prediction isn't quite giving them the result they were expecting, and I think mostly it's due to overlooking the very important point that everything in Box2D takes discrete movements between time steps. You cannot use a formula based on seconds, and plugin a value like 2.62 seconds to get an accurate value that will match how the Box2D body will advance along its projectile parabola.

However you could note that 2.62 seconds is 157 time steps (at 60fps) and use a formula like the one above to reproduce the same calculation that Box2D will actually carry out. It may not be obvious at first, but the reason that using seconds is unreliable is because the rate of acceleration per time step is different depending on how many time steps per second are used.

As an example, suppose you have an object at height 0, and you let it drop under a gravity of -10m/s/s for one second. No matter what time step you choose the velocity after one second will be -10m/s because this is what gravity is defined as, but the resulting position will be different depending on how many position and velocity updates were allowed during that one second time span:

Box2D projected trajectory comparison

On the left, the time step is much larger than on the right, so less steps are allowed per second to reach the required -10m/s, which results in less distance covered overall. Anyway, just remember to be wary of using seconds and we'll be fine.

Drawing a Projected Trajectory

Most people coming to this page will be looking to make a visual indicator to show where a launched projectile will travel, so let's do that right away, then we can get to some trickier things after that. This is quite simple - we put the formula above into a function to make it a little more convenient:

function getTrajectoryPoint(startingPosition, startingVelocity, n) {
    // velocity and gravity are given per second but we want time step values here
    const t = 1 / 60.0; // seconds per time step (at 60fps)
    stepVelocity = new b2Vec2(t * startingVelocity.x, t * startingVelocity.y); // m/s

    const gravity = b2World_GetGravity(worldId);
    stepGravity = new b2Vec2(t * t * gravity.x, t * t * gravity.y); // m/s/s

    return new b2Vec2(
        startingPosition.x + n * stepVelocity.x + 0.5 * (n * n + n) * stepGravity.x,
        startingPosition.y + n * stepVelocity.y + 0.5 * (n * n + n) * stepGravity.y
    );
}

And now it's easy to draw a projected trajectory line, for example:

// Using a canvas context for drawing
ctx.strokeStyle = '#FFFF00';
ctx.beginPath();
for (let i = 0; i < 180; i++) { // three seconds at 60fps
    const trajectoryPosition = getTrajectoryPoint(startingPosition, startingVelocity, i);
    if (i === 0) {
        ctx.moveTo(trajectoryPosition.x, trajectoryPosition.y);
    } else {
        ctx.lineTo(trajectoryPosition.x, trajectoryPosition.y);
    }
}
ctx.stroke();

Box2D projected trajectory visualization

To satisfy yourself that we've got the exact locations correct for the prediction, pause the simulation and zoom in to check that the center of the box (green dot) is right on the end of one of the yellow dashed lines at every step.

Box2D trajectory closeup

In a real application you would probably only need to draw every nth position, instead of drawing every position like in the example above. The plugin formula is handy for this because we can just calculate the points for any time in the future we want instead of needing to run through the whole simulation in sequence to find them.

Box2D trajectory spacing example

What Will It Hit?

As mentioned already, if there are other dynamic bodies moving around in the world, there is no way to correctly know what the projectile will hit without running the full simulation for the whole world. However in many cases it may just be good enough to show the user the first point that the trajectory would intersect something in the current state of the world.

This is pretty simple to accomplish by adding a raycast check to the loop which draws the projected trajectory. All you need to do is cast a ray between successive points along the trajectory until something is hit, and then stop drawing. Raycasting has been covered in other topics so I will direct you there for the details.

// Create raycast callback
const raycastCallback = {
    hit: false,
    point: new b2Vec2(0, 0);
};

let lastTP = startingPosition;

// Begin drawing lines
ctx.beginPath();
for (let i = 0; i < 180; i++) {
    const trajectoryPosition = getTrajectoryPoint(startingPosition, startingVelocity, i);

    if (i > 0) { // avoid degenerate raycast where start and end point are the same
        const rayResult = b2World_CastRayClosest(worldId, lastTP, trajectoryPosition, {
            categoryBits: 0xFFFF,
            maskBits: 0xFFFF
        });

        if (rayResult.hit) {
            ctx.lineTo(rayResult.point.x, rayResult.point.y);
            break;
        }
    }

    ctx.lineTo(trajectoryPosition.x, trajectoryPosition.y);
    lastTP = trajectoryPosition;
}
ctx.stroke();

This should allow to you find the first point at which the trajectory will intersect something in the world, as it currently exists (not in future steps!). You can also obtain the fixture that was hit by the raycast if necessary.

A trajectory hits a box

One thing to keep in mind if you do this, is that the intersection point is only calculated from the single-line path of the trajectory, which in this case is the center of the body in motion. The actual first impact point between the body and the thing it hits will depend on what shapes the body has and how they are positioned.

the shape hits before the trajectory because the shape extends out from the center-of-mass

Nevertheless, in many situations this would be enough to show the user a second trajectory calculated from how the body would bounce. For example if the projectile is a circle and the thing it hits is static, then the direction it will bounce off can be found with reasonable accuracy.

How High Will It Go?

There are two related things we can look at here. One is finding out how high a projectile will get at its highest point, given a certain starting velocity - this is very similar to what we just did above. But probably the more common question people have is the opposite - how fast should I launch it so that it reaches a specific height?

These can both be answered by noting that when the projectile reaches its highest point, the velocity will be zero for a brief moment before it comes back down. If we could find out what the time step was at that time, we could use the original formula above to get the height.

The velocity part of projectile motion can be shown to be:

Box2D velocity formula

where:

  • v(n) is the velocity at the nth time step
  • v is the starting velocity per time step
  • a is the acceleration due to gravity per time step

We want to know what n is when v(n) is zero, so solving for this gives us:

Box2D time step formula

This will give us the number of time steps until maximum height is reached, then we can use that in the first formula, like this:

function getMaxHeight(startingPosition, startingVelocity) {
    // if the projectile is already heading down, this is as high as it will get
    if (startingVelocity.y < 0) {
        return startingPosition.y;
    }

    // velocity and gravity are given per second but we want time step values here
    const t = 1 / 60.0;
    stepVelocity = new b2Vec2(t * startingVelocity.x, t * startingVelocity.y); // m/s

    const gravity = b2World_GetGravity(worldId);
    stepGravity = new b2Vec2(t * t * gravity.x, t * t * gravity.y); // m/s/s

    // find n when velocity is zero
    const n = -stepVelocity.y / stepGravity.y - 1;

    // plug n into position formula, using only vertical components
    return startingPosition.y + n * stepVelocity.y + 0.5 * (n * n + n) * stepGravity.y;
}

a trajectory travels up to the max height line then falls back down again

How Fast Should It Be Launched to Reach a Desired Height?

From the previous section, we know the maximum height is reached when the velocity gets to zero and the object starts to fall down again, and we have a way to know how many time steps that will take for a certain launch velocity. But for this question we don't really care how long it takes to get to the maximum height, we care about the the initial velocity.

So taking the two equations from the previous section and substituting the 'timesteps to reach max height' expression into the original position formula, we can express the maximum height in terms of a and v only: (here I've ignored the initial position for clarity)

where:

  • d is the vertical movement made before reaching the maximum height
  • v is the starting velocity per time step
  • a is the acceleration due to gravity per time step

Okay, that form is easy to use if we wanted to know the height, but we already know the height (d) and we want to know the velocity. Unfortunately there is no way to rearrange this to give a single easy solution for v, but if we note that this a quadratic equation (ax² + bx + c = 0) for v, we can use the quadratic formula to solve it.

Box2D quadratic formula

Here is an example of how this could be done:

function calculateVerticalVelocityForHeight(desiredHeight) {
    if (desiredHeight <= 0) {
        return 0; // wanna go down? just let it drop
    }

    // gravity is given per second but we want time step values here
    const t = 1 / 60.0;
    const gravity = b2World_GetGravity(worldId);
    stepGravity = new b2Vec2(t * t * gravity.x, t * t * gravity.y); // m/s/s

    // quadratic equation setup (ax² + bx + c = 0)
    const a = 0.5 / stepGravity.y;
    const b = 0.5;
    const c = desiredHeight;

    // check both possible solutions
    const discriminant = Math.sqrt(b * b - 4 * a * c);
    const quadraticSolution1 = (-b - discriminant) / (2 * a);
    const quadraticSolution2 = (-b + discriminant) / (2 * a);

    // use the one which is positive
    let v = quadraticSolution1;
    if (v < 0) {
        v = quadraticSolution2;
    }

    // convert answer back to seconds
    return v * 60.0;
}

In the example at the end of this topic, the computer 'player' adjusts the vertical component of its launch velocity so that the maximum height of the ball it fires will just clear the corner of the the blue golf-tee-like thingy. Once the vertical launch velocity has been determined, the time the projectile will take to reach the top of its arc is also known, so the horizontal component of the velocity is set so that the ball arrives at the edge of the ledge just as it reaches the maximum height.

The goal of this setup of course is to make the ball land on the tee and stay there, and if the horizontal velocity is not too large, the calculations we have looked at in this topic actually do a reasonable job of it. Because the projected trajectory only considers the center of the ball, it can hit the underside of the tee as it comes up, so there is a limited range in which the ball can land correctly with this simple technique.

a ball and a golf-tee with vectors

Update: About the demo mentioned above where a ball is launched to land on the v-shaped target, the example code sets the target point to be exactly at the apex of the trajectory. The function calculateVerticalVelocityForHeight is used to find the vertical component of the launch speed, and the time taken to arrive at the maximum height is then calculated using the n = -v/a-1 formula above (see the 'getTimeToTop' function in the source code). Once we know how long the vertical part of the trajectory will take, we can set the horizontal component of the launch velocity so that the body will cover the horizontal distance to the target in the same time. You could think of this as the left half of the trajectory parabola below - eg. the h1 part of this diagram:

Box2D trajectory diagram

If you want to calculate a trajectory that goes up and then down again to arrive at a target point, you can repeat this process for each side of the parabola. First, you need to decide what the maximum height should be at the apex. Then you can do calculateVerticalVelocityForHeight for each side, from which you can find the time required for each side. Adding these times together will give you the total time taken for the vertical component of the trajectory to reach the target, from which you can set the horizontal component of the launch velocity as before. (If the target point is below the starting point and you don't want the body to go up at all, you only need to consider the falling half of the parabola.)

Source Code

// Trajectory test for Box2D v3.0
// Note: Uses 60fps fixed time steps

class TrajectoryTest {
    constructor() {
        // World setup
        gravity = new b2Vec2(0, -10 );
        const worldDef = b2DefaultWorldDef();
        worldDef.gravity = gravity;
        this.world = b2CreateWorld(worldDef);

        // Ground body
        const groundDef = b2DefaultBodyDef();
        const ground = b2CreateBody(this.world, groundDef);

        // Ground shape
        const groundShape = b2DefaultShapeDef();
        const groundBox = b2MakeBox(40.0, 1.0);
        b2CreatePolygonShape(ground, groundShape, groundBox);

        // Projectile body
        const bodyDef = b2DefaultBodyDef();
        bodyDef.type = DYNAMIC;
        bodyDef.position = new b2Vec2(-35.0, 5.0 );
        this.body = b2CreateBody(this.world, bodyDef);

        // Projectile shape
        const shapeDef = b2DefaultShapeDef();
        shapeDef.density = 1.0;
        const circle = { radius: 0.2 };
        b2CreateCircleShape(this.body, shapeDef, circle);

        // Initial velocity
        velocity = new b2Vec2(50.0, 25.0 );
        b2Body_SetLinearVelocity(this.body, velocity);
    }

    // Get predicted trajectory points
    getTrajectoryPoints() {
        const points = [];
        const timeStep = 1.0 / 60.0;
        const position = b2Body_GetPosition(this.body);
        const velocity = b2Body_GetLinearVelocity(this.body);

        // Create temporary world for prediction
        const worldDef = b2DefaultWorldDef();
        worldDef.gravity = b2World_GetGravity(this.world);
        const tempWorld = b2CreateWorld(worldDef);

        // Create temporary body
        const bodyDef = b2DefaultBodyDef();
        bodyDef.type = DYNAMIC;
        bodyDef.position = position;
        const tempBody = b2CreateBody(tempWorld, bodyDef);
        b2Body_SetLinearVelocity(tempBody, velocity);

        // Add shape
        const shapeDef = b2DefaultShapeDef();
        shapeDef.density = 1.0;
        const circle = { radius: 0.2 };
        b2CreateCircleShape(tempBody, shapeDef, circle);

        // Simulate and record positions
        for (let i = 0; i < 180; i++) {
            b2World_Step(tempWorld, timeStep, 1);
            points.push(b2Body_GetPosition(tempBody));
        }

        b2DestroyWorld(tempWorld);
        return points;
    }

    // Launch projectile
    launch(angle) {
        const speed = 50.0;
        velocity = new b2Vec2(speed * Math.cos(angle), speed * Math.sin(angle));
        b2Body_SetLinearVelocity(this.body, velocity);
    }

    // Step simulation
    step() {
        const timeStep = 1.0 / 60.0;
        b2World_Step(this.world, timeStep, 8, 3);
    }
}

This test creates a projectile that can be launched at different angles and predicts its trajectory. The simulation uses fixed 60fps time steps for consistent results. The trajectory prediction creates a temporary world to simulate the projectile's path without affecting the main simulation.

Credits

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