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

This topic will look at a few ways to do explosions in Box2D. This makes use of a variety of techniques already covered in other topics, so here we'll focus on broader design aspects and the pros and cons of each method rather than look at the actual coding in detail. By now your brain is probably so full of Box2D info that you have to wear earplugs to keep it from falling out anyway.

Simulating an explosion comes down to finding out what bodies are in the vicinity of the blast and applying an impulse to push them away from the blast location. In this topic we will look at three methods of varying complexity:

  • Proximity - find bodies in range
  • Raycast - find bodies in range and line-of-sight
  • Particles - spray a bunch of small bodies outward from the blast location

Actually, in the last of these methods we don't need to find what bodies are in range because the physics engine does the work for us.

A circle 'bomb' in a rich environment full of bodies to blow up

Applying a blast impulse

For this topic I did not get too technical with any formulas for how pressure (and therefore impulse) changes relative to the distance from the blast point, but we know that the area of a circle increases relative to the square of the radius. An explosion causes a finite amount of gas to expand until the pressure with the surrounding air is equalized, which will be when it takes up the same volume (area for 2d). So the pressure should decrease inversely to the square of the radius... okay maybe I did get a little technical.

Anyway, we could put this in a function like:

function applyBlastImpulse(bodyId, blastCenter, applyPoint, blastPower) {
    const blastDir = b2Sub(applyPoint, blastCenter);
    const { normal, length } = b2GetLengthAndNormalize(blastDir);

    // ignore bodies exactly at the blast point - blast direction is undefined
    if (length === 0) {
        return;
    }

    const invDistance = 1 / length;
    const impulseMag = blastPower * invDistance * invDistance;
    const impulse = b2MulSV(impulseMag, normal);

    b2Body_ApplyLinearImpulse(bodyId, impulse, applyPoint, true);
}

There are some other considerations to take into account, such as limiting the impulse to a maximum value etc, but these have been left out here for clarity.

Proximity method

The simplest approach is to find all bodies within a certain distance of the blast location. To define this a little more, we want bodies with their center of mass within range. We could use an area query to efficiently find all dynamic bodies with a shape in the area around the blast point. Handily enough, this simple approach has been made even easier because Box2D contains a function to do it for us!

/**
 * @function b2World_Explode
 * @summary Creates an explosion effect that applies forces to nearby dynamic bodies
 * @param {b2WorldId} worldId - The ID of the Box2D world
 * @param {b2Vec2} position - The center point of the explosion
 * @param {number} radius - The radius of the explosion effect
 * @param {number} magnitude - The force magnitude of the explosion
 * @returns {void}
 * @description
 * Creates a circular explosion centered at the given position that applies radial forces
 * to dynamic bodies within the explosion radius. The explosion force decreases with
 * distance from the center point.
 * @throws {Error} Throws assertion errors if:
 * - position is invalid
 * - radius is invalid or <= 0
 * - magnitude is invalid
 * - world is locked
 */

function b2World_Explode(worldId, position, radius, magnitude);

Now let's look at the results this gives us. Here is a screenshot showing which bodies in the test scene will be given an impulse by this method:

Explosions

This is a pretty good start, but there is a problem... the blast can travel through the platforms, the ground, and other obstructing objects.

Raycast method

We can improve on this by using raycasts to find which bodies to interact with instead of a simple distance check. Check out the topic on ray casting and the related section in the world querying topic for implementation details.

function applyBlastRaycast(worldId, center, blastRadius, blastPower, numRays) {
    for (let i = 0; i < numRays; i++) {
        const angle = (i / numRays) * 360 * (Math.PI / 180);
        const rayDir = new b2Vec2(Math.sin(angle), Math.cos(angle));
        const rayEnd = new b2Vec2(center.x + blastRadius * rayDir.x, center.y + blastRadius * rayDir.y);

        // Cast ray and get closest hit
        const hit = b2World_CastRayClosest(worldId, center, rayEnd);
        if (hit && hit.body) {
            applyBlastImpulse(
                hit.body, 
                center, 
                hit.point, 
                blastPower / numRays
            );
        }
    }
}

Notice we divide the blast magnitude by the number of rays - this is just to make it easier to adjust the number of rays and without changing the overall blast effect. Let's see how this goes in the test scene with 32 rays:

