The Game Screen Class

There's only one more screen to implement. Let's see what that screen does:

As defined in Mr. Nom's design in Chapter 3, it can be in one of three states: waiting for the user to confirm that he's ready, running the game, waiting in a paused state, and waiting for the user to click a button in the game-over state.

In the ready state we simply ask the user to touch the screen to start the game.

In the running state we update the world, render it, and also tell Mr. Nom to turn left and right when the player presses one of the buttons at the bottom of the screen.

In the paused state we simply show two options: one to resume the game and one to quit it.

In the game-over state we tell the user that the game is over and provide him with a button to touch so that he can get back to the main menu.

For each state we have different update and present methods to implement, as each state does different things and shows a different UI.

Once the game is over we have to make sure that we store the score if it is a high score.

That's quite a bit of responsibility, which translates to more code than usual. We'll therefore split up the source listing of this class. Before we dive into the code, let's lay out how we arrange the different UI elements in each state. Figure 6-8 shows the four different states.

Figure 6-8. The game screen in its four states: ready, running, paused, and game-over

Note that we also render the score at the bottom of the screen, along with a line that separates Mr. Nom's world from the buttons at the bottom. The score is rendered with the same routine that we used in the HighscoreScreen. We additionally center it horizontally based on the score string width.

The last missing bit of information is how to render Mr. Nom's world based on its model. That's actually pretty easy. Take a look at Figure 6-1 and 6-5 again. Each cell is exactly 32! 32 pixels in size. The stain images are also 32! 32 pixels in size, and so are the tail parts of Mr. Nom. The head images of Mr. Nom for all directions are 42! 42 pixels, so they don't fit entirely into a single cell. That's not a problem, though. All we need to do to render Mr. Nom's world is take each stain and snake part, and multiply its world coordinates by 32 to arrive at the object's center in pixels on the screen—for example, a stain at (3,2) in world coordinates would have its center at 96! 64 on the screen. Based on these centers, all that's left to do is take the appropriate asset and render it centered around those coordinates. Let's get coding. Listing 6-12 shows the GameScreen class.

Listing 6-12. GameScreen.java package com.badlogic.androidgames.mrnom;

import java.util.List;

import android.graphics.Color;

import com.badlogic.androidgames.framework.Game; import com.badlogic.androidgames.framework.Graphics; import com.badlogic.androidgames.framework.Input.TouchEvent; import com.badlogic.androidgames.framework.Pixmap; import com.badlogic.androidgames.framework.Screen;

