The Game Screen

We are nearing the completion of Super Jumper. The last thing we need to implement is the game screen, which will present the actual game world to the player and allow it to interact with it. The game screen consists of five subscreens, as shown in Figure 9-2. We have the ready screen, the normal running screen, the next-level screen, the gameover screen, and the pause screen. The game screen in Mr. Nom was similar to this; it only lacked a next-level screen, as there was only one level. We will use the same approach as in Mr. Nom: we'll have separate update and present methods for all subscreens that update and render the game world, as well as the UI elements that are part of the subscreens. Since the game screen code is a little longer, we'll split it up into multiple listings here. Listing 9-19 shows the first part of the game screen.

Listing 9-19. Excerpt from GameScreen.java: Members and Constructor package com.badlogic.androidgames.jumper;

import java.util.List;

import javax.microedition.khronos.opengles.GL10;

import com.badlogic.androidgames.framework.Game; import com.badlogic.androidgames.framework.Input.TouchEvent; import com.badlogic.androidgames.framework.gl.Camera2D; import com.badlogic.androidgames.framework.gl.FPSCounter; import com.badlogic.androidgames.framework.gl.SpriteBatcher; import com.badlogic.androidgames.framework.impl.GLScreen; import com.badlogic.androidgames.framework.math.OverlapTester; import com.badlogic.androidgames.framework.math.Rectangle; import com.badlogic.androidgames.framework.math.Vector2; import com.badlogic.androidgames.jumper.World.WorldListener;

