Putting It All Together

Let's put all the knowledge we gathered in this section together to form a nice little example. We'll extend the cannon example of the last section as discussed a few pages back. We'll use a Cannon object for the cannon, a DynamicGameObject for the cannonball, and a number of GameObjects for the targets. Each target will have a size of 0.5x0.5 meters and be placed randomly in the world.

We want to be able to shoot those targets. For this we need collision detection. We could just loop over all targets and check them against the cannonball, but that would be boring. We'll use our fancy new SpatialHashGrid class to speed up finding the potentially colliding targets for the current ball position. We won't insert the ball or the cannon into the grid, though, as that wouldn't really gain us anything.

Since this example is already pretty big, we'll split it up into multiple listings. We'll call the test CollisionTest and the corresponding screen CollisionScreen. As always, we'll only look at the screen. Let's start with the members and the constructor, in Listing 812.

Listing 8-12. Excerpt from CollisionTestjava: Members and Constructor class CollisionScreen extends Screen { final int NUM_TARGETS = 20; final float WORLD_WIDTH = 9.6f; final float WORLD_HEIGHT = 4.8f; GLGraphics glGraphics; Cannon cannon; DynamicGameObject ball; List<GameObject> targets; SpatialHashGrid grid;

Vertices cannonVertices; Vertices ballVertices; Vertices targetVertices;

Vector2 touchPos = new Vector2(); Vector2 gravity = new Vector2(0,-l0);

public CollisionScreen(Game game) { super(game);

glGraphics = ((GLGame)game).getGLGraphics();

targets = new ArrayList<GameObject>(NUM_TARGETS);

grid = new SpatialHashGrid(WORLD_WIDTH, WORLD_HEIGHT, 2.5f);

GameObject target = new GameObject((float)Math.random() * WORLD_WIDTH,

grid.insertStaticObject(target); targets.add(target);

cannonVertices = new Vertices(glGraphics, 3, 0, false, false);

cannonVertices.setVertices(new float[] { -0.5f, -0.5f,

ballVertices = new Vertices(glGraphics, 4, 6, false, false);

ballVertices.setVertices(new float[] { -0.1f, -0.1f,

ballVertices.setIndices(new short[] {0, 1, 2, 2, 3, 0}, 0, 6);

targetVertices = new Vertices(glGraphics, 4, 6, false, false); targetVertices.setVertices(new float[] { -0.25f, -0.25f,

0.25f, -0.25f, 0.25f, 0.25f, -0.25f, 0.25f }, 0, 8); targetVertices.setIndices(new short[] {0, 1, 2, 2, 3, 0}, 0, 6);

We brought over a lot from the CannonGravityScreen. We start off with a couple of constant definitions, governing the number of targets and our world's size. Next we have the GLGraphics instance, as well as the objects for the cannon, the ball, and the targets, which we store in a list. We also have a SpatialHashGrid, of course. For rendering our world we need a few meshes: one for the cannon, one for the ball, and one we'll use to render each target. Remember that we only had a single rectangle in BobTest to render the 100 Bobs to the screen. We'll reuse that principle here as well, instead of having a single Vertices instance holding the triangles (rectangles) of our targets. The last two members are the same as in the CannonGravityTest. We use them to shoot the ball and apply gravity when the user touches the screen.

The constructor just does all the things we discussed already. We instantiate our world objects and meshes. The only interesting thing is that we also add the targets as static objects to the spatial hash grid.

Let's check out the next method of the CollisionTest class, in Listing 8-13.

Listing 8-13. Excerpt from CollisionTest.java: The update() Method

^Override public void update(float deltaTime) {

List<TouchEvent> touchEvents = game.getInput().getTouchEvents(); game.getInput().getKeyEvents();

int len = touchEvents.size(); for (int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i);

touchPos.x = (event.x / (float) glGraphics.getWidth())* WORLD_WIDTH; touchPos.y = (1 - event.y / (float) glGraphics.getHeight()) * WORLD_HEIGHT;

cannon.angle = touchPos.sub(cannon.position).angle();

if(event.type == TouchEvent.TOUCH_UP) {

float radians = cannon.angle * Vector2.TO_RADIANS;

float ballSpeed = touchPos.len() * 2; ball.position.set(cannon.position); ball.velocity.x = FloatMath.cos(radians) * ballSpeed; ball.velocity.y = FloatMath.sin(radians) * ballSpeed;

ball.bounds.lowerLeft.set(ball.position.x - 0.lf, ball.position.y - 0.lf);

float ballSpeed = touchPos.len() * 2; ball.position.set(cannon.position); ball.velocity.x = FloatMath.cos(radians) * ballSpeed; ball.velocity.y = FloatMath.sin(radians) * ballSpeed;

ball.bounds.lowerLeft.set(ball.position.x - 0.lf, ball.position.y - 0.lf);

ball.velocity.add(gravity.x * deltaTime, gravity.y * deltaTime); ball.position.add(ball.velocity.x * deltaTime, ball.velocity.y * deltaTime); ball.bounds.lowerLeft.add(ball.velocity.x * deltaTime, ball.velocity.y * deltaTime);

List<GameObject> colliders = grid.getPotentialColliders(ball); len = colliders.size(); for(int i = 0; i < len; i++) {

GameObject collider = colliders.get(i);

if(OverlapTester.overIapftectangIes(ball.bounds, collider.bounds)) { grid.removeObject(collider); targets.remove(collider);

As always, we first fetch the touch and key events, and only iterate over the touch events. The handling of touch events is nearly the same as in the CannonGravityTest. The only difference is that we use the Cannon object instead of the vectors we had in the old example, and we also reset the ball's bounding rectangle when the cannon is made ready to shoot on a touch-up event.

The next change is how we update the ball. Instead of straight vectors, we use the members of the DynamicGameObject that we instantiated for the ball. We neglect the DynamicGameObject.acceleration member and instead add our gravity to the ball's velocity. We also multiply the ball's speed by 2 so that the cannonball flies a little faster. The interesting thing is that we update not only the ball's position, but also the bounding rectangle's lower-left corner's position. This is crucial, as otherwise our ball would move but its bounding rectangle wouldn't. Why don't we just use the ball's bounding rectangle to store the ball's position? We might want to have multiple bounding shapes attached to an object. Which bounding shape would then hold the actual position of the object? Separating these two things is thus beneficial, and introduces only a little computational overhead. We could of course optimize this a little by only multiplying the velocity with the delta time once. The overhead would then boil down to two more additions—a small price to pay for the flexibility we gain.

The final portion of this method is our collision detection code. All we do is find the targets in the spatial hash grid that are in the same cells as our cannonball. We use the SpatialHashGrid.getPotentialColliders() method for this. Since the cells the ball is contained in are evaluated in that method directly, we do not need to insert the ball into the grid. Next we loop through all the potential colliders and check if there really is an overlap between the ball's bounding rectangle and a potential collider's bounding rectangle. If there is, we simply remove the target from the target list. Remember, we only added targets as static objects to the grid.

And those are our complete game mechanics. The last piece of the puzzle is the actual rendering, which shouldn't really surprise you. See the code in Listing 8-14.

Listing 8-14. Excerpt from CollisionTestjava: The present() Method

^Override public void present(float deltaTime) { GLl0 gl = glGraphics.getGL();

gl.glViewport(0, 0, glGraphics.getWidth(), glGraphics.getHeight()); gl.glClear(GLl0. GL_COLOR_BUFFER_BIT); gl.glMatrixMode(GLl0.GL_PROJECTION); gl.glLoadIdentity();

gl.gl0rthof(0, WORLD_WIDTH, 0, WORLD_HEIGHT, l, -l); gl.glMatrixMode(GLl0.GL_MODELVIEW);

gl.glColor4f(0, l, 0, l); targetVertices.bind(); int len = targets.size(); for(int i = 0; i < len; i++) {

GameObject target = targets.get(i); gl.glLoadIdentity();

gl.glTranslatef(target.position.x, target.position.y, 0); targetVertices.draw(GLl0.GL_TRIANGLES, 0, 6);

targetVertices.unbind(); gl.glLoadIdentity();

gl.glTranslatef(ball.position.x, ball.position.y, 0);

ballVertices.bind();

ballVertices.draw(GLl0.GL_TRIANGLES, 0, 6); ballVertices.unbind();

gl.glLoadIdentity();

gl.glTranslatef(cannon.position.x, cannon.position.y, 0); gl.glRotatef(cannon.angle, 0, 0, l); gl.glColor4f(l,l,l,l); cannonVertices.bind();

cannonVertices.draw(GLl0.GL_TRIANGLES, 0, 3); cannonVertices.unbind();

Nothing new here. As always, we set the projection matrix and viewport, and clear the screen first. Next we render all targets, reusing the rectangular model stored in targetVertices. This is essentially the same thing we did in BobTest, but this time we render targets instead. Next we render the ball and the cannon, as we did in the CollisionGravityTest.

The only thing to note here is that I changed the drawing order so that the ball will always be above the targets and the cannon will always be above the ball. I also colored the targets green with a call to glColor4f().

The output of this little test is exactly the same as in Figure 8-17, so I'll spare you the repetition. When you fire the cannonball, it will plow through the field of targets. Any target that gets hit by the ball will be removed from the world.

This example could actually be a nice game if we polish it up a little and add some motivating game mechanics. Can you think of additions? I suggest you play around with the example a little to get a feeling for all the new tools we have developed over the course of the last couple of pages.

There are a few more things I'd like to discuss in this chapter: cameras, texture atlases, and sprites. These use graphics-related tricks that are independent of our model of the game world. Let's get going!

Up until now, we haven't had the concept of a camera in our code; we've only had the definition of our view frustum via glOrthof(), like this:

gl.glMatrixMode(GLl0.GL_PROJECTION); gl.glLoadIdentity();

gl.glOrthof(0, FRUSTUM_WIDTH, 0, FRUSTUM_HEIGHT, l, -l);

From Chapter 6 we know that the first two parameters define the x-coordinates of the left and right edges of our frustum in the world, the next two parameters define the y-coordinates of the bottom and top edges of the frustum, and the last two parameters define the near and far clipping planes. Figure 8-19 shows that frustum again.

Figure 8-19. The view frustum for our 2D world, again

So we only see the region (0,0,1) to (FRUSTUM_WIDTH, FRUSTUM_HEIGHT,-1) of our world. Wouldn't it be nice if we could move the frustum? Say, to the left? Of course that would be nice, and it is dead simple as well:

gl.glOrthof(x, x + FRUSTUM_WIDTH, 0, FRUSTUM_HEIGHT, l, -l);

In this case, x is just some offset we can define. We can of course also move on the x-and y-axes:

0 0

Post a comment