public class GameScreen extends Screen { enum GameState { Ready, Running, Paused, GameOver

GameState state = GameState.Ready;

World world;

int oldScore = 0;

We start off by defining an enumeration called GameState that encodes our four states (ready, running, paused, and game-over). Next we define a member that holds the current state of the screen, another member that holds the World instance, and two more members that hold the currently displayed score in the form of an integer and as a string. The reason we have the last two members is that we don't want to constantly create new strings from the World.score member each time we draw the score. Instead we'll cache the string and only create a new one when the score changes. That way we play nice with the garbage collector.

public GameScreen(Game game) { super(game); world = new World();

The constructor just calls the superclass constructor and creates a new World instance. The game screen will be in the ready state after the constructor returns to the caller.

@Override public void update(float deltaTime) {

List<TouchEvent> touchEvents = game.getInput().getTouchEvents(); game.getInput().getKeyEvents();

if(state == GameState.Ready) updateReady(touchEvents); if(state == GameState.Running)

updateRunning(touchEvents, deltaTime); if(state == GameState.Paused) updatePaused(touchEvents); if(state == GameState.GameOver) updateGameOver(touchEvents);

Next comes the screen's update() method. All it does is fetch the TouchEvents and KeyEvents from the input module and then delegate the update to one of the four update methods that we implement for each state based on the current state.

private void updateReady(List<TouchEvent> touchEvents) { if(touchEvents.size() > 0) state = GameState.Running;

The next method is called updateReady(). It will be called when the screen is in the ready state. All it does is check if the screen was touched. If that's the case, it changes the state to running.

private void updateRunning(List<TouchEvent> touchEvents, float deltaTime) { int len = touchEvents.size(); for(int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i); if(event.type == TouchEvent.TOUCH_UP) { if(event.x < 64 && event.y < 64) { if(Settings.soundEnabled) Assets.click.play(1); state = GameState.Paused; return;

if(event.type == TouchEvent.TOUCH_DOWN) { if(event.x < 64 && event.y > 416) { world.snake.turnLeft();

if(event.x > 256 && event.y > 416) { world.snake.turnRight();

world.update(deltaTime); if(world.gameOver) {

if(Settings.soundEnabled) Assets.bitten.play(1); state = GameState.GameOver;

if(oldScore != world.score) { oldScore = world.score; score = "" + oldScore; if(Settings.soundEnabled)

Assets.eat.play(1);

The updateRunning()method first checks whether the pause button in the top-left corner of the screen was pressed. If that's the case, it sets the state to paused. It then checks whether one of the controller buttons at the bottom of the screen was pressed. Note that we don't check for touch-up events here, but for touch-down events. If either of the buttons was pressed, we tell the Snake instance of the World to turn left or right. That's right, the updateRunning() method contains the controller code of our MVC schema! After all the touch events have been checked, we tell the world to update itself with the given delta time. If the World signals that the game is over, we change the state accordingly, and also play the bitten.ogg sound. Next we check if the old score we have cached is different from the score that the World stores. If it is, then we know two things: Mr. Nom has eaten a stain, and the score string must be changed. In that case, we play the eat.ogg sound. And that's all there is to the running state update.

private void updatePaused(List<TouchEvent> touchEvents) { int len = touchEvents.size(); for(int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i); if(event.type == TouchEvent.TOUCH_UP) { if(event.x > 80 && event.x <= 240) {

if(event.y > 100 && event.y <= 148) { if(Settings.soundEnabled) Assets.click.play(1); state = GameState.Running; return;

if(event.y > 148 && event.y < 196) { if(Settings.soundEnabled) Assets.click.play(1); game.setScreen(new MainMenuScreen(game)); return;

The updatePaused() method again just checks whether one of the menu options was touched and changes state accordingly.

private void updateGameOver(List<TouchEvent> touchEvents) { int len = touchEvents.size(); for(int i = 0; i < len; i++) {

TouchEvent event = touchEvents.get(i); if(event.type == TouchEvent.TOUCH_UP) { if(event.x >= 128 && event.x <= 192 && event.y >= 200 && event.y <= 264) { if(Settings.soundEnabled) Assets.click.play(1); game.setScreen(new MainMenuScreen(game)); return;

The updateGameOver() method also just checks if the button in the middle of the screen was pressed. If it has, then we initiate a screen transition back to the main menu screen.

@Override public void present(float deltaTime) { Graphics g = game.getGraphics();

g.drawPixmap(Assets.background, 0, 0);

drawWorld(world);

if(state == GameState.Ready)

drawReadyUI(); if(state == GameState.Running)

drawRunningUI(); if(state == GameState.Paused)

drawPausedUI(); if(state == GameState.GameOver) drawGameOverUI();

drawText(g, score, g.getWidth() / 2 - score.length()*20 / 2, g.getHeight() -

Next up are the rendering methods. The present() method first draws the background image, as that is needed in all states. Next it calls the respective drawing method for the state we are in. Finally it renders Mr. Nom's world and draws the score at the bottomcenter of the screen.

private void drawWorld(World world) {

Graphics g = game.getGraphics(); Snake snake = world.snake; SnakePart head = snake.parts.get(0); Stain stain = world.stain;

Pixmap stainPixmap = null; if(stain.type == Stain.TYPE_1)

stainPixmap = Assets.stain1; if(stain.type == Stain.TYPE_2)

stainPixmap = Assets.stain2; if(stain.type == Stain.TYPE_3)

stainPixmap = Assets.stain3; int x = stain.x * 32; int y = stain.y * 32; g.drawPixmap(stainPixmap, x, y);

int len = snake.parts.size(); for(int i = 1; i < len; i++) {

SnakePart part = snake.parts.get(i); x = part.x * 32; y = part.y * 32;

g.drawPixmap(Assets.tail, x, y);

Pixmap headPixmap = null; if(snake.direction == Snake.UP) headPixmap = Assets.headUp; if(snake.direction == Snake.LEFT) headPixmap = Assets.headLeft; if(snake.direction == Snake.DOWN) headPixmap = Assets.headDown; if(snake.direction == Snake.RIGHT) headPixmap = Assets.headRight; x = head.x * 32 + 16; y = head.y * 32 + 16;

g.drawPixmap(headPixmap, x - headPixmap.getWidth() / 2, y -

The drawWorld() method draws the world, as we just discussed. It starts off by choosing the Pixmap to use for rendering the stain, and then draws it and centers it horizontally at its screen position. Next we render all the tail parts of Mr. Nom, which is pretty simple. Finally we choose which Pixmap of the head to use based on Mr. Nom's direction, and draw that Pixmap at the position of the head in screen coordinates. As with the other objects, we also center the image around that position. And that's the code of the view in MVC.

private void drawReadyUI() {

Graphics g = game.getGraphics();

g.drawPixmap(Assets.ready, 47, 100); g.drawLine(0, 416, 480, 416, Color.BLACK);

private void drawRunningUI() {

Graphics g = game.getGraphics();

g.drawPixmap(Assets.buttons, 0, 0, 64, 128, 64, 64);

g.drawPixmap(Assets.buttons, 0, 416, 64, 64, 64, 64);

g.drawPixmap(Assets.buttons, 256, 416, 0, 64, 64, 64);

private void drawPausedUI() {

Graphics g = game.getGraphics();

g.drawPixmap(Assets.pause, 80, 100); g.drawLine(0, 416, 480, 416, Color.BLACK);

private void drawGameOverUI() {

Graphics g = game.getGraphics();

g.drawPixmap(Assets.gameOver, 62, 100); g.drawPixmap(Assets.buttons, 128, 200, 0, 128, 64, 64); g.drawLine(0, 416, 480, 416, Color.BLACK);

public void drawText(Graphics g, String line, int x, int y) {

char character = line.charAt(i);

int srcX = 0; int srcWidth = 0; if (character == '.') { srcX = 200; srcWidth = 10; } else {

g.drawPixmap(Assets.numbers, x, x += srcWidth;

The methods drawReadUI(), drawRunningUI(), drawPausedUI(), and drawGameOverUI() are nothing new. They perform the same old UI rendering as always, based on the coordinates shown Figure 6-8. The drawText() method is the same as the one in HighscoreScreen, so we won't discuss that one either.

@Override public void pause() {

if(state == GameState.Running) state = GameState.Paused;

if(world.gameOver) {

Settings.addScore(world.score); Settings.save(game.getFileIO());

@Override public void resume() { }

@Override public void dispose() { }

Finally there's one last vital method, pause(), which gets called when the activity is paused or the game screen is replaced by another screen. That's the perfect place to save our settings. First we set the state of the game to paused. If the paused() method got called due to the activity being paused, this will guarantee that the user will be asked to resume the game when she returns to it. That's good behavior, as it would be stressful to immediately pick up from where one left the game. Next we check whether the game screen is in a game-over state. If that's the case, we add the score the player achieved to the high scores (or not, depending on its value) and save all the settings to the external storage.

And that's it. We've written a full-fledged game for Android from scratch! We can be proud of ourselves, as we've conquered all the necessary topics to create almost any game we like. From here on, it's mostly just cosmetics.

0 0

Post a comment