Querying the world

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?

Getting ready

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.

How to do it…

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();

Raycasting

  1. First of all, initialize four 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:

    Raycasting

    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.

  2. With each player's click/touch, we will always update 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;
    }

    Note

    Be aware that you cannot have a 0 length raycast, that is why 0.00001 is added to the y component in the p2 vector. Otherwise, it will crash.

  3. You will surely wonder what the 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.

  4. Clicking or touching is not the only way to interact with the former code because if the user keeps dragging, 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.

  5. Finally, draw the visual representation of our four Vector2 points:
    sr.setProjectionMatrix(viewport.getCamera().combined);
    sr.begin(ShapeType.Line);
    sr.line(p1, p2);
    sr.line(collision, normal);
    sr.end();

    Note

    Remember that ShapeRenderer works in screen coordinates by default, so do not forget to update its projection matrix to be the same as the camera's.

Area querying

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.

  1. The first step is to define an AABB region to query. As it will make easier its later drawing, we will work with a plain old Java array where two subsequent float numbers will represent a pair of x-y coordinates. The first two and last two numbers must be the same in order to close the box:
    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;
  2. Next, we will query the world for the fixtures located within the 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]);
  3. The interface to inherit from is not the same for area callbacks. In this case, 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.

    Note

    The last if statement is performed to keep the example as simple as possible, but consider using collision filtering techniques in your games.

  4. The final steps are destined to draw the model. The 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();
  5. Marks are represented with a white sphere over the boxes. You will have to specify how many segments will form those circles. In our case, set it to 20 to get a decent precision:
    sr.begin(ShapeType.Filled);
    for(Body b : bodiesWithinArea)
      sr.circle(b.getPosition().x, b.getPosition().y, 0.2f, 20);
    sr.end();

How it works…

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:

How it works…

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:

  • -1: Box2D will just filter the current fixture and carry on with the next
  • 0: This will stop the query process
  • Fraction: Raycast will be clipped to the returned value, so you will be raycasting its closest shape
  • 1: Box2D will just continue processing as if no hit occurred

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):

How it works…

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.

There's more…

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:

  • A line of fire test.
  • Flying objects that need to avoid obstacles.
  • Cover posture detection.
  • Gap detection (jump!).
  • Checking whether a character is not too tall to go through a path.
  • Games such as The Sims provide you with a tool to spawn objects in a world. Is there any obstacle that prevents you from placing that pool table in the living room?

There are a lot of possibilities, does any other possibility come to mind?

See also

  • The Implementing a deferred raycaster recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.133.180