Often you will want to know what entities are in a given part of the scene. For example if a bomb goes off, everything in the vicinity should take some damage, or in an RTS type game you might want to let the user select units by dragging a rectangle around them. The method of quickly checking what objects are in that part of the world to use for further detailed processing is known as 'world querying'.

Box2D provides two tools for this - ray casting and AABB testing. Ray casting... didn't we just do that? Yes, we did it the manual way, by looping through every shape in the world and checking the ray against them all to find out which one was the closest. This can be very inefficient when you have a large number of shapes in the scene. A better way is to use the CastRay function of the world itself. This allows the engine to focus on shapes which it knows are near the ray's path.

Ray casting, the efficient way

Since we have already looked at ray casting in some detail, we'll just take a quick look at the world's CastRay function without making a demonstration. The function looks like this:

b2World_CastRay(worldId, origin, translation, filter, resultCallback, context);

The parameters are:

  • origin: Starting point of the ray (b2Vec2)
  • translation: Vector from start to end point (b2Vec2)
  • filter: Query filter to control which shapes to check (b2QueryFilter)
  • resultCallback: Function called for each intersection found (b2CastResultFcn)
  • context: User data passed to callback (object)

The callback function should have this signature:

function rayCastCallback(shapeId, point, normal, fraction, context) {
    // Return value controls ray behavior:
    // -1: ignore this intersection
    // 0: terminate ray immediately
    // fraction: clip ray to this point
    // 1: continue ray to full length
    return fraction;
}

During the ray cast calculation, each time a shape is found that the ray hits, this callback function will be called. For each intersection we can get the shape which was hit, the point at which it was hit, the normal to the shape 'surface' and the fraction of the distance from origin to translation that the intersection point is at.

The final point to cover is the return value that you should give for this callback function. Remember that if your ray is long enough there could be many shapes that it intersects with, and your callback could be called many times during one CastRay. Very importantly, this raycast does not detect shapes in order of nearest to furthest, it just gives them to you in any old order - this helps it to be efficient for very large scenes. The engine lets you decide how you want to deal with each shape as it is encountered. This is where the return value of the callback comes in. You will return a floating point value, which you can think of as adjusting the length of the ray while the raycast is in progress.

  • return -1 to completely ignore the current intersection
  • return a value from 0 - 1 to adjust the length of the ray, for example:
    • returning 0 says there is now no ray at all
    • returning 1 says that the ray length does not change
    • returning the fraction value makes the ray just long enough to hit the intersected shape

Common cases:

  • To find only the closest intersection:
    • return the fraction value from the callback
    • use the most recent intersection as the result
  • To find all intersections along the ray:
    • return 1 from the callback
    • store the intersections in a list
  • To simply find if the ray hits anything:
    • if you get a callback, something was hit (but it may not be the closest)
    • return 0 from the callback for efficiency

Area querying (aka AABB querying)

The Box2D world has another function for finding shapes overlapping a given area, the AABB query. This one allows us to define a rectangular region, and the engine will then find all shapes in that region and call a callback function for each of them.

AABBs enclose various shapes

The function looks like this:

b2World_OverlapAABB(worldId, aabb, filter, resultCallback, context);

The callback function should have this signature:

function overlapCallback(shapeId, context) {
    // Return true to continue checking more shapes
    // Return false to terminate the query
    return true;
}

We will set up a test where we define a rectangular area, and then draw a marker on each shape which is currently overlapping the area. We'll use the same scene as at the beginning of the last topic, so go copy the code from there.

a selection of shapes inside the bounds

Let's add some mouse tracking so we can select a region of interest. The user will click down to mark one corner and then release the mouse button after moving to set the diagonally opposite corner, this will define an AABB box for the region:

// Track mouse positions
let mouseDownPos = new b2Vec2(0, 0);
let mouseUpPos = new b2Vec2(0, 0);

function onMouseDown(p) {
    mouseDownPos = mouseUpPos = p;
}

function onMouseUp(p) {
    mouseUpPos = p;
}

// In update/render loop, draw the selection rectangle
function drawSelection() {
    const lower = new b2Vec2(
        Math.min(mouseDownPos.x, mouseUpPos.x),
        Math.min(mouseDownPos.y, mouseUpPos.y)
    );
    const upper = new b2Vec2(
        Math.max(mouseDownPos.x, mouseUpPos.x), 
        Math.max(mouseDownPos.y, mouseUpPos.y)
    );

    // Draw white rectangle (using your preferred graphics API)
    drawRect(lower, upper, "white");
}

// Don't forget to hook the mouse functions to your mouse events in the usual way

a white box shows the area that has been selected with the mouse

Query implementation:

// Store for the found bodies
const foundBodies = [];

function queryCallback(shapeId, context) {
    const bodyId = b2Shape_GetBody(shapeId);
    foundBodies.push(bodyId);
    return true; // keep going to find all shapes in the query area
}

// In update/render loop
function queryArea() {
    // Clear previous results
    foundBodies.length = 0;

    // Create AABB from mouse positions
    const aabb = new b2AABB(
        Math.min(mouseDownPos.x, mouseUpPos.x),
        Math.min(mouseDownPos.y, mouseUpPos.y)
        Math.max(mouseDownPos.x, mouseUpPos.x),
        Math.max(mouseDownPos.y, mouseUpPos.y)
    );

    // Default filter to accept all shapes
    const filter = b2DefaultQueryFilter();

    // Perform query
    b2World_OverlapAABB(worldId, aabb, filter, queryCallback, null);

    // Draw points on found bodies
    for (const bodyId of foundBodies) {
        const pos = b2Body_GetPosition(bodyId);
        drawPoint(pos, 6, "white");
    }
}

Note that we need to return true from the callback function to make sure the query keeps going until all of the overlapping shapes have been put into the list. You should see a white spot on each shape which has an AABB overlapping the region.

a white box and some shapes, the ones that are overlapped have a white spot inside

Credits

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