Explosions

Good, no magical blasting through solid walls anymore, so the boxes above the platforms are correctly not affected. The number of rays can be adjusted to allow the blast to reach between smaller gaps, and to balance CPU usage with the quality of result.

Particle method

The last method we'll try in this topic is somewhat different to the first two. Instead of checking what bodies are around the blast point, we'll just create a bunch of small bodies to simulate quickly expanding air particles, and let them fly. This is a closer simulation of what actually happens in an explosion so we can expect it to look more realistic.

Not only does this method gives great results, but it's also easy to implement because most of the work is done by the physics engine. On the other hand, more work done by the engine means more CPU time used. Here is a typical example of how you might set up these particle bodies:

function createBlastParticles(worldId, center, blastPower, numRays) {
    for (let i = 0; i < numRays; i++) {
        const angle = (i / numRays) * 360 * (Math.PI / 180);
        const rayDir = { 
            x: Math.sin(angle), 
            y: Math.cos(angle) 
        };

        // Create body
        const bodyDef = b2DefaultBodyDef();
        bodyDef.type = DYNAMIC;
        bodyDef.position = center;  // start at blast center
        bodyDef.linearVelocity = b2MulSV(blastPower, rayDir);
        bodyDef.linearDamping = 10; // drag due to moving through air
        bodyDef.gravityScale = 0; // ignore gravity
        bodyDef.bullet = true;  // prevent tunneling at high speed
        bodyDef.fixedRotation = true; // rotation is not needed

        const bodyId = b2CreateBody(worldId, bodyDef);

        // Create circle shape
        const circle = new b2Circle(new b2Vec2(0,0), 0.05); // tiny

        const shapeDef = b2DefaultShapeDef();
        shapeDef.density = 60 / numRays;  // very high - shared across all particles
        shapeDef.friction = 0;  // friction is not needed
        shapeDef.restitution = 0.99;  // high 'bounce'
        shapeDef.filter = b2DefaultFilter();
        shapeDef.filter.groupIndex = -1; // Particles don't collide with each other

        b2CreateCircleShape(bodyId, shapeDef);
    }
}

That's a lot of code just to create a body and add a shape! But most of it is simply setting the necessary properties, because unlike most situations we have seen so far, many of the default properties are not what we want. The comments there should be self-explanatory... some things such as the friction and fixed rotation are not strictly necessary, but since there will be a lot of these particles it may help to do anything we can to reduce the calculation required for each of them.

The effects of this are difficult to show in static screenshots, so here is an animated gif showing the trail left by each particle. This shows 24 frames after the explosion, or about half a second:

Explosions

It's quite obvious that this method has all the advantages of the raycast method, plus a few more. Now we have a real blast 'wave' that reflects off obstacles, allowing the energy of the explosion to correctly get around corners, as in the case of the underground tunnel, and the diagonal plate beside the upper platform. This scattering of energy helps give the impression that the explosion is actually a physical space-occupying presence rather than the telekinetic impulse we have with the other methods. It also gives us something to base rendering (smoke textures etc) on. When we run this with the debug drawing turned on, we can see all of these particles, however in a game you might choose to show only a few of them with sprites attached for shrapnel, or other explosion related graphics. The rest will continue to affect physics bodies in the world but will not be visible.

You can vary the weight of the particles, their initial velocity, linear damping, restitution, and of course the number of them, to get just the right feel. The particles require some management to clear them after the explosion is finished, but it's not a lot of work. The only real drawback to this method is the CPU time required. Mobile devices may struggle a little with multiple explosions of a high number of particles, but for regular computers even 100 or more (128 in the screenshot below) is not a problem.

Explosions

Another nice side-effect is that the energy from the blast does not affect everything instantaneously - it takes a tiny bit longer for the particles to reach objects that are further from the blast center. It's only a few milliseconds of difference but those milliseconds do seem to add a touch of realism. For instance looking at the block stack immediately to the right of the explosion center, we can see the blast work its way up the stack over a few timesteps.

Finally, this method ensures that in tight spaces the expected energy does not just disappear. The particles can bounce around and hit many different surfaces before they dissipate.

Explosions

Credits

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