Introduction

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.

Decoding OGG files

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.

Playing the sounds in OpenAL

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!

Sound Effects in Asteroids

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

GUI and Menu

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.

Using the Bitmap Font

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 Menu State

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.

Conclusion


This part has added what seem like minor things to start with, a simple piece of text to display the score and some sound effects. However, while these things don't seem greatly important at the start of the game they're what makes a game feel more "complete" more like a real game. Its important not to under estimate how much sound effects add to the atmosphere of a game.

Links


Tutorial written by Kevin Glass
JOrbis from JCraft
Bitmap Font Builder from Thom Wetze