Game worlds can be a fully interactive environment where realism usually plays an important role and actions occur coherently to the player's eye. To achieve these sensations, you will need to query the physics world. A good example is Line of Sight (LOS) tests for artificial intelligence or even a 3D sound system, which should attenuate the sound received from the vicinity if there is any wall between the audio source and the listener. This check is easy to carry out by casting a ray between the two world points.
Another way of receiving direct feedback from the physics world is area queries, which can help you to detect dynamic obstacles and jump to avoid them. Even less common cases are where lots of people are upon a wooden bridge, so you can calculate their total weight and make them fall if it reaches a certain threshold.
Useful, right?
The code for this example resides in the Box2DQuerySample.java
source file and covers both of the earlier mentioned ways of querying the world with the help of 10 boxes that will randomly fall from the sky.
In our example, raycasting is performed by clicking/touching and dragging, so a line is drawn representing the query range. At the same time, a fixed Axis-aligned bounding box (AABB) or simply a virtual box is placed at one end of the scenery, so if any of the other 10 boxes fall into its bounds, a white sphere will appear on its interior to highlight them.
As the proverb says, divide and rule, so each type of world query will have its separate explanation in the next lines. However, there is a common old known utility to instantiate in order to draw some visual representation:
ShapeRenderer sr = new ShapeRenderer();
Vector2
objects to represent the origin (p1
) and the end (p2
) of the raycast, as well as its normal
in case it gets overlapped by another body:Vector2 p1, p2, collision, normal; p1 = new Vector2(); …
Look at the following figure to get a better understanding of these four concepts:
A ray is cast from point p1
to point p2
. The collision
vector represents the location on the ray of the contact point where the line hits a body. The normal
vector represents the normalized perpendicular vector of the surface on that body at the contact point.
p1
and p2
according to the position of the cursor while normal
and collision
will be reset to zero. At this point, we have all the required information to perform the query:public boolean touchDown (int x, int y, int pointer, int button) { // Screen coordinates to World coordinates viewport.getCamera().unproject(point.set(x, y, 0)); if (button == Input.Buttons.LEFT) { p1.set(point.x, point.y); p2.set(point.x, point.y+0.00001f); normal.set(Vector2.Zero); collision.set(Vector2.Zero); // Make the query world.rayCast(callback, p1, p2); return true; } return false; }
callback
variable is. It is nothing more than an object whose type is an anonymous class that implements the Box2D RayCastCallback
interface. It implies that you must override the reportRayFixture(…)
method to obtain the resulting collision data. The following code will make you see things clearer:RayCastCallback callback = new RayCastCallback() { public float reportRayFixture(Fixture fixture, Vector2 point, Vector2 normal, float fraction) { collision.set(point); Box2DQuerySample.this.normal.set(normal).add(point); //Process collision data return 1; } };
Once a fixture is found in the path of the ray, we update our already declared Vector2
points: collision
and normal
. After that, do whatever you need, for instance, print that collision data on the screen. A further explanation on this function and its return value will be provided in the How it works… section.
p2
should be updated to its end point if and when it is not the same as p1
:public boolean touchDragged(int x, int y, int pointer) { viewport.getCamera().unproject(point.set(x, y, 0)); if(Gdx.input.isButtonPressed(Buttons.LEFT)) { if(p1.x != point.x && p1.y != point.y) { p2.set(point.x, point.y); world.rayCast(callback, p1, p2); return true; } } return false; }
Be aware that we are raycasting every frame (or almost every frame) that we drag.
Vector2
points:sr.setProjectionMatrix(viewport.getCamera().combined); sr.begin(ShapeType.Line); sr.line(p1, p2); sr.line(collision, normal); sr.end();
Adding an area query example will follow the same line as the raycast one: instantiate some visual elements, define a callback, and finally draw the model.
float[] aabb = new float[10]; aabb[0] = 1f; aabb[1] = 1.5f; aabb[2] = 4f ; aabb[3] = 1.5f; aabb[4] = 4f ; aabb[5] = 4.5f; aabb[6] = 1f; aabb[7] = 4.5f; aabb[8] = 1f; aabb[9] = 1.5f;
aabb
bounds, specifically you will have to provide its maximum and minimum (x,y) coordinates. In the same way as before, a callback object must be passed too:world.QueryAABB(areaCallback, aabb[0], aabb[1], aabb[4], aabb[5]);
QueryCallback
is the one that will provide us with the set of fixtures that match the condition:QueryCallback areaCallback = new QueryCallback() { public boolean reportFixture(Fixture fixture) { if(fixture.getBody().getType() != BodyType.StaticBody) bodiesWithinArea.add(fixture.getBody()); return true; } };
To carry on with our example, we ensure that we do not mark the ground.
aabb
field can be drawn with no effort thanks to the polyline
function from ShapeRenderer
. You will just have to add it to the previous batch of shapes:sr.begin(ShapeType.Line); … sr.polyline(aabb); … sr.end();
sr.begin(ShapeType.Filled); for(Body b : bodiesWithinArea) sr.circle(b.getPosition().x, b.getPosition().y, 0.2f, 20); sr.end();
The RayCastCallback
interface requires you to implement this method:
float reportRayFixture(Fixture fixture, Vector2 point, Vector2 normal, float fraction)
Not everybody will know what all those parameters are for. Do not fear, a friendly graphic explanation is waiting for you:
With the diagram, it is easy to see that the fixture is the shape, point represents the exact place where collision has occurred, and normal is a perpendicular vector to the colliding segment from the fixture. The fraction is the distance that the ray had to go through in order to find an intersection.
In addition to the parameters, the reason why the function returns a float value is because it accepts the following values:
Regarding AABB queries and the hidden magic that Box2D appeals to in order to determine intersections, there are still some loose ends that the most curious readers would like to tie up.
Separating Axis Theorem (SAT) is the underlying technique to check whether two convex polygons are overlapping. For those who do not know it, a convex polygon is the one with all its interior angles lower than 180 degrees.
SAT consists of checking whether there is a gap between two shapes by projecting every side of one polygon into its perpendicular vector (normal) and checking whether any of the other polygons' projections overlap. This is shown in the following diagram where we project the left-hand side of box 3 into its normal (brought to the bottom of the diagram for clarity reasons):
The result of this tiny example says that box 1 and box 3 are colliding as you can see that the first projection collides with the second.
Specifically in the case of circles that the normal vector will be the one that joins the center of the circle with the closest vertex on the polygon.
Raycasting and AABB intersection are awesome tools widely used within the industry. Apart from the aforementioned examples, there are a lot of situations where querying could be useful:
There are a lot of possibilities, does any other possibility come to mind?
3.142.133.180