Introduction

In the last part we covered a bit about the basis for the game. This section is going to cover the data model thats going to run the game. So, its asteroids right? What game entities are we going to be thinking about?

Asteroids - the lumps of rock floating around the world. They have to be different sizes and we have to be able to detect when they get hit by anything (including themselfs)

Player - The player's ship itself. The player has to be able to control this and like asteroids we must be able to detect when the player hits anything. The player must also be able to fire shots.

Shots - The lazer blasts produced by the player. Again, we must be able to detect when these hit other entities. They must also only last for a short time.

Now we know what our data is we can start thinking about a quick bit of design. We've got a bunch of "things" in the game world, normally referred to as game entities. So, like with space invaders, we end up with an interface called Entity which describes what we expect of all entities and allows us to treat them all the same. We could also note that there are some common bits of functionality (collision, movement) so lets add an AbstractEntity abstract class to hold this. Finally, we end up with three implements, one for each type of entity, here they are:

Rock
Player
Shot

This is what in the Entity interface this time:

public interface Entity { public void update(EntityManager manager, int delta); public void render(); public float getSize(); public float getX(); public float getY(); public void collide(EntityManager manager, Entity other); public boolean collides(Entity other); }

The first two methods are related to the GameState interface we looked at in the last part. update() allows the entity to control itself, for instance entities will move themselfs based on their current velocity. EntityManager gives the entities being updated methods to call back to game engine to manipulate other entities in the game. For instance, if a rock detects that it hits something it will want to remove the entity that was hit. render() allows the entity to call what ever OpenGL funciton it needs to draw itself to the screen.

The other methods are entity specific. The getSize(), getX() and getY() allow other classes to interegate the position and size of the entity for collision purposes. These methods are utilised in the collides() method to work out whether one entity collides with another.

Finally, entities can be informed that they've collided with another entity through the collide() method. This gives the entity a chance to define some logic that will be enacted when a collision occurs. We reuse the EntityManager interface here to allow the entity to manipulate the game world when it collides with another.

So, what implements EntityManager and what calls render(), update() and checks collisions? We briefly looked at the InGameState class which also acts at the holder for all the game data and runs the engine that handles the interactions between the player and the game world.

When the InGameState is rendered it simply renders a background, renderers all the entities in the game by calling render() on them, and then renders the GUI over the top. It looks like this:

drawBackground(window); // position the view a way back from the models so we // can see them GL11.glTranslatef(0,0,-50); // loop through all entities in the game rendering them for (int i=0;i<entities.size();i++) { Entity entity = (Entity) entities.get(i); entity.render(); } drawGUI(window);

The drawBackground() method simply draws a single quad over the background textured with a spacey image. Next we cycle through the entity list held in the InGameState rendering each one in turn. Finally, we draw the GUI (the score, lifes etc) over the top. Rendering complete!

Next lets look at how the engine handles updating the entities and handling collisions:

public void update(GameWindow window, int delta) { if (gameOver) { gameOverTimeout -= delta; if (gameOverTimeout < 0) { window.changeToState(MenuState.NAME); } } for (int i=0;i<entities.size();i++) { Entity entity = (Entity) entities.get(i); float firstSize = entity.getSize(); for (int j=i+1;j<entities.size();j++) { Entity other = (Entity) entities.get(j); if (entity.collides(other)) { entity.collide(this, other); other.collide(this, entity); } } } entities.removeAll(removeList); entities.addAll(addList); removeList.clear(); addList.clear(); // loop through all the entities in the game causing them // to update (i.e. move, shoot, etc) int rockCount = 0; for (int i=0;i<entities.size();i++) { Entity entity = (Entity) entities.get(i); entity.update(this, delta); if (entity instanceof Rock) { rockCount++; } } if (rockCount == 0) { level++; spawnRocks(level); } }

So, whats going on here? The first bits pretty simple, if the flag to indicate the game is over has been set then after a certain timeout skip back to MenuState (note, we haven't looked at this state yet, its a finishing touch we'll cover later). The next loop cycles through every entity in the game world checking its collision against every other entity in the game world. Not very efficent huh? Absolutely right, but its simple to code and maintain and for our purposes here isn't going to have a high performance hit (we're only talking about 20 odd entities). Weighing up code simplicity against performance it makes more sense to leave this simple, after all, if it does cause a problem we can always change it later. The collision code itself is inside the AbstractEntity (since all entities are collision checked in the same way):

