
This tutorial can be discussed here
Disclaimer: This tutorial is provided as is. I don't guarantee that the provided source is perfect or that that it provides best practices.
Note that this tutorial isn't intended to be guide to writing each and every line of the code. Its designed as a walk through the code for the game which should highlight the bits that are interesting for general games development.
Disclaimer: This tutorial is provided as is. I don't guarantee that the provided source is perfect or that that it provides best practices.
Windows Natives
Mac OS X Natives
Linux x86 Natives
We're also going to utilise the texture loaded provided by the Space Invaders tutorials which can be found here:
TextureLoader.java
Texture.java
The final playable version of the game can be found here: Asteroids Tutorial
While reading the tutorial it makes sense to have the source code open in another window or IDE. In each part the tutorial source applicable will be linked - note however that this will be the complete source and not the half completed version in line with the current stage of the tutorial.
We're going to build a simple framework to allow us to maintain the LWJGL window/display creation code seperately from the rest of the game. This will allow us greater flexibility to extend the game later by reducing the dependcies between the setup code and the game code. Let first look at getting an LWJGL window on the screen...
Although this tutorial is based on using LWJGL most of the code would be an easy port to any other OpenGL binding (e.g. JOGL). Lets take a look at the GameWindow class. This class trys to contain everything for setting up, controlling and managing the LWJGL specific bits of code.
The entry point to the game (main()) is here and simply constructs a GameWindow. If we were going to think about more than just a simple game it might make sense to move this to its own class and add some bootstrap code. However, since we're just writing asteroids its fine here.
The GameWindow class has 3 main functions. First, the constructor is responsible for initialising the LWJGL display and start the whole game off. Here it is:
public GameWindow() { try { // find out what the current bits per pixel of the desktop is int currentBpp = Display.getDisplayMode().getBitsPerPixel(); // find a display mode at 800x600 DisplayMode mode = findDisplayMode(800, 600, currentBpp); // if can't find a mode, notify the user the give up if (mode == null) { Sys.alert("Error", "800x600x"+currentBpp+" display mode unavailable"); return; } // configure and create the LWJGL display Display.setTitle("Asteroids Tutorial"); Display.setDisplayMode(mode); Display.setFullscreen(false); Display.create(); // initialise the game states init(); } catch (LWJGLException e) { e.printStackTrace(); Sys.alert("Error", "Failed: "+e.getMessage()); } } public void startGame() { // enter the game loop gameLoop(); }
So, the first thing we do is try and find a display mode. We're aiming for a windowed version at 800x600. We attempt to get a bit-per-pixel based on what the desktop is currently running at since this is mostly like to work. If something goes wrong we use the Sys.alert() LWJGL function to display a message to the user.
Next, we set a few details up and create the Display. At this point we should get a window on the screen. Great! Finally, we call init() - to initialise the game states (more about these in a moment) and gameLoop() - the method that runs the whole game. gameLoop() doesn't return so we're done here.
The init() method is responsible for creating the game state objects - these objects will conform to the GameState interface which is how the GameWindow will view the rest of game. The GameState interface is intended to decouple the LWJGL window logic from the actual game code. This hopefully allows us to add extra bits of game without having too much effect on the LWJGL code (which in turn helps up to maintain either side and to port to different rendering technologies). Lets take a look at the init() method:
public void init() { // initialise our sound loader to determine if we can // play sounds on this system SoundLoader.get().init(); // run through some based OpenGL capability settings. Textures // enabled, back face culling enabled, depth testing is on, GL11.glEnable(GL11.GL_TEXTURE_2D); GL11.glEnable(GL11.GL_CULL_FACE); GL11.glEnable(GL11.GL_DEPTH_TEST); GL11.glDepthFunc(GL11.GL_LEQUAL); GL11.glShadeModel(GL11.GL_SMOOTH); // define the properties for the perspective of the scene GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glLoadIdentity(); GLU.gluPerspective(45.0f, ((float) 800) / ((float) 600), 0.1f, 100.0f); GL11.glMatrixMode(GL11.GL_MODELVIEW); GL11.glHint(GL11.GL_PERSPECTIVE_CORRECTION_HINT, GL11.GL_NICEST); // add the two game states that build up our game, the menu // state allows starting of the game. The ingame state rendered // the asteroids and the player addState(new MenuState()); addState(new InGameState()); try { // initialse all the game states we've just created. This allows // them to load any resources they require Iterator states = gameStates.values().iterator(); // loop through all the states that have been registered // causing them to initialise while (states.hasNext()) { GameState state = (GameState) states.next(); state.init(this); } } catch (IOException e) { // if anything goes wrong, show an error message and then exit. // This is a bit abrupt but for the sake of this tutorial its // enough. Sys.alert("Error", "Unable to initialise state: " + e.getMessage()); System.exit(0); } }
The first thing we do here is ask the sound system to initialise. What this is actually for will be convered in detail in 4 - suffice it to say it cause the OpenAL interface to be created. Next we initialise OpenGL configuration by enabling a few bits of pieces:
GL_TEXTURE_2D - We're going to texture objects in the game
GL_CULL_FACE - We're going to speed up rendering by culling faces that point away from the view.
GL_DEPTH_TEST - Depth testing is turned on to prevent things in the background being drawn over things in the foreground.
Next we set up the perspective mode. When we're working in 3D its important to describe to OpenGL how we want the distance from the viewer to effect the size things are drawn on the screen. This is whats called the project matrix (GL_PROJECTION). Here we've used the GL utility method gluPerspective to specify the perspective based on the size of the screen (800x600) and front and back planes (the distances at which things will stop being displayed - i.e. disappear into the distance).
Next we go onto create and add the game states that will control the game play. Having added them we loop through the added states and cause them to initialise - by calling the init() method that all game states have to implement due to their interface. This might seem a bit wierd given that we're the ones that just added the states and we could have initialised them at any point. The intention here is to eventually allow game states to be added externally to the GameWindow, so the game could be extended further. Ok, so why do we call init() on each state so much later than we could? The states could want to initialise textures, or display lists, or any other OpenGL resource. These can only be created once LWJGL has been initialised - so we wait until the Display has been created then go on to initialise the states. Of course, once we've done it this way once and put it in a nice maintainable class we can go on and forget about that sort of detail :)
Ok.. so now our window is up and game states are initialise.. onto the game logic..
The game loop lets the game state get on with the logic while maintaining updating of the LWJGL display. It looks like this:
public void gameLoop() { boolean gameRunning = true; long lastLoop = getTime(); currentState.enter(this); // while the game is running we loop round updating and rendering // the current game state while (gameRunning) { // calculate how long it was since we last came round this loop // and hold on to it so we can let the updating/rendering routine // know how much time to update by int delta = (int) (getTime() - lastLoop); lastLoop = getTime(); // clear the screen and the buffer used to maintain the appearance // of depth in the 3D world (the depth buffer) GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); // cause the game state that we're currently running to update // based on the amount of time passed int remainder = delta % 10; int step = delta / 10; for (int i=0;i<step;i++) { currentState.update(this, 10); } if (remainder != 0) { currentState.update(this, remainder); } // cause the game state that we're currently running to be // render currentState.render(this,delta); // finally tell the display to cause an update. We've now // rendered out scene we just want to get it on the screen // As a side effect LWJGL re-checks the keyboard, mouse and // controllers for us at this point Display.update(); // if the user has requested that the window be closed, either // pressing CTRL-F4 on windows, or clicking the close button // on the window - then we want to stop the game if (Display.isCloseRequested()) { gameRunning = false; System.exit(0); } } }
So first, we tell the game state we're currently in that we've entered it. This gives game states a chance to do things as they become active - maybe changing the music or displaying an effect.
Next we go into a loop, this is the one thats going to keep the game running. Each loop we work out how much time has passed since we last rendered (often referred to as delta - from the term "change"). We use this time gap to work out how much to update the game on this loop. After this we clear the screen to prepare to render.
Now this next bit is interesting:
// cause the game state that we're currently running to update // based on the amount of time passed int remainder = delta % 10; int step = delta / 10; for (int i=0;i<step;i++) { currentState.update(this, 10); } if (remainder != 0) { currentState.update(this, remainder); }
The method update() on GameState allows the state to update its elements. Maybe its got to move space ships around the screen or progress an animation. So, here we allow the game states to progress based on the amount of time passed since last render. However, we don't want to let the game progress in big jumps (since this might allow us to miss collisions or jump through solid objects) so instead we update the game state in increments of 10 milliseconds. Since the game state adapts based on the amount of time thats passed this has no effect on them apart from making the game logic far more accurate.
Ok, so we've allowed the game states to run whatever logic they want to. Next, we ask the current game state to render itself giving it a reference to ourself so it can access some utility functions (see next section). Once the state has rendered we get LWJGL to update the display causing the rendering to be shown to the player.
Finally in the game loop we check to see if the user has tried to close the window in anyway. If they have we honour this by exiting the game.
Well, thats it for LWJGL display handling. Next lets look at the few LWJGL utilities available from the GameWindow class.
Theres a few things that are display or library related that it'd be nicer to have in the LWJGL specific bit of the code. For this tutorial we've provided methods for the basics, but you could take the whole thing a step further by abstracting the actual rendering opertions so they were independent on the rendering library in use. Of course this would be an awful lot of work. An important point of building any framework it picking the level at which your aiming to provide your tools.
This time we've provided a simple method to get the current time based on the high resolution in the LWJGL library:
private long getTime() { return (Sys.getTime() * 1000) / Sys.getTimerResolution(); }
This just gets us the current time in milliseconds. Its useful for moving elements based on time rather than frame rate (see the Space Invaders tutorials for details).
The other useful utility we're going to expose from GameWindow is orthographic projection mangement. As mentioned above the projection matrix describes how the distance an object is from the viewer will effect its size and position. This gives us the feeling of perspective. However, when we want to draw things in pixel coordinates we don't want this effect. Say we want to draw a line of text on the screen, we want it to appear as though its overlayed over the 3D game world. How do we do this?
If you look back at the space invaders tutorials this is exactly what we were doing. Its called an "orthographic projection matrix". This means that the distance an object is away from the view has no effect on its screen position - which fits nicely with drawing things on the screen. There are a few other details but lets look at the code first..
public void enterOrtho() { // store the current state of the renderer GL11.glPushAttrib(GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_ENABLE_BIT); GL11.glPushMatrix(); GL11.glLoadIdentity(); GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPushMatrix(); // now enter orthographic projection GL11.glLoadIdentity(); GL11.glOrtho(0, 800, 600, 0, -1, 1); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_LIGHTING); } public void leaveOrtho() { // restore the state of the renderer GL11.glPopMatrix(); GL11.glMatrixMode(GL11.GL_MODELVIEW); GL11.glPopMatrix(); GL11.glPopAttrib(); }
What we've got here is a way to start using an orthographic projection matrix (enterOrtho()) and a way to stop using it (leaveOrtho()). Lets look at the second section of enterOrtho() first - just like in space invaders we use the glOrtho() method to define a matrix that will allow us to draw to the screen as though its a normal 2D display. A couple of other things here. First we want our ortho mode graphics to be displayed "above" everything else - like an overlay. To achieve this we disable the depth testing (GL_DEPTH_TEST) which means that elements are drawn to the screen without considering what might be infront of them. Secondly, we don't want the overlay graphics to be effected by lighting so we disable this too (GL_LIGHTING).
Right, so thats how we get into orthographic projection mode but whats all the pushing and popping about? When we look at the rest of the game we'll be manipulating the view matrix so that we can view different parts of the scene. Now, there is only *one* view matrix and setting the orthographic view is going to overwrite that matrix - but we don't want to lose the any setup we might have done for the 3D view - so, how are we going to save it? This is when the matrix stack comes in. Imagine it as a stack of paper where each piece of paper has one matrix written on it. When we "glPushMatrix" we're saving the current matrix onto the stack. When we "glPopMatrix" we're pulling the top matrix off and putting it into the current matrix. We can do the same thing with rendering attributes, see "glPushAttrib" and "glPopAttrib".
So, now we get pushing and popping, what are we actually doing? We save the current matrix and attributes by pushing them onto the stack. Next we setup orthographic projection. When we want to leave othrographic projection mode we simply pop everything back off the stack - simplicity itself!
Now we have looking at everything GameWindow provides its probably worth just having a quick look at the GameState interface and the first implementation of one the InGameState
Lets take a look at the GameState interface that allows the GameWindow to communicate in an abstract way with the rest of the game source.
public interface GameState { public String getName(); public void init(GameWindow window) throws IOException; public void render(GameWindow window, int delta); public void update(GameWindow window, int delta); public void enter(GameWindow window); public void leave(GameWindow window); }
So, the state is responsible for providing its own name. This gives us a way of identifing the states when we want to move between them (say from a menu state into a playing state). GameState's also have a change to render and update themselfs every frame. This allows them to draw the in game graphics and update the related data models.
Finally GameStates are notified when they are about to be activated and when they are about to be deactivated using the enter() and leave() methods repsectively.
We've now got a description of the elements that are going to build up the game logic. Lets take a look at an implementation of this interface used for the in game play.
This state is going to be responsible for rendering the in game models and update the game logic for flying around, shooting and asteroids exploding. First, take a look at init()
public void init(GameWindow window) throws IOException { defineLight(); TextureLoader loader = new TextureLoader(); background = loader.getTexture("res/bg.jpg"); shotTexture = loader.getTexture("res/shot.png"); shipTexture = loader.getTexture("res/ship.jpg"); shipModel = ObjLoader.loadObj("res/ship.obj"); rockTexture = loader.getTexture("res/rock.jpg"); rockModel = ObjLoader.loadObj("res/rock.obj"); Texture fontTexture = loader.getTexture("res/font.png"); font = new BitmapFont(fontTexture, 32, 32); shoot = SoundLoader.get().getOgg("res/hit.ogg"); split = SoundLoader.get().getOgg("res/bush.ogg"); }
We'll talk about what each of the specific details are in the init() method in later part. However, as mentioned above, note how anything that requires a GL context to be present (textures, models etc) when loading is loaded in init(). Here we load a few textures, a couple of models (one of the ship, one for the rocks) and a couple of sounds. We'll cover how things things work later.
Next lets look at the render() implementation:
public void render(GameWindow window, int delta) { // reset the view transformation matrix back to the empty // state. GL11.glLoadIdentity(); material.put(1).put(1).put(1).put(1); material.flip(); GL11.glMaterial(GL11.GL_FRONT, GL11.GL_DIFFUSE, material); GL11.glMaterial(GL11.GL_BACK, GL11.GL_DIFFUSE, material); // draw our background image GL11.glDisable(GL11.GL_LIGHTING); 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); }
As you can see the pieces of code get to be quite short and to the point. Here for instance, we apply a material so everything we render will be lit. Next we draw the background, cycle through all the entities getting them to render their models. Finally we draw the GUI over the top of the game view.
In a similar way the update method is also quite simple. The source code is split out into simple blocks that we'll cover in each part of this tutorial. However, the central game state just act as the glue to pull everything together.
There is final point we should look as is the good example of using enter() method from the state interface. This lets us reset all the game statistics when the state is entered, which effectively lets us start a new game when we enter the state:
public void enter(GameWindow window) { entities.clear(); player = new Player(shipTexture, shipModel, shotTexture); entities.add(player); life = 4; score = 0; level = 5; gameOver = false; spawnRocks(level); }
We clear out all the game entities, create a new player, reset the stats and spawns some more rocks to blast.
This part of the tutorial has hopefully explained the simple framework that we're going to use in the later parts to build a simple game.
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:
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.
As mention above each of the entities extends some common functionality in the abstract base class and then adds its own specifics.
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!
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).
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!
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.
If you've followed the first 2 parts of this tutorial we've now talked about how to get a window on the screen, build and simple framework and code up the mechanics of the asteroids game. However, up to this point we've not really looked at how we visualise anything. This part of the tutorial will cover how we load up 3D models and display them to represent the player and the asteroid entities we looked at in Part 1.
3D Models are generally loaded from files created in other tools. For instance, the player models in Quake were often created with "QME" and then loaded into the game from MD2 files. For this tutorial we're going to look at Wavefront Object Files. They generally use the extension ".obj". This format was chosen because of its popularity as a export format for modelling tools. Pretty much any modelling tool will export to OBJ format. Its also a particularlly easy format to process.
Model file formats and generally documented somewhere - however the search for a reliable and accurate specification often takes longer than writing the code to load it. A good site to try initially is http://www.wotsit.org/. Then tend to have the majority of formats hanging around.
The specification/guide we're going to use for OBJ files is here. Its not a full specification but it gives us enough information to get the features we want. Note that this is an important point to realise. 3D file formats often support a great range of features, normally you only want to utilise a small set of these so you don't really need to cope with everything. Picking and choosing like this can dramatically reduce your development time. There are cases of course where a full feature set should be supported, for instance if you happen to be writing a generic loader for a scenegraph (Xith3D, JME).
Before we get working on the loader thats going to render the models in OpenGL we need to
be familiar with the format. Lets take a look at what our format specification says:
v x y z The vertex command, this specifies a vertex by its three coordinates. The vertex is implicitly named by the order it is found in the file. For example, the first vertex in the file is referenced as '1', the second as '2' and so on. None of the vertex commands actually specify any geometry, they are just points in space. vt u v [w] The vertex texture command specifies the UV (and optionally W) mapping. These will be floating point values between 0 and 1 which say how to map the texture. They really don't tell you anything by themselves, they must be grouped with a vertex in a 'f' face command. vn x y z The vertex normal command specifies a normal vector. A lot of times these aren't used, because the 'f' face command will use the order the 'v' commands are given to determine the normal instead. Like the 'vt' commands, they don't mean anything until grouped with a vertex in the 'f' face command. f v1[/vt1][/vn1] v2[/vt2][/vn2] v3[/vt3][/vn3] ... The face command specifies a polygon made from the verticies listed. You may have as many verticies as you like. To reference a vertex you just give its index in the file, for example 'f 54 55 56 57' means a face built from vertecies 54 - 57.
So, the OBJ file format is text based and consists of 4 types of definition. The first defines vertices (or points in space). This is signified by a line starting with a "v" followed by 3 numbers indicating the x, y and z coordinate of the point in space. The next definition is signified by "vt", standing for vertex texture. The two numbers following the indicator define a texture coordinate. The 3rd definition, "vn", defines a vertex normal - a single normal that can be applied to a vertex.
The 4th and most important definition is signified by "f" standing for face. The list of numbers after the "f" indicate which vertex, vertex texture coordinate and vertex normal should be combined to form a face. For instance, the following line:
f 1/3/3 2/4/4 3/5/5
indicates that a face should be built with 3 points. The first point uses the 1st vertex defined in the file, the 3rd texture coordinate and the 3rd normal. The second point used the 2nd vertex defined in the file, the 4th texture coordinate and the 4th normal. The final point uses the 3rd vertex defined in the file and the 5th texture coordinate and normal.
Now we have a feel for how the format works lets consider how we're going to load the data.
The pseudo code for loading a OBJ file looks like this:
while there are more lines to read read a line if the line starts with a "v" then read the vertex and store it if the line starts with a "vt" then read the texture coordinate and store it if the line starts with a "vn" then read the normal coodinate and store it if the line starts with a "f" then read the indexes and store a face based on the data read
Lets put this into real Java code then.. What this code is actually going to do is just read the data and store it for rendering use. The class in question is ObjData. Take a look at the constructor, its responsible for executing the pseudo code above:
public ObjData(InputStream in) throws IOException { // read the file line by line adding the data to the appropriate // list held locally BufferedReader reader = new BufferedReader(new InputStreamReader(in)); while (reader.ready()) { String line = reader.readLine(); // if we read a null line thats means on some systems // we've reached the end of the file, hence we want to // to jump out of the loop if (line == null) { break; } // "vn" indicates normal data if (line.startsWith("vn")) { Tuple3 normal = readTuple3(line); normals.add(normal); // "vt" indicates texture coordinate data } else if (line.startsWith("vt")) { Tuple2 tex = readTuple2(line); texCoords.add(tex); // "v" indicates vertex data } else if (line.startsWith("v")) { Tuple3 vert = readTuple3(line); verts.add(vert); // "f" indicates a face } else if (line.startsWith("f")) { Face face = readFace(line); faces.add(face); } } // Print some diagnositics data so we can see whats happening // while testing System.out.println("Read " + verts.size() + " verticies"); System.out.println("Read " + faces.size() + " faces"); }
So we read each line, processing it and storing each of the vertex related bits of data into some lists stored in the ObjData instance. A "tuple" is just a term for a group of values that are related to each other. So, a Tuple2 in this case is used for the pair of values used for a vertex coordinate and a Tuple3 is used for normals and verticies. The utility methods to read these tuples simply split the remainder of the line on spaces.
Finally, if we read a face we're going to construct a record describing the face and store in another list. This way our ObjData simply contains all of the data stored in the OBJ file but in a format that can be used by other Java objects easily. The creation of the face object is handled in the readFace() method:
private Face readFace(String line) throws IOException { StringTokenizer points = new StringTokenizer(line, " "); points.nextToken(); int faceCount = points.countTokens(); // currently we only support triangels so anything other than // 3 verticies is invalid if (faceCount != 3) { throw new RuntimeException("Only triangles are supported"); } // create a new face data to populate with the values from the line Face face = new Face(faceCount); try { // for each line we're going to read 3 bits of data, the index // of the vertex, the index of the texture coordinate and the // normal. for (int i=0;i<faceCount;i++) { StringTokenizer parts = new StringTokenizer(points.nextToken(), "/"); int v = Integer.parseInt(parts.nextToken()); int t = Integer.parseInt(parts.nextToken()); int n = Integer.parseInt(parts.nextToken()); // we have the indicies we can now just add the point // data to the face. face.addPoint((Tuple3) verts.get(v-1), (Tuple2) texCoords.get(t-1), (Tuple3) normals.get(n-1)); } } catch (NumberFormatException e) { throw new IOException(e.getMessage()); } return face; }
First, note that the code assumes that only triangles will be supported (i.e. only 3 points per face). For our purposes here we only need triangles (all the models are carefully designed to only use triangles) - so we to save time we've skipped anything else that the model format might support. However, to be on the safe side, if a model with more than 3 points in a face is read in we'll throw a RuntimeException - it should be pretty obvious if anything goes wrong :)
Next we read in the index for each component of each point in the face, look them up in our lists and store them in a Face object which acts a data record.
So, after the ObjData has processed the input stream containing the OBJ file, the lists in the ObjData instance have been populated with all the data for the model. This object can now be used to create the OpenGL expressions to render the model to the screen.
Now we've got all the data out of the OBJ model we want to be able to render our triangles in OpenGL. We could simply do this in immediate mode (i.e. lots of calls to glVertex() etc at runtime) but thats not very good for performance. Since we know our models arn't going to change as we render them we can compile a OpenGL display list containing the model which we can render with one command!
A display list is a list of OpenGL operations identified by a single value. A developer creates a list and then issues the commands required to be contained with in it. The OpenGL implementation can them optimize these commands and store them on the graphics hardware. Consider that every command you issue to OpenGL must make it to the hardware, so the more commands you have to issue the more has to be sent to the card, and hence the slower things happen. With a good OpenGL implementation the display list will be completely optimised onto the card so we only have to issue a single command to the graphics hardware to get it to execute a whole list of commands.
Right, lets have a look at how will build the display list from our ObjData instance. It all happens in the ObjModel :
public ObjModel(ObjData data) { // we're going to process the OBJ data and produce a display list // that will display the model. First we ask OpenGL to create // us a display list listID = GL11.glGenLists(1); // next we start producing the contents of the list GL11.glNewList(listID, GL11.GL_COMPILE); // cycle through all the faces in the model data // rendering a triangle for it GL11.glBegin(GL11.GL_TRIANGLES); int faceCount = data.getFaceCount(); for (int i=0;i<data.getFaceCount();i++) { for (int v=0;v<3;v++) { // a position, normal and texture coordinate // for each vertex in the face Tuple3 vert = data.getFace(i).getVertex(v); Tuple3 norm = data.getFace(i).getNormal(v); Tuple2 tex = data.getFace(i).getTexCoord(v); GL11.glNormal3f(norm.getX(), norm.getY(), norm.getZ()); GL11.glTexCoord2f(tex.getX(), tex.getY()); GL11.glVertex3f(vert.getX(), vert.getY(), vert.getZ()); } } GL11.glEnd(); GL11.glEndList(); }
First, we create a display list by calling glGenLists(). We store the ID of the list locally so we can use it later on. Next, we start building the list wiht glNewList() and then process the ObjData we read from the file earlier.
For each face we found in the OBJ model we create a single triangle in a triangles array in OpenGL. The triangle is defined by applying the normal, texture coordinate and vertex information store in the face that we created earlier.
Once we've processed all the faces we're done so will close off the triangle array and end the list compilation. At this point the OpenGL implemention (and related hardware) can optimize the commands and ship them over to the hardware. So, how do we actually render the model? Well, thats the easy bit:
public void render() { // since we rendered our display list at construction we // can now just call this list causing it to be rendered // to the screen GL11.glCallList(listID); }
We simply call our list and OpenGL executes all the operations we set up earlier (based on an OBJ model).
Cool, now we can load models and render them in OpenGL. However, its a little intricate to create ObjData object then pass that into a ObjModel and also write the code to get an InputStream. We could make it a bit easier by writing a simple static utility method that performs the common bits for us based on a simple reference to a model. Take a look a ObjLoader, tihs is where we've put that static method. It looks like this:
public static ObjModel loadObj(String ref) throws IOException { InputStream in = ObjLoader.class.getClassLoader().getResourceAsStream(ref); if (in == null) { throw new IOException("Unable to find: "+ref); } return new ObjModel(new ObjData(in)); }
Simply look up the reference specified on the class path. Check if the reference was actually found. Finally call the loading classes we've talked about above to load the data and then render it to a display list.
Now we can load models in one line. Nice!
The game uses the model loader to load the models for both asteroids and the player. However, the we don't want to load the model on a per entity basis since that could take up a lot of memory and the model can happily be shared. So, for instance, in InGameState we load the player model like so:
shipTexture = loader.getTexture("res/ship.jpg"); shipModel = ObjLoader.loadObj("res/ship.obj");
and then we pass the model and texture into the player entity when we create it:
public void enter(GameWindow window) { entities.clear(); player = new Player(shipTexture, shipModel, shotTexture);
Finally, when the Player entity is rendered we simply bind our texture (since we want our model to be textured) then call our list (which contains our polys with texture coordinates), like this:
public void render() { // enable lighting for the player's model GL11.glEnable(GL11.GL_LIGHTING); // store the original matrix setup so we can modify it // without worrying about effecting things outside of this // class GL11.glPushMatrix(); // position the model based on the players currently game // location GL11.glTranslatef(positionX,positionY,0); // rotate the ship round to our current orientation for shooting GL11.glRotatef(rotationZ,0,0,1); // setup the matrix to draw the model for our player // rotate the ship to the right orientation for rendering. Our // ship model is modelled on a different axis to the one we're // using so we'd like to rotate it round GL11.glRotatef(90,1,0,0); // scale the model down because its way to big by default GL11.glScalef(0.01f,0.01f,0.01f); // bind to the texture for our model then render it. This // actually draws the geometry to the screen texture.bind(); model.render();
First, we move to the right location to place the player based on its current position and orientation. Next we scale the model down (because its a bit big to start with). Finally, we do the actual rendering code. We bind to the texture (so the model appears with it) - texture.bind(), and then render the model by calling model.render() (which calls the display list as we saw earlier).
Now, we can render the models for our entities as they bounce through space. We've talked about model formats, reading data and rendering the polygons. We've also had a quick look at how the game uses the models after they've been loaded.
If you've followed through the first 3 parts of the tutorial we should now understand how the game framework works, how the window is display, how the game logic is abstract from the rendering and in the last part how we display 3D models for each of the game entities.
This part of the tutorial is going to begin looking at the smaller features that make a game feel more complete. First we'll take a look at sound effects. In this case we've chosen to use OGG files. OGGs are a bit like MP3s apart from without any of the lawful restrictions on use. Luckily enough there is a Java implementation of OGG decoding freely available called JOrbis. Since we'll be playing the sound effects through LWJGL's binding to OpenAL we're going to need the sound effect data in "PCM" format. This is exactly what JOrbis does for us, it takes OGG encoded data and decodes it into PCM. We'll look at decoding the OGG using JOrbis and playing it through OpenAL.
The second part of this tutorial will look at displaying some GUI type graphics. These will allow us to display a game menu before the game starts and display the score and lives while the game is playing. This is really very much like what you might have seen in the Space Invaders tutorials.
First lets start by saying using JOrbis properly is pretty tricky. Luckily the nice people at JCraft provide example code in the JOrbis package which is what we've used here. Take a look at the OggDecoder. Its a monster! The decoder opens the input stream, checks the file is a indeed an Ogg Vorbis and then proceeds to cycle through the pages in the file decoding them into PCM data.
The important thing to note here is that once our method is finished we either end up with a failure or an instance of the OggData which describes the PCM data pulled out of the OGG. OggData looks like this:
class OggData { /** The data that has been read from the OGG file */ public ByteBuffer data; /** The sampling rate */ public int rate; /** The number of channels in the sound file */ public int channels; }
What we've pulled out here is exactly what we need to play the sound through OpenAL. The data buffer contains the PCM data that been decoded. The rate tells us the sampling rate of the OGG (this can vary and hence needs to be passed to OpenAL). Likewise, channels specifies the number of channels in the OGG, which again OpenAL needs to play the PCM.
Since we've now got our PCM data all we need to do is pipe it into OpenAL to get it to play. Well, its a little more complicated, lets take a look at SoundLoader.
There are two initial concepts we have to grasp about OpenAL. First, there are buffers, these are used to store the batchs of PCM data which are our sound effects or music. The number of these that you hold is simply memory dependent.
Second, OpenAL has the concept of sources, these are used to actually play the sound effects. Think of them as the source of the sound effect or music. A source can only play a single sound at any given time. The number of sources that can be used is dependent on your user's sound hardware. To assume 16 sound sources is pretty safe with todays players on the PC. However, choosing to go for 8 source is safer and is normally enough to give the feel of multiple sound effects going off at the same time. Note that you'll normally want to reserve one of your sources for music playing at all times. This allows you to keep the music playing while shooting off the sound effects.
Right, lets go back and take a look at SoundLoader initialisation.
public void init() { inited = true; try { AL.create(); soundWorks = true; sounds = true; } catch (Exception e) { e.printStackTrace(); soundWorks = false; sounds = false; } if (soundWorks) { sourceCount = 8; sources = BufferUtils.createIntBuffer(8); AL10.alGenSources(sources); if (AL10.alGetError() != AL10.AL_NO_ERROR) { sounds = false; soundWorks = false; } } }
We use the LWJGL binding to initialise the OpenAL driver. Next we try to allocate 8 sources - we're trying to be on the safe side here. Note how we record if anything goes wrong. If sounds don't initialise properly we'd rather not stop the game, it would be better to just not play the sounds.
Right, we've got 8 sources to play sounds on, how are we going to use the OGG decoder to get our buffers.. lets take a look at the getOgg() method:
public Sound getOgg(String ref) throws IOException { if (!soundWorks) { return new Sound(this, 0); } if (!inited) { throw new RuntimeException("Can't load sounds " + "until SoundLoader is init()"); } int buffer = -1; if (loaded.get(ref) != null) { buffer = ((Integer) loaded.get(ref)).intValue(); } else { System.out.println("Loading: "+ref); try { IntBuffer buf = BufferUtils.createIntBuffer(1); InputStream in = Thread.currentThread(). getContextClassLoader(). getResourceAsStream(ref); OggDecoder decoder = new OggDecoder(); OggData ogg = decoder.getData(in); AL10.alGenBuffers(buf); AL10.alBufferData(buf.get(0), ogg.channels > 1 ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_MONO16, ogg.data, ogg.rate); loaded.put(ref,new Integer(buf.get(0))); buffer = buf.get(0); } catch (Exception e) { e.printStackTrace(); Sys.alert("Error","Failed to load: "+ref+" - "+e.getMessage()); System.exit(0); } } if (buffer == -1) { throw new IOException("Unable to load: "+ref); } return new Sound(this, buffer); }
So, what are we doing here? First, if the sound system is working just return an empty sound object. The object will never actually do anything so its juts nice and quick to return an empty object. Next, if we haven't already initialised the sound manager then fail because there must have been a programming error.
Now we actually start loading things. If we've already loaded the OGG once return the preloaded buffer identifier. If we haven't loaded it before use the OggDecoder to get the PCM data from the specified reference. Next we ask OpenGL to create us a new buffer (alGenBuffer()) and then fill the buffer with the data we've loaded from the OGG file (alBufferData()). Finally, we cache the buffer just incase someone trys to load the same one again and pass back a sound object that holds a reference to the buffer we've created.
At the end we have a Sound object that contains enough information to be able to play the sound effect or music thats loaded from the OGG. We pass this back to the caller. We can now use the sound object to play the sound by calling:
public void play(float pitch, float gain) { store.playAsSound(buffer, pitch, gain); }
The sound object simply calls back to the SoundManager with its buffer. In SoundManager playAsSound() looks like this:
void playAsSound(int buffer,float pitch,float gain) { if (soundWorks) { if (sounds) { nextSource++; if (nextSource > 7) { nextSource = 1; } AL10.alSourceStop(sources.get(nextSource)); AL10.alSourcei(sources.get(nextSource), AL10.AL_BUFFER, buffer); AL10.alSourcef(sources.get(nextSource), AL10.AL_PITCH, pitch); AL10.alSourcef(sources.get(nextSource), AL10.AL_GAIN, gain); AL10.alSourcePlay(sources.get(nextSource)); } } }
Here we can see actually playing the source effect. If the sound system is working and sounds are turned on we first find a source to play the effect through. When we start playing a sound through a source any sound currently being played on that source will stop. So, as we play more sound effects we want to cycle through our sources to try and prevent collisions. We choose the next source, stop any sound playing on it by calling alSourceStop(), then load it with the sound effect buffer we want to use (alSourcei()). Finally we request that the source starting playing the buffer its loaded with (alSourcePlay()) - and there we go, the sound starts playing!
Now we can play sounds where are we going to use them in the game? It'd be nice to have a sound when the play fires (its traditional after all) and maybe one when an asteroid is blown up. To achieve this we need the entities to be able to feed back to the central game logic (InGameState) so that it can play the sounds when events occur. This is done using our EntityManager interface again, with the follow methods:
/** * Notification that a rock has been destroyed * * @param size The size of the rock that was destroyed */ public void rockDestroyed(int size); /** * Notification that the player fired a shot */ public void shotFired();
Since the EntityManager is passed into the entity logic and collision (see Part 2) we can notify the central game logic when things occur. Lets look at how these things are implement in InGameState. First, heres the init() method where we load up the sound effects:
public void init(GameWindow window) throws IOException { ... shoot = SoundLoader.get().getOgg("res/hit.ogg"); split = SoundLoader.get().getOgg("res/bush.ogg"); }
We simply load up the sounds and store them locally to the instance. Now, lets look at the implementation of the notification methods on InGameState:
public void rockDestroyed(int size) { split.play(1.0f,1.0f); score += (4 - size) * 100; } public void shotFired() { shoot.play(1.0f,0.5f); }
What we've done is built a nice simple utility for loading and playing sound effects. As you can see, from a utility users point of view loading a sound effects is a one line, and playing yet another one line. Great! So, when our entities detect that a rock was destroyed (Rock.split()) or that that a shot was fired (Player.update()) they notify the game logic and it in turns plays an appropriate sound effect (which will only get played if the sound system is working).
If we look at InGameState we can see that the during the game play the player has some things they have to think about:
/** The current score */ private int score; /** The number of lifes left */ private int life = 4;
The player accumulates score as they play and has a number of lifes - or times they can hit an asteroid without dieing. Lets start with the score, we want to render the numbers to the screen. If we were working in Java2D we could just use a font and render the text. However, OpenGL doesn't provide any default text writing capabilities so we're going to have to write a Font class to render the text.
Fonts can be rendered in OpenGL in lots of different ways. The easiest by far is to simply use a texture with the characters in it and map them onto OpenGL quads on the screen. This way you can simply choose the character based on the texture coordinates. Here's an example of a Font Texture. Be warned its alpha based so it may appear as a white only image. The image is actually a set of white character layed out to be mapped easily onto the screen. Theres a great tool for doing this called the Bitmap Font Builder. It can take any windows font and produce a font texture from it.
Right, so we've got our font layed out onto an image how are we going to use it? Lets take a look at the BitmapFont class. This one loads up a font texture and allows us to write strings to the screen using it. Loading up the texture looks like this:
public BitmapFont(Texture texture, int characterWidth, int characterHeight) { this.texture = texture; this.characterWidth = characterWidth; this.characterHeight = characterHeight; // calculate how much of the texture is taken up with each character // by working out the proportion of the texture size that the character // size in pixels takes up characterWidthInTexture = texture.getWidth() / (texture.getImageWidth() / characterWidth); characterHeightInTexture = texture.getHeight() / (texture.getImageHeight() / characterHeight); // work out the number of characters that fit across the sheet charactersAcross = texture.getImageWidth() / characterWidth; // chosen an arbitary value here to move the letters a bit // closer together when rendering them characterStep = characterWidth - 5; }
So, we're provided with the texture and told how big each character is on the font sheet. Based on this we can work out how much as a fraction of the texture each character takes up and hence how far along in texture coordinates we need to move to get to each character. Sounds a bit complicated? Think about it as taking a square and cutting it up into a grid. We know that in texture coordinates the whole square is 1.0 is width. If there are 16 characters across the square then each character takes up 1.0 divided by 16.0 texture coordinates up.
Right, so how do we use this fact to draw the string? Now this gets a bit tricky, heres the rendering code:
public void drawString(int font, String text, int x, int y) { // bind the font text so we can render quads with the characters // on texture.bind(); // turn blending on so characters are displayed above the // scene GL11.glEnable(GL11.GL_BLEND); GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); // cycle through each character drawing a quad to the screen // mapped to the right part of the texture GL11.glBegin(GL11.GL_QUADS); for (int i=0;i<text.length();i++) { // get the index of the character baesd on the font starting // with the space character int c = text.charAt(i) - ' '; // work out the u,v texture mapping coordinates based on the // index of the character and the amount of texture per // character float u = ((c % charactersAcross) * characterWidthInTexture); float v = 1 - ((c / charactersAcross) * characterHeightInTexture); v -= font * 0.5f; // setup the quad GL11.glTexCoord2f(u, v); GL11.glVertex2i(x+(i*characterStep), y); GL11.glTexCoord2f(u, v - characterHeightInTexture); GL11.glVertex2i(x+(i*characterStep), y+characterHeight); GL11.glTexCoord2f(u + characterWidthInTexture, v - characterHeightInTexture); GL11.glVertex2i(x+(i*characterStep)+characterWidth, y+characterHeight); GL11.glTexCoord2f(u + characterWidthInTexture, v); GL11.glVertex2i(x+(i*characterStep)+characterWidth, y); } GL11.glEnd(); // reset the blending GL11.glDisable(GL11.GL_BLEND); }
Lets take this in small steps. The first thing we do is bind to the font texture since this is what we want to draw with this makes sense. Next we enable blending - this allows us to draw the text to the screen but not draw the parts of the texture whose alpha value is 0. Remember we looked at the font texture itself above, the background has an alpha of 0 so enabling blending like this will allow us to draw the texture as through its overlayed over the image.
Next we start drawing the actual string. We're going to draw quad per character and map the appropriate part of the texture onto the quad. So for each character we draw a quad at the right place on the screen. This is based on the coordinates we want to draw the string at and a bit of space for each character drawn (so we move left as we're writing characters). The important bit to note here is how we caculate the texture coordinates.
float u = ((c % charactersAcross) * characterWidthInTexture); float v = 1 - ((c / charactersAcross) * characterHeightInTexture);
So the u (horizontal texture coordinate) value is caculated by finding the modulus (or remainder) of the character we're looking for against the number of characters across the font sheet. We take this value and multiply by how much of the texture is taken up by each character horizontally. The v (vertical texture coordinate) value is the calculated in a similar way accept that we use division to work out which row the character appears on. The "one minus" at the start of the caculation is due to the way that textures are being loaded in. They're actually up-side down so this "one minus" offsets the texture coordinate appropriately.
How about a worked example. Lets say we want character 21 (maybe its "A"). The font texture has 16 character across so its column will be 21 % 16 which comes to 5. The row will be 21 / 16 which equals 1. So we want the character at 5,1 on the sheet. Multiply these values up by the texture coordinate ratio and we're at the right position on the texture!
Finally we draw the quad using the texture coordiantes we've generated and hence the character is displayed on the screen.
The first simple way the font is use is to display the score and lives in the InGameState. Lets take a look at loading the font in init() first:
public void init(GameWindow window) throws IOException { ... Texture fontTexture = loader.getTexture("res/font.png"); font = new BitmapFont(fontTexture, 32, 32); ... }
We load up the font texture in the normal way and create a font object based on the texture and knowledge that the characters on the sheet are 32 pixels by 32 pixels. As we've seen all the calculations go on inside the BitmapFont object to work out how to display the text.
Now we need to render the text, during the render() method we call drawGUI() which looks like this:
private void drawGUI(GameWindow window) { window.enterOrtho(); GL11.glColor3f(1,1,0); font.drawString(1, "SCORE:" + score, 5, 5); GL11.glColor3f(1,0,0); String lifeString = ""; for (int i=0;i<life;i++) { lifeString += "O"; } font.drawString(0, lifeString, 795 - (lifeString.length() * 27), 5); GL11.glColor3f(1,1,1); if (gameOver) { font.drawString(1, "GAME OVER", 280, 286); } window.leaveOrtho(); }
We finally get to use our GameWindow utility methods that we saw in Part 1 of the tutorial! The first thing we do here is to enter orthographic projection mode - this allows us to draw our text in 2D style coordinates as we talked about in Part 1. Next we set the color to yellow and use the font to draw the current score in the top left corner of the screen. Then we create a string that contains one zero for each life remaining, set the color to red and draw this string in the top right corner. If the player has lost and the game is over we display the "GAME OVER" message in the middle of the screen in white.
Finally and importantly, we leave orthographic projection mode which resets everything to how it was before we entered this method.
Yet again, once we've written utilities to perform bitmap font rendering the actual code to draw text to the screen becomes very simple. This is generally what we should be aiming for, one time complexity reused over and over in very simple ways.
The other major place where we've used the font rendering is in the game menu thats been added to the front of the game. To achieve the game menu we've added a second game state called MenuState. This class is strictly responsible for rendering and maintaing the game menu. It utilises the BitmapFont class in its render() method as follows:
/** The options to present to the user */ private String[] options = new String[] {"Start Game", " Exit"}; public void render(GameWindow window, int delta) { GL11.glColor3f(0.2f,0.2f,0.3f); drawBackground(window); window.enterOrtho(); GL11.glColor3f(1f,1f,1f); font.drawString(1, "ASTEROIDS", 280, 210); for (int i=0;i<options.length;i++) { GL11.glColor3f(0.5f,0.5f,0); if (selected == i) { GL11.glColor3f(1,1,0.3f); } font.drawString(0, options[i], 270, 280+(i*40)); } window.leaveOrtho(); }
So, we draw a title in the center of the screen, "ASTEROIDS". Next we cycle through the menu options drawing them in the center of the screen. Note how we change the color when we're about draw the menu option that is currently selected. Also note how all the rendering of the menu is sandwiched between enterOrtho() and leaveOrtho() again.
To complete the picture lets take a look at the update() method that handles the keyboard input on the menu screen and changing state:
public void update(GameWindow window, int delta) { while (Keyboard.next()) { if (Keyboard.getEventKeyState()) { if (Keyboard.getEventKey() == Keyboard.KEY_UP) { selected--; if (selected < 0) { selected = options.length - 1; } } if (Keyboard.getEventKey() == Keyboard.KEY_DOWN) { selected++; if (selected >= options.length) { selected = 0; } } if (Keyboard.getEventKey() == Keyboard.KEY_RETURN) { if (selected == START) { window.changeToState(InGameState.NAME); } if (selected == EXIT) { System.exit(0); } } } } }
In LWJGL the Keyboard class holds a list of events that occured since last LWJGL update(). Here we cycle through these events looking for controls we're interested in. The up and down cursors are used to move the selected option up and down. The return key changes us to the InGameState (i.e. it starts the game) with window.changeToState(InGameState.NAME).
Taking a look a the MenuState we can see how a reasonably short piece of code can actually give us a complete game menu all being rendered in OpenGL.
We're getting close to completion now. If you've been following through the first four parts then well done! We've now looked at getting a window on the screen, building a simple game framework, creating a data model for the game, rendering models, playing sound effects and displaying text and overlays over the game. Our last part is going to add some simple effects to make the whole game feel that bit more glowy!
The term particle engine is thrown around alot in games development with no one being really clear what it is. For our purposes a particle engine is a data structure that maintains a set of points that are effected by "some external force". These points are then rendered as small quads which appear on the screen as blobs of color. Each particle maintains its position, color and faded-ness (alpha) which can be changed by this "external force". Now, the reason we lump the particles together in an engine is two fold.
First, performance, updating and rendering all the particles together can save us time. For instance, rendering all the particles as one large OpenGL vertex array rather than as individual quads can really speed up rendering. Second, particles can interact with each other and maintaining them from a single location makes this very easy.
When particle engines are implemented generically they can be used for some really nice effects. This flame is generated using a generic particle system with some specific parameters. The same particle engine can be used for lazer fire, smoke and explosions.
The particle engine we're going to create for the asetroids game here is alot simpler than a full generic system. Its going to be used to render the lazers fired by the player and a engine glow from the player's ship.
The particle engine for our asteroids game is contained within a single class named ParticleGroup. This class is responsible for holding all the information about the particles, updating it as time goes by and rendering the particles themself through OpenGL. First, lets look at the data thats being held in the class for each particle:
/** The positions of the particles being rendered */ private float[][] pos; /** The life left in each particle being rendered */ private int[] life; /** The initial size of each particle */ private float[] initialSize; /** The current size of each particle */ private float[] size; /** The alpha value of each particle */ private float[] alpha; /** The index of the next particle to be used */ private int next; /** The time particles should take to fade out */ private int fadeOut; /** The red component of the colour of each particle */ private float r; /** The green component of the colour of each particle */ private float g; /** The blue component of the colour of each particle */ private float b;
"pos" stores the position coordinates of each particle. The "life" array stores a counter for each particle that indicate how much longer it has to live, i.e. how much longer in milliseconds it will continue to be updated and rendered. "initialSize" and "size" allow us to scale the particles down as they begin to die. The "alpha" value for each particle defines how faded the particle will be rendered - as particles begin to die we fade them out to prevent them flashing out of existence. The "fadeOut" time defines how long it takes particles to disappear. Finally, the "r", "g" and "b" values define the color that are particle group will take on. In this case we're only going to support one color across all particles.
Right, we understand what data we're holding, lets take a look at creating a group of particles:
public ParticleGroup(int count, int fadeOut, float r, float g, float b) { pos = new float[count][2]; life = new int[count]; size = new float[count]; initialSize = new float[count]; alpha = new float[count]; this.fadeOut = fadeOut; this.r = r; this.g = g; this.b = b; }
Note that we're using arrays for the particles and there are fixed number of particles supported by any group. This is more for the potential gains in performance over using something like a dynamic list. The caller only has the ability to specified very simple parameters, count - the number of particles to be held in the group, fadeOut - the amount of time it takes particles to disappear and r,g,b - the colour of the particles that should be rendered.
Initially there are no particles, heres the method that lets the developer add particles at run time:
public void addParticle(float x, float y, float size, int life) { pos[next][0] = x; pos[next][1] = y; this.size[next] = size; this.initialSize[next] = size; this.life[next] = life; alpha[next] = 1; next++; if (next >= this.life.length) { next = 0; } }
So, each time the entity wants to add an entity it must specify the position at which the particle should appear, its initial size and how long its going to last for. We stick these values into the array entries for the next available particle and increment the next available particle. This isn't the prettiest way to handle particle reuse but it'll work fine for the simple systems we're going to use here.
Heres how the Shot fired from the player uses the particle group in its update:
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); } }
While the shot is still alive we add a particle at the currently location of the shot and then update the particle engine. This gives a lump of particles in a trail behind the shot making it look like a lump of plasma. The way the particles are rendered the more there are in one space the whiter the particles look - this means that there is a white glow around the front of the shot.
The other place we use the particle group is in the Player entity. Here we leave particles behind the ship as it flys giving us a gentle engine trail. Its done like this in the update() method of Player:
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); }
Here we add particles as the player pushes the thrust button. We actually drop the particles just behind the ship to give the feeling they are being pushed out the engine. Again, as the particles congregate around the back of the ship the area gets white glow since the particles all blend together. This is due to the way we blend the particles, lets look at rendering the particle group. First take a look at the renderEngine() method in Player:
private void renderEngine() { // disable lighting for the particles, we want them to // be very glowy GL11.glDisable(GL11.GL_LIGHTING); // When rendering particles we want to blend them together // to give that smoothed look. Note that we're setting the // blend function to GL_ONE which causes the final alpha value // rendered to be ramped up making everything look glowing GL11.glEnable(GL11.GL_BLEND); GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE); // bind to our texture then ask the particle engine to render // all its particles shotTexture.bind(); engine.render(); // turn blending back off, since we're done now GL11.glDisable(GL11.GL_BLEND); }
Much like the bitmap font rendering we saw in the last part of this tutorial, heres where we handle the texture binding and the blending. However, this time we're binding to a texture that looks like a small star - which is painted across every particle quad we draw. Also, note the blending we're using here "GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);" - this blending means thats we'll combine the alpha from the texture with the value GL_ONE (1). This means that the where the alpha value in the texture get high they'll end being drawn as a value of 1 - which in turn gives us that glowly look to the particles.
Ok, so we've set up the texture and blending function, here's the code in render() in ParticleGroup which actually produces the particle quads:
public void render() { GL11.glBegin(GL11.GL_QUADS); for (int i=0;i0) { float scalar = alpha[i] / 3; GL11.glColor4f((r * (1 - scalar)) + scalar, (g * (1 - scalar)) + scalar, (b * (1 - scalar)) + scalar, alpha[i]); GL11.glTexCoord2f(0,0); GL11.glVertex3f(pos[i][0]-size[i],pos[i][1]-size[i],-0.3f); GL11.glTexCoord2f(1,0); GL11.glVertex3f(pos[i][0]+size[i],pos[i][1]-size[i],-0.3f); GL11.glTexCoord2f(1,1); GL11.glVertex3f(pos[i][0]+size[i],pos[i][1]+size[i],-0.3f); GL11.glTexCoord2f(0,1); GL11.glVertex3f(pos[i][0]-size[i],pos[i][1]+size[i],-0.3f); } } GL11.glEnd(); GL11.glColor4f(1,1,1,1); }
The code above draws each of the particles as part of a OpenGL quad array. The size and position is read stright from the arrays we hold as part of the group (the z coordinate is constant since we want the quads to face the screen).
The colour of quad is based partly on the color requested by the ParticleGroup creator and partly on the remaining alpha of the particle (the value that goes down as the particle fades out). This alpha value is decremented as the particle gets older causing it to fade and dim as it dies. Here's how that happens in the update() method:
public void update(int delta) { // cycle through every particle, aging it. It the particle // is still alive this frame then fade it and shrink it for (int i=0;i= 0) { life[i] -= delta; if (life[i] < fadeOut) { alpha[i] = life[i] / ((float) fadeOut); size[i] = (life[i] / ((float) fadeOut)) * initialSize[i]; } } } }
When the particle group is updated the particles are cycled through. Each particle is aged based on the amount of time thats passed since the last update. This causes the particles to eventually shrink and fade as we've seen above. Note the "if (life[i] >= 0)" condition. If the particles have no life, they're ignored.
Cool! Now we've got a simple particle engine to fit our needs and its integrated with entities. This last touch makes the game a little more flash and adds a bit of a wow factor.