public class GameScreen extends GLScreen {

static final

int

GAME

READY = 0;

static final

int

GAME

'RUNNING = 1;

static final

int

GAME

'PAUSED = 2;

static final

int

GAME

'level END = 3;

static final

int

GAME

'over = 4;

int state; Camera2D guiCam; Vector2 touchPoint;

int state; Camera2D guiCam; Vector2 touchPoint;

SpriteBatcher batcher;

World world;

WorldListener worldListener;

WorldRenderer renderer;

Rectangle pauseBounds;

Rectangle resumeBounds;

Rectangle quitBounds;

int lastScore;

String scoreString;

The class starts off with a couple of constants defining the five states that the screen can be in. Next we have the members. We have a camera for rendering the UI elements, as well as a vector so we can transform touch coordinates to world coordinates (as in the other screens, to a view frustum of 320x480 units, our target resolution). Next we have a SpriteBatcher, a World instance, and a WorldListener. The WorldRenderer class is something we'll look into in a minute. It basically just takes a World and renders it. Note that it takes a reference to the SpriteBatcher as well as the World as parameters of its constructors. This means we'll use the same SpriteBatcher to render the UI elements of the screen, as well as the game world. The rest of the members are Rectangles for different UI elements (such as the RESUME and QUIT menu entries on the paused subscreen) and two members for keeping track of the current score. We want to avoid creating a new string every frame when rendering the score so that we make the garbage collector happy.

public GameScreen(Game game) { super(game); state = GAME_READY;

guiCam = new Camera2D(glGraphics, 320, 480); touchPoint = new Vector2(); batcher = new SpriteBatcher(glGraphics, 1000); worldListener = new WorldListener() {

@Override public void jump() {

Assets.playSound(Assets.jumpSound);

@Override public void highJump() {

Assets.playSound(Assets.highJumpSound);

@Override public void hit() {

Assets.playSound(Assets.hitSound);

@Override public void coin() {

Assets.playSound(Assets.coinSound);

world = new World(worldListener);

renderer = new WorldRenderer(glGraphics, batcher, world); pauseBounds = new Rectangle(320- 64, 480- 64, 64, 64);

resumeBounds = new Rectangle(l60 - 96, 240, 192, 36); quitBounds = new Rectangle(l60 - 96, 240 - 36, 192, 36); lastScore = 0; scoreString = "score: 0";

In the constructor we initialize all the member variables. The only interesting thing here is the WorldListener we implement as an anonymous inner class. It's registered with the World instance and will play back sound effects according to the event that gets reported to it.

Updating the GameScreen

Next we have the update methods, which will make sure any user input is processed correctly and will also update the World instance if necessary. Listing 9-20 shows the code.

Listing 9-20. Excerpt from GameScreen.java: The Update Methods ^Override public void update(float deltaTime) { if(deltaTime > 0.1f) deltaTime = 0.lf;

switch(state) { case GAME_READY: updateReady(); break; case GAME_RUNNING:

updateRunning(deltaTime); break; case GAME_PAUSED: updatePaused(); break; case GAME_LEVEL_END: updateLevelEnd(); break; case GAME_OVER:

updateGameOver(); break;

We have the GLScreen.update() method as the master method again, which calls one of the other update methods depending on the current state of the screen. Note that we limit the delta time to 0.1 seconds. Why do we do that? In Chapter 6 we talked about a bug in the direct ByteBuffers on Android version 1.5, which generates garbage. We will have that problem in Super Jumper as well on Android 1.5 devices. Every now and then our game will be interrupted for a couple of hundred milliseconds by the garbage collector. This would be reflected in a delta time of a couple of hundred milliseconds, which would make Bob sort of teleport from one place to another instead of smoothly moving there. That's annoying for the player and it also has an effect on our collision detection. Bob could tunnel through a platform without ever overlapping with it due to him moving a large distance in a single frame. By limiting the delta time to a sensible maximum value of 0.1 seconds, we can compensate for those effects.

private void updateReady() {

if(game.getInput().getTouchEvents().size() > 0) { state = GAME_RUNNING;

The updateReady() method is invoked in the paused subscreen. All it does is wait for a touch event, in which case it will change the state of the game screen to the GAME_RUNNING state.

private void updateRunning(float deltaTime) {

List<TouchEvent> touchEvents = game.getInput().getTouchEvents(); int len = touchEvents.size(); for(int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i); if(event.type != TouchEvent.TOUCH_UP) continue;

touchPoint.set(event.x, event.y); guiCam.touchToWorld(touchPoint);

if(OverlapTester.pointInRectangle(pauseBounds, touchPoint)) { Assets.playSound(Assets.clickSound); state = GAME_PAUSED; return;

world.update(deltaTime, game.getInput().getAccelX()); if(world.score != lastScore) { lastScore = world.score; scoreString = "" + lastScore;

if(world.state == World.WORLD_STATE_NEXT_LEVEL) { state = GAME_LEVEL_END;

if(world.state == World.WORLD_STATE_GAME_OVER) { state = GAME_OVER;

if(lastScore >= Settings.highscores[4])

scoreString = "new highscore: " + lastScore;

else scoreString = "score: " + lastScore; Settings.addScore(lastScore); Settings.save(game.getFileIO());

In the updateRunning() method, we first check whether the user touched the pause button in the upper-right corner. If that's the case, then the game is put into the GAME_PAUSED state. Otherwise we update the World instance with the current delta time and the x-axis value of the accelerometer, which are responsible for moving Bob horizontally. After the world is updated we check whether our score string needs updating. We also check whether Bob has reached the castle, in which case we enter the GAME_NEXT_LEVEL state, which will show the message in the top left screen in Figure 9-2, and will wait for a touch event to generate the next level. In case the game is over, we set the score string to either score: #score or new highscore: #score, depending on whether the score achieved is a new high score. We then add the score to the Settings and tell it to save all the settings to the SD card. Additionally we set the game screen to the GAME_OVER state.

private void updatePaused() {

List<TouchEvent> touchEvents = game.getInput().getTouchEvents(); int len = touchEvents.size(); for(int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i); if(event.type != TouchEvent.TOUCH_UP) continue;

touchPoint.set(event.x, event.y); guiCam.touchToWorld(touchPoint);

if(OverlapTester.point!nRectangle(resumeBounds, touchPoint)) { Assets.playSound(Assets.clickSound); state = GAME_RUNNING; return;

if(OverlapTester.pointInRectangle(quitBounds, touchPoint)) { Assets.playSound(Assets.clickSound); game.setScreen(new MainMenuScreen(game)); return;

In the updatePaused() method we check whether the user has touched the RESUME or QUIT UI elements and react accordingly.

private void updateLevelEnd() {

List<TouchEvent> touchEvents = game.getInput().getTouchEvents(); int len = touchEvents.size(); for(int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i); if(event.type != TouchEvent.TOUCH_UP)

continue; world = new World(worldListener);

renderer = new WorldRenderer(glGraphics, batcher, world); world.score = lastScore; state = GAME_READY;

In the updateLevelEnd() method we check for a touch-up event; if there has been one, we create a new World and WorldRenderer instance. We also tell the World to use the score achieved so far and set the game screen to the GAME_READY state, which will again wait for a touch event.

private void updateGameOver() {

List<TouchEvent> touchEvents = game.getInput().getTouchEvents(); int len = touchEvents.size(); for(int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i); if(event.type != TouchEvent.TOUCH_UP) continue;

game.setScreen(new MainMenuScreen(game));

In the updateGameOver() method, we again just check for a touch event, in which case we simply transition to back to the main menu, as indicated in Figure 9-2.

Rendering the GameScreen

After all those updates, the game screen will be asked to render itself via a call to GameScreen.present(). Let's have a look at that method in Listing 9-21.

Listing 9-21. Excerpt from GameScreen.java: The Rendering Methods

^Override public void present(float deltaTime) { GL10 gl = glGraphics.getGL(); gl.glClear(GL10. GL_COLOR_BUFFER_BIT); gl.glEnable(GL10. GL_TEXTURE_2D);

renderer.render();

guiCam.setViewportAndMatrices(); gl.glEnable(GL10.GL_BLEND);

gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); batcher.beginBatch(Assets.items); switch(state) { case GAME_READY: presentReady(); break; case GAME_RUNNING: presentRunning(); break; case GAME_PAUSED: presentPaused(); break; case GAME_LEVEL_END: presentLevelEnd(); break; case GAME_OVER:

presentGameOver(); break;

batcher.endBatch(); gl.glDisable(GL10.GL_BLEND);

Rendering of the game screen is done in two steps. We first render the actual game world via the WorldRenderer class, and then render all the UI elements on top of the game world depending on the current state of the game screen. The render() method does just that. As with our update methods, we again have a separate rendering method for all the subscreens.

private void presentReady() {

batcher.drawSprite(160, 240, 192, 32, Assets.ready);

The presentRunning() method just displays the pause button in the top-right corner, as well as the score string in the top-left corner.

private void presentRunning() {

batcher.drawSprite(320 - 32, 480 - 32, 64, 64, Assets.pause); Assets./ont.drawText(batcher, scoreString, 16, 480-20);

In the presentRunning() method we simply render the pause button and the current score string.

private void presentPaused() {

batcher.drawSprite(160, 240, 192, 96, Assets.pauseMenu); Assets./ont.drawText(batcher, scoreString, 16, 480-20);

The presentPaused() method displays the pause menu UI elements and the score again.

private void presentLevelEnd() {

String bottomText = "in another castle!";

float topWidth = Assets./ont.glyphWidth * topText.length();

float bottomWidth = Assets./ont.glyphWidth * bottomText.length();

Assets./ont.drawText(batcher, topText, 160 - topWidth / 2, 480 - 40);

Assets./ont.drawText(batcher, bottomText, 160 - bottomWidth / 2, 40);

The presentLevelEnd() method renders the string THE PRINCESS IS ... at the top of the screen and the string IN ANOTHER CASTLE! at the bottom of the screen, as shown in Figure 9-2. We perform some calculations to center those strings horizontally.

private void presentGameOver() {

batcher.drawSprite(160, 240, 160, 96, Assets.gameOver);

float scoreWidth = Assets./ont.glyphWidth * scoreString.length();

Assets./ont.drawText(batcher, scoreString, 160 - scoreWidth / 2, 480-20);

The presentGameOver() method displays the game-over UI element as well the score string. Remember that the score screen is set in the updateRunning() method to either score: #score or new highscore: #value.

Finishing Touches

That's basically our game screen class. The rest of its code is given in Listing 9-22. Listing 9-22. The Rest of GameScreen.java: The pause(), resume(), and dispose) Methods @Override public void pause() {

if(state == GAME_RUNNING) state = GAME_PAUSED;

@Override public void resume() { }

@Override public void dispose() { }

We just make sure our game screen is paused when the user decides to pause the application.

The last thing we have to implement is the WorldRenderer class.

0 0

Post a comment