public boolean collides(Entity other) { // We're going to use simple circle collision here since we're // only worried about 2D collision. // // Normal math tells us that if the distance between the two // centres of the circles is less than the sum of their radius // then they collide. However, working out the distance between // the two would require a square root (Math.sqrt((dx*dx)+(dy*dy)) // which could be quite slow. // // Instead we're going to square the sum of their radius and compare // that against the un-rooted value. This is equivilent but // much faster // Get the size of the other entity and combine it with our // own, giving the range of collision. Square this so we can // compare it against the current distance. float otherSize = other.getSize(); float range = (otherSize + getSize()); range *= range; // Get the distance on X and Y between the two entities, then // find the squared distance between the two. float dx = getX() - other.getX(); float dy = getY() - other.getY(); float distance = (dx*dx)+(dy*dy); // if the squared distance is less than the squared range // then we've had a collision! return (distance <= range); }

Well, read the comment above. What we're basically doing here is modelling the collision of the entities as circles colliding on a 2D plane. The game itself is really 2D so this is fine. The one nice optimisation here is not bothering with the square root to get the distance - since in this case we only want to know that its less than the radius of the two circles. This ends up being a nice fast bit of code. So, looking back at our loop - we cycle through each entity, calling collides() on every other entity, which does this speedy check to work out if there's been a collision. Finally, if we detect a collision we notify both entities to let them do what ever work they need.

Ok, so we cycle through letting the entities know if they collided with each other... but whats the addList and removeList stuff about? Remember we allowed collision and update of entities to manipulate the game world through the EntityManager interface. The problem being that is the game world changes while we're looping through that could get our loop out of synch - the collision handler might, for instance, remove an entity that we're about to process! So, when the entities call back to remove or add an entity we don't actually perform the operation immediately. We just add the remove or add request to a list which we execute once per loop. Heres the code for addEntity and removeEntity:

public void removeEntity(Entity entity) { removeList.add(entity); } public void addEntity(Entity entity) { addList.add(entity); }

So, it doesn't really do much.. but combined with the following lines from update():

entities.removeAll(removeList); entities.addAll(addList); removeList.clear(); addList.clear();

the adds and removes get executed without interfering with any loops we might br processing at the time. There are more complicated and intricate ways of dealing with this sort of interaction but this way is pretty simple and does the job for us here just fine.

Right, back to update().. the last thing we do is cycle through all the entities allowing them to update given the amount of time thats passed in from the game framework. While we're updating we look for entities that are rocks/asteroids.. this is just a game mechanic. If there are no rocks left then the level is complete and the player needs some more to shoot!

The only thing we've got to look at now for our data model is the implementations of our Entities.

Entities

As mention above each of the entities extends some common functionality in the abstract base class and then adds its own specifics.

AbstractEntity

Abstract Entity is going to contain the common bits of functionality between all entities. We already know thats includes collision checking (see above) but what else? Well, every entity in the game is going to move so they all need a position and a velocity and when they're updated the position needs to be changed based veclotiy, like so:

public void update(EntityManager manager, int delta) { // update the position of this entity based on its current // velocity. positionX += (velocityX * delta) / 1000.0f; positionY += (velocityY * delta) / 1000.0f; // if we move off either side of the player area, then come back on // the other side. In asteroids all entities have this behaviour if (positionX < -HALF_WIDTH) { positionX = HALF_WIDTH - 1; } if (positionX > HALF_WIDTH) { positionX = -(HALF_WIDTH - 1); } // same again but for top and bottom this time if (positionY < -HALF_HEIGHT) { positionY = HALF_HEIGHT - 1; } if (positionY > HALF_HEIGHT) { positionY = -(HALF_HEIGHT - 1); } } public float getX() { return positionX; } public float getY() { return positionY; }

The first bit is pretty simple, update the position based on the amount of time thats passed and the current velocity. Notice we divide by 1000.0 since the time is in milliseconds and we'd like to specify velocity in units per second.

The next bit checks if the entity has moved off the edge of the screen. If it has, it gets put on the opposite side. This gives the classic asteroids wrap around. The great bit is that because we define this behaviour up in our abstract class it applies to all entities giving us a nice consistent feel.

So, all entities will now move and collide. Great!

Player Entity

The player entity is represented by a space ship which the player can control. In this case we're going to use keyboard control so in the Player entity's update method we check the keys and update velocities like this:

public void update(EntityManager manager, int delta) { // if the player is pushing left or right then rotate the // ship. Note that the amount rotated is scaled by delta, the // amount of time that has passed. This means that rotation // stays framerate independent if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) { rotationZ += (delta / 5.0f); } if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) { rotationZ -= (delta / 5.0f); } // recalculate the forward vector based on the current // ship rotation forwardX = (float) Math.sin(Math.toRadians(rotationZ)); forwardY = (float) -Math.cos(Math.toRadians(rotationZ)); // count down the timer until a shot can be taken again // if the timeout has run out (<= 0) then check if the player // wants to fire. If so, fire and then reset the timeout shotTimeout -= delta; if (shotTimeout <= 0) { if (Keyboard.isKeyDown(Keyboard.KEY_SPACE)) { fire(manager); shotTimeout = shotInterval; } } // if the player is pushing the thrust key (up) then // increse the velocity in the direction we're currently // facing if (Keyboard.isKeyDown(Keyboard.KEY_UP)) { // increase the velocity based on the current forward // vector (note again that this is scaled by delta to // keep us framerate independent) velocityX += (forwardX * delta) / 50.0f; velocityY += (forwardY * delta) / 50.0f; // since we're thrusting now we need to add some effect // to our engine engine trail - add a particle to the // engine positioning it just behind the ship float flameOffset = 1.1f; engine.addParticle(positionX-(forwardX*flameOffset), positionY-(forwardY*flameOffset), 0.6f, 150); } // call the update the abstract class to cause our generic // movement and anything else the abstract implementation // provides for us super.update(manager, delta); // update the particle engine to cause the particles to fade // out over their lifespan. engine.update(delta); }

First, if the player is holding left or right then update the rotation of the ship appropriately. Next we work out the vector that is forward based on the current rotation. This allows us to add velocity in the correct direction should the user be pushing up (thrust). We also check if the player is pressing space - if they are we want the ship to fire and hence create an Shot entity. However, we don't want the player to be able to fire constantly so we add a counter that will count down between the player's allowed fire rate (shotTimeout).

Next, we call the super class implementation to actually move the player entity based on a potentially updated velocity - which uses the AbstractEntity implementation we saw above.

Finally, we call update on the particle engine being rendered for this entity (the ship's engine trail) - we look at the details of this in Part 4.

The Player specific code is the response to collision, heres how its implemented:

public void collide(EntityManager manager, Entity other) { // if we've collide with a rock then the rock must split apart, // and our velocity needs to be changed to push us away from the // rock if (other instanceof Rock) { velocityX = (getX() - other.getX()); velocityY = (getY() - other.getY()); ((Rock) other).split(manager, this); // notify the class manging the entities that the player // has been hit, just in case anything needs doing manager.playerHit(); } }

Pretty easy? If the player hits a rock, change the velocity so the ship bounces off, cause the rock to split in two and finally notify the manager that the player has be hit (so it can cause effects - like sounds).

Shot Entity

Shots are really simple, they keep moving until they hit something or they timeout. So the update() method is nice and easy:

public void update(EntityManager manager, int delta) { // cause the particle to move by calling the abstract super // class's implementation of update super.update(manager, delta); // update the amount of time left for this shot to exist life -= delta; if (life < 0) { // if the life time has run out then remove the shot // entity from the game manager.removeEntity(this); } else { // otherwise add another particle to the engine at the // current position and update the particle engine to // cause existing particles to fade out particles.addParticle(getX(), getY(), size, 200); particles.update(delta); } }

Like in player will call the super class implementation of update to move the shot based on its velocity. We decrement the life counter to time out the shot as it flys across the screen. If the shot has run out of life we remove it from the game, otherwise we add another particle to the shot trail.

So, shots move and timeout. The other specific part, as with Player, is the collision handler. Here's the implementation:

public void collide(EntityManager manager, Entity other) { // if the shot hits a rock then we've scored! The rock // needs to split apart and then this shot has been used up // so remove it. if (other instanceof Rock) { ((Rock) other).split(manager, this); manager.removeEntity(this); } }

Yes, its that short! If the shot is notified that its collided with the a rock then we split the rock in to and remove the shot. Now, we can blast rocks!

Rock Entity

Our finally entity is the asteroid. There will be a whole group of these floating round the screen. There are a couple of rock specific things going on here. Lets take a look at update():

public void update(EntityManager manager, int delta) { // call the abstract entitie's update method to cause the // rock to move based on its current settings super.update(manager, delta); // the rocks just spin round all the time, so adjust // the rotation of the rock based on the amount of time // that has passed rotationZ += (delta / 10.0f) * rotateSpeed; }

Not much there right? Rocks are special in that we'd like them to rotate as they move, hence hte update to the Z rotation of the rock as we go. As with the other entities we call the super implementation of the update method to get the asteroid to move.

However, rocks have more special functionality.. when they collide, they bounce:

public void collide(EntityManager manager, Entity other) { // if anything collides with a rock its direction must change // (to prevent rocks intersecting with each other). For effect // we'll also change the direction of rotation velocityX = (getX() - other.getX()); velocityY = (getY() - other.getY()); rotateSpeed = -rotateSpeed; }

We don't need to check what the asteroid hit. Whatever it hits its going to bounce off. How then are asteroids destroyed? Well, they have a special method that other entities have already been calling. So, when the player detects that they've hit an asteroid the Player entity calls split() to kill the asteroid. The split method looks like this:

void split(EntityManager manager, Entity reason) { // remove this rock (since its about to split) and notify // the manager that a rock has been destroyed (just in case // it needs to take an action) manager.removeEntity(this); manager.rockDestroyed(size); // if the rock isn't the smallest we'll need to create // two smaller rocks which the rock destroyed have "split" // into if (size > 1) { // work out the vector at which the reason for this rock // split hit the rock in question float dx = getX() - reason.getX(); float dy = getY() - reason.getY(); // scale it down a bit since we're about to use it // for position the split out rocks. The smaller the original // rock the closer the resulting rocks need to be to each other dx *= (size * 0.2f); dy *= (size * 0.2f); // the speed that the rocks splitting out are going to be sent // out at. This value has no units and is just a scalar used // to tune the game float speed = 2; // create and add the two new rocks based on the direction of // impact and the size of the new rocks. Rock rock1 = new Rock(texture, model, getX() + dy, getY() - dx, size - 1, dy * speed, -dx * speed); Rock rock2 = new Rock(texture, model, getX() - dy, getY() + dx, size - 1, -dy * speed, dx * speed); manager.addEntity(rock1); manager.addEntity(rock2); } }

When we split a rock we first remove the existing rock since its about to explode. Next we check if it should turn into smaller rocks by checking its size. If its big enough we create two new rocks of a smaller size and add the to the game world using the EntityManager (which of course is actually the InGameState). Another nice small piece of code specific to the job at hand.

Summary


In this part we've taken a look at the data model used to run the asteroids game in preperation for adding all the fancy rendering of models and effects. Note that its important to understand how the game is going to work out underneath before we begin adding the graphics and sounds.

Links


Tutorial written by Kevin Glass