Introduction


Since writing the Space Invaders 101-104 tutorials I've been asked a few times to produce some notes on how to do things in 3D. This tutorial (which will be over a series of parts) aims to produce a Asteroids style game but utilising techniques from the 3D world. The game will however still be essentially 2D. I've taken this approach with the intention of not complicating matters with 3D coordinate systems. This tutorial isn't as simple as the Space Invaders things so its recommended that you've read and worked through them before attempting this one.

The tutorial will be broken down into serveral parts

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.

Source Code and Downloads


This tutorial is based on use LWJGL 0.98 and JOrbis for OGG decoding. The source and pure java libraries are available here (src.zip). The native libraries for each platform are available here:

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.

The Framework

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...

The Game Window

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..

Game Loop

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.

Utility Methods

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

GameState and 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.

InGameState

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.

Summary

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.

Links


Tutorial by Kevin Glass
Light Weight Java Game Library
JOrbis