- Android:Game Programming
- John Horton Raul Portales
- 6700字
- 2021-07-14 10:00:33
Chapter 8. The Snake Game
In this chapter, we will get straight down to designing and implementing a clone of the highly addictive Snake game. We will look at the design of the game and learn how to animate some bitmaps. Then we will look at a few aspects of the code that are new, such as our coordinate system. After that, we will whiz through the implementation of the game. Finally, we will look at how we could enhance our game.
In this chapter, we will cover the following topics:
- Examine the design of our game
- Look at the coordinate system of our Snake game
- Examine the code structure so that when we come to implement the game, it will be more straightforward
- Learn about animation with sprite sheets at the same time as implementing the home screen of our game
- Break the code for the Snake game into manageable chunks and run through its full implementation
- Enhance the game a little
Game design
If you haven't played the excellent Snake game before, here is an explanation of how it works. You control a very small snake. In our version, there is just a head, one body segment, and a tail. Here is a screenshot of our snake, made out of three segments:

The following screenshot shows the three segments individually:

Now, here is the thing; our snake is very hungry and also a very quick grower. Every time he eats an apple, he grows a body segment. This is a screenshot of the apple:

Life is great! Our snake just eats and grows! The problem that the player of our game needs to solve is that the snake is a little hyperactive. It never stops moving! What exacerbates this problem is that if the snake touches the side of the screen, it dies.
At first, this doesn't seem like too much of a problem, but as he grows longer and longer, he can't just keep going around in circles because he will bump inevitably into himself. This would again result in his demise:

For each apple eaten, we add an increasingly large amount to the score. Here is a sneak peek at what the game will look like after the basic implementation and before the enhancements:

The player controls the snake by tapping on the left or the right side of the screen. The snake will respond by turning left or right. The turn directions are relative to the direction the snake is traveling, which adds to the challenge because the player needs to think like a snake—kind of!
At the end of the chapter, we will also take a brief look at enhancing the game, use that enhanced version in the next chapter to publish it to the Google Play Store, and add leaderboards and achievements.
The coordinate system
In the previous chapter, we drew all our game objects directly to points on the screen, and we used real screen coordinates to detect collisions, bounces, and so on. This time, we will be doing things slightly differently. This is partly out of necessity, but as we will see, collision detection and keeping track of our game objects will also get simpler. This might be surprising when we think about the potential of our snake to be many blocks long.
Keeping track of the snake segments
To keep track of all the snake segments, we will first define a block size to define a portion of a grid for the entire game area. Every game object will reside at an (x,y) coordinate, based not on the pixel resolution of the screen but on a position within our virtual grid. In the game, we define a grid that is 40 blocks wide, like this:
//Determine the size of each block/place on the game board blockSize = screenWidth/40;
So we know that:
numBlocksWide = 40;
The height of the game screen in blocks will then simply be calculated by dividing the height of the screen in pixels by the previously determined value of blockSize
minus a bit of space at the top for the score:
numBlocksHigh = ((screenHeight - topGap ))/blockSize;
This then allows us to keep track of our snake using two arrays for x and y coordinates, where element zero is the head and the last used element is the tail, a bit like this:
//An array for our snake snakeX = new int[200]; snakeY = new int[200];
As long as we have a system for moving the head, perhaps something similar to the squash ball but based on our new game grid, we can do the following to make the body follow the head:
//move the body starting at the back for(int i = snakeLength; i >0 ; i--){ snakeX[i] = snakeX[i-1]; snakeY[i] = snakeY[i-1]; }
The previous code simply starts at the back section of the snake and creates its location in the grid irrespective of what the section in front of it was. It proceeds up the body doing the same until everything has been moved to the location of the section that used to be just ahead of it.
This also makes collision detection (even for a very long snake) nice and easy.
Detecting collisions
Using our grid based on blockSize
, we can detect a collision, for example, with the right side of the screen, like this:
if(snakeX[0] >= numBlocksWide)dead=true;
The previous code simply checks whether the first element of our array, which holds the x coordinate of the snake, is equal to or greater than the width of our game grid in blocks. Try to work out the code for collision with the left, top, and bottom before we see it during the implementation.
Detecting the event of the snake bumping into itself is quick too. We just need to check whether the first element of our array (the head) is in exactly the same position as any of the other sections, like this:
//Have we eaten ourselves? for (int i = snakeLength-1; i > 0; i--) { if ((i > 4) && (snakeX[0] == snakeX[i]) && (snakeY[0] == snakeY[i])) { dead = true; } }
Drawing the snake
We simply draw every section of the snake relative to its grid location multiplied by the size in pixels of a block. The blockSize
variable handles the entire challenge of making the game work on different screen sizes, like this:
//loop through every section of the snake and draw it //a block at a time. canvas.drawBitmap(bodyBitmap, snakeX[i]*blockSize, (snakeY[i]*blockSize)+topGap, paint);
Admittedly, there are probably more questions about how our implementation will work, but they are probably best answered by actually building the game.
Thus, we can easily follow along by either writing the code or just reading from the completed project. Let's take a look at the overall structure of our code.
The code structure
We will have two activities, one for the menu screen and one for the game screen. The menu screen activity will be called MainActivity
, and the game screen activity will be called GameActivity
. You can find all the completed code files as well as all the assets such as images, sprite sheets, and sound files in the Chapter8/Snake
folder in the download bundle.
MainActivity
In contrast to our other projects, the menu screen will not have a UI designed in the Android Studio UI designer. It will consist of an animated snake head, a title, and a high score. The player will proceed to GameActivity
by tapping anywhere on the screen. As we need to accomplish animations and user interactions, even the home screen will have a thread, a view object, and methods normally associated with our game screens, like this:
MainActivity.java file Imports MainActivity class Declare some variables and objects onCreate SnakeAnimView class Constructor Run method Update method Draw method controlFPS method pause method resume method onTouchEvent method onStop method onResume method onPause method onKeyDown method
We will not go deeper into the menu screen for now because at the end of this section, we will implement it line by line.
GameActivity
The game screen structure has many similarities to our Squash game and to the structure of the menu screen, although the internals of this structure vary a lot (as we have discussed and as we will see). There are some differences towards the end of the structure, most notably, a loadSound
method and a configureDisplay
method. Here is the structure (we will see afterwards why the two extra methods are there):
MainActivity.java file Imports GameActivity class Declare some variables and objects onCreate SnakeView class Constructor getSnake method getApple method Run method updateGame method drawGame method controlFPS method pause method resume method onTouchEvent method onStop method onResume method onPause method onKeyDown method loadSOund method configureDisplay method
Tidying up onCreate
One of the first things you might notice when you examine the code from the GameActivity
class we will soon implement is just how short the onCreate
method is:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); loadSound(); configureDisplay(); snakeView = new SnakeView(this); setContentView(snakeView); }
We have written two methods, loadSound
and configureDisplay
. They do most of the initialization and setup present in our squash game. This makes our code less cluttered. All that is left in onCreate
is the initialization of our SnakeView
object and a call to setContentView
.
We will look in detail at our loadSound
and configureDisplay
methods when we implement them.
As we have had advanced sight of the structure as well as previous experience of this type of implementation, we will just go through all of the implementation of our game activity in one phase.
Let's quickly implement the menu screen.
Animation, sprite sheets, and the Snake home screen
In the previous chapter, we used a bitmap to draw text, a circle, a line, and a single pixel on the blank bitmap we created in Java code. We then displayed the bitmap with all of its doodling using the Canvas
class. Now we will look at a technique to draw two dimensional images, sometimes referred to as sprites. These are made from predrawn images. The images can be as simple as a plain pong ball or as complex as a glorious two-dimensional character with muscle definition, elaborate clothing, weapons, and hair.
So far, we have animated with unchanging objects, that is, we have moved a static unchanging image around the screen. In this section, we will see how to not only display a predrawn bitmap image on the screen but also continually alter it to create the illusion of on-the-spot animation.
Of course, the ultimate combination would be to animate the bitmap both by changing its image and moving it around at the same time. We will see that briefly when we look at an enhanced version of this chapter's Snake game, but will not be analyzing the code.
To do this on-the-spot bitmap animation, we need some bitmaps, as you might expect. For example, to draw a snake's tail swishing back and forth, we would need at least two frames of animation, showing the tail in different positions. In the following screenshot, the flower's head is towards the left:

In this screenshot, the flower has been flipped:

If the two bitmaps were shown one after the other, repeatedly, they would create the basic effect of a flower blowing in the wind. Of course, two frames of animation aren't going to contest for any animation awards, and there is another problem with these images as well, as we will learn, so we should add in more frames to make the animation as life-like as is practical.
We have just one more thing to discuss before we make an animated snake head for our game's home screen. How do we get Android to switch between these bitmaps?
Animating with sprite sheets
Firstly, we need to present the frames in a manner that is easy to manipulate in code. This is where sprite sheets come in. The following image shows some frames from a basic snake head animation that we will use on our game home screen. This time, they are presented in a strip of frames. All of them are parts of the same image, a bit like a series of images in a film. Also, notice in the following image that the frames are centered relative to each other and are exactly equal in size:

If we were to actually show the two previous flower images consecutively, they would not only would they sway but also jump around from one side to another on their stems, which is probably not the effect we were looking for.
Thus, with regard to the snake sprite sheet, as long as we show one frame after another, we will create a basic animation.
So how do we make our code jump from one part of the sprite sheet to the next? Each frame is exactly the same size, 64 x 64 pixels in this case, so we just need a way to display pixels from 0 to 63, then 64 to 127, then 128 to 192, and so on. As each frame of the sprite sheet image is subtly different, it allows us to use one image file with multiple frames to create our animation. Fortunately, we have a class to handle this, which is nothing quite as luxurious as a specific sprite sheet class but almost.
Tip
Regarding sprite sheet classes, such a thing does exist, although not in the regular Android classes. An API specifically designed for two-dimensional games will usually contain classes for sprite sheets. We will look at examples of this in the next chapter.
The Rect
class holds the coordinates of a rectangle. Here, we create a new object of the Rect
type, and initialize it to start at 0, 0 and end at 63, 63:
Rect rectToBeDrawn = new Rect(0, 0, 63, 63);
The Canvas
class can then actually use our Rect
object to define a portion of a previously loaded bitmap:
canvas.drawBitmap(headAnimBitmap, rectToBeDrawn, destRect, paint);
The preceding code is much simpler than it looks. First, we see canvas.drawBitmap
. We are using the drawBitmap
method of the Canvas
class just as we have before. Then we pass headAnimBitmap
, which is our sprite sheet containing all the frames we want to animate, as an argument. Rect rectToBeDrawn
represents the coordinates of the currently relevant frame within headAnimationBitmap
. destRect
simply represents the screen coordinates at which we want to draw the current frame, and of course, paint
is our object of the Paint
class.
All we have to do now is change the coordinates of rectToBeDrawn
and control the frame rate with a thread and we are done! Let's do that and create an animated home screen for our Snake game.
Implementing the Snake home screen
With the background information we just covered and our detailed look at the structure of the code we are about to write, there shouldn't be any surprises in this code. We will break things up into chunks just to make sure we follow exactly what is going on:
- Create a new project of API level 13. Call it
Snake
. - Make the activity full screen as we have done before, and put your graphics into the
drawable/mdpi
folder. Of course, you can use my graphics as usual. They are supplied in the code download in thegraphics
folder of theSnake
project. - Here, you will find our
MainActivity
class declaration and member variables. Notice the variables for ourCanvas
andBitmap
class as well, we are declaring variables to hold frame size (width and height) as well as the number of frames. We also have aRect
object to hold the coordinates of the current frame of the sprite sheet. We will see these variables in action soon. Type the following code:public class MainActivity extends Activity { Canvas canvas; SnakeAnimView snakeAnimView; //The snake head sprite sheet Bitmap headAnimBitmap; //The portion of the bitmap to be drawn in the current frame Rect rectToBeDrawn; //The dimensions of a single frame int frameHeight = 64; int frameWidth = 64; int numFrames = 6; int frameNumber; int screenWidth; int screenHeight; //stats long lastFrameTime; int fps; int hi; //To start the game from onTouchEvent Intent i;
- The following is the implementation of the overridden
onCreate
method. We get the screen dimensions in the usual way. We load our sprite sheet into theheadAnimBitmap
Bitmap. Finally, we create a newSnakeAnimView
and set it as the content view. Type the following code after the code from the previous step:@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //find out the width and height of the screen Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); screenWidth = size.x; screenHeight = size.y; headAnimBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head_sprite_sheet); snakeAnimView = new SnakeAnimView(this); setContentView(snakeAnimView); i = new Intent(this, GameActivity.class); }
- Here is the declaration of our
SurfaceView
class, calledSnakeAnimView
, along with its member variables. Notice that it extendsSurfaceView
and implementsRunnable
. All its methods follow in the next steps. Type this code after the code from the preceding step:class SnakeAnimView extends SurfaceView implements Runnable { Thread ourThread = null; SurfaceHolder ourHolder; volatile boolean playingSnake; Paint paint;
- Here is the constructor that gets the
frameWidth
value by dividing the bitmap width by the number of frames, and theframeHeight
value using thegetHeight
method. Type this code after the code from the previous step:public SnakeAnimView(Context context) { super(context); ourHolder = getHolder(); paint = new Paint(); frameWidth = headAnimBitmap.getWidth()/numFrames; frameHeight = headAnimBitmap.getHeight(); }
- Now we implement the short but crucial
run
method. It calls each of the key methods of this class one after the other. These three methods are implemented in the following three steps after this step. Type the following code after the code from the preceding step:@Override public void run() { while (playingSnake) { update(); draw(); controlFPS(); } }
- Here is the
update
method. It tracks and chooses the frame number that needs to be displayed. Each time through theupdate
method, we calculate the coordinates of the sprite sheet to be drawn usingframeWidth
,frameHeight
, andframeNumber
. If you are wondering why we subtract1
from each horizontal coordinate, it is because like the screen coordinates, bitmaps start their coordinates at 0, 0:public void update() { //which frame should we draw rectToBeDrawn = new Rect((frameNumber * frameWidth)-1, 0,(frameNumber * frameWidth +frameWidth)-1, frameHeight); //now the next frame frameNumber++; //don't try and draw frames that don't exist if(frameNumber == numFrames){ frameNumber = 0;//back to the first frame } }
- Next is the
draw
method, which does nothing new until the end, when it calculates the place on the screen to draw the bitmap by dividing thescreenHeight
andscreenWidth
variables by 2. These coordinates are then saved indestRect
. BothdestRect
andrectToDraw
are then passed to thedrawBitmap
method, which draws the frame required at the location required. Type this code after the code from the previous step:public void draw() { if (ourHolder.getSurface().isValid()) { canvas = ourHolder.lockCanvas(); //Paint paint = new Paint(); canvas.drawColor(Color.BLACK);//the background paint.setColor(Color.argb(255, 255, 255, 255)); paint.setTextSize(150); canvas.drawText("Snake", 10, 150, paint); paint.setTextSize(25); canvas.drawText(" Hi Score:" + hi, 10, screenHeight-50, paint); //Draw the snake head //make this Rect whatever size and location you like //(startX, startY, endX, endY) Rect destRect = new Rect(screenWidth/2-100, screenHeight/2-100, screenWidth/2+100, screenHeight/2+100); canvas.drawBitmap(headAnimBitmap, rectToBeDrawn, destRect, paint); ourHolder.unlockCanvasAndPost(canvas); } }
- Our trusty old
controlFPS
method ensures that our animation appears at a sensible rate. The only change in this code is that the initialization oftimeTosleep
is changed to create a 500-millisecond pause between each frame. Type the following code after the code from the preceding step:public void controlFPS() { long timeThisFrame = (System.currentTimeMillis() - lastFrameTime); long timeToSleep = 500 - timeThisFrame; if (timeThisFrame > 0) { fps = (int) (1000 / timeThisFrame); } if (timeToSleep > 0) { try { ourThread.sleep(timeToSleep); } catch (InterruptedException e) { } } lastFrameTime = System.currentTimeMillis(); }
- Next are our
pause
andresume
methods, which work with the Android lifecycle methods to start and stop our thread. Type this code after the code from the previous step:public void pause() { playingSnake = false; try { ourThread.join(); } catch (InterruptedException e) { } } public void resume() { playingSnake = true; ourThread = new Thread(this); ourThread.start(); }
- For our
SnakeAnimView
class and ouronTouchEvent
method, which simply starts the game when the screen is touched anywhere, we enter the following code. Obviously, we don't have aGameActivity
yet:@Override public boolean onTouchEvent(MotionEvent motionEvent) { startActivity(i); return true; } }
- Finally, back in the
MainActivity
class, we handle some Android lifecycle methods. We also handle what happens when the player presses the back button:@Override protected void onStop() { super.onStop(); while (true) { snakeAnimView.pause(); break; } finish(); } @Override protected void onResume() { super.onResume(); snakeAnimView.resume(); } @Override protected void onPause() { super.onPause(); snakeAnimView.pause(); } public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { snakeAnimView.pause(); finish(); return true; } return false; }
- Now you must temporarily comment out this line from step 4 to test the animation. The reason for this is that it causes an error until we implement the
GameActivity
class://i = new Intent(this, GameActivity.class);
- Test the app.
- Uncomment the line from step 14 when we have implemented the
GameActivity
class. Here is our completed home screen:
In this exercise, we set up a class that extended SurfaceView
, just like we did for our squash game. We had a run
method, which controlled the thread, as well as an update
method, which calculated the coordinates of the current animation within our sprite sheet. The draw
method simply drew to the screen using the coordinates calculated by the update
method.
As in the squash game, we had an onTouchUpdate
method, but the code this time was very simple. As a touch of any type in any location was all we needed to detect, we added just one line of code to the method.
Implementing the Snake game activity
Not all of this code is new. In fact, we have either used most of it before or discussed it earlier in the chapter. However, I wanted to present every line to you in order and in context with at least a brief explanation, even when we have seen it before. Having said that, I haven't included the long list of imports as we will either be prompted to add them automatically or we can just press Alt + Enter when needed.
This way, we can remind ourselves how the whole thing comes together without any blanks in our understanding. As usual, I will summarize as we proceed through the implementation, and go into a few bits of extra depth at the end:
- Add an activity called
GameActivity
. Select a blank activity when asked. - Make the activity full screen as we have done before.
- As usual, create some sound effects or use mine. Create an
assets
directory in themain
directory in the usual way. Copy and paste the sound files (sample1.ogg
,sample2.ogg
,sample3.ogg
, andsample4.ogg
) into it. - Create individual non-sprite-sheet versions of graphics or use mine. Copy and paste them in the
res/drawable-mdpi
folder. - Here is the
GameActivity
class declaration with the member variables. There is nothing new here until we declare our arrays for our snake (snakeX
andsnakeY
). Also, notice our variables used to control our game grid (blockSize
,numBlocksHigh
, andnumBlocksWide
). Now type this code:public class GameActivity extends Activity { Canvas canvas; SnakeView snakeView; Bitmap headBitmap; Bitmap bodyBitmap; Bitmap tailBitmap; Bitmap appleBitmap; //Sound //initialize sound variables private SoundPool soundPool; int sample1 = -1; int sample2 = -1; int sample3 = -1; int sample4 = -1; //for snake movement int directionOfTravel=0; //0 = up, 1 = right, 2 = down, 3= left int screenWidth; int screenHeight; int topGap; //stats long lastFrameTime; int fps; int score; int hi; //Game objects int [] snakeX; int [] snakeY; int snakeLength; int appleX; int appleY; //The size in pixels of a place on the game board int blockSize; int numBlocksWide; int numBlocksHigh;
- As explained previously, our new, small
onCreate
method has very little to do because much of the work is done in theloadSound
andconfigureDisplay
methods. Type this code after the code from the previous step:@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); loadSound(); configureDisplay(); snakeView = new SnakeView(this); setContentView(snakeView); }
- Here is the class declaration, member variables, and constructor for our
SnakeView
class. We allocate 200int
variables to thesnakeX
andsnakeY
arrays, and call thegetSnake
andgetApple
methods, which will place an apple and our snake on the screen. This is just what we want when the class is constructed:class SnakeView extends SurfaceView implements Runnable { Thread ourThread = null; SurfaceHolder ourHolder; volatile boolean playingSnake; Paint paint; public SnakeView(Context context) { super(context); ourHolder = getHolder(); paint = new Paint(); //Even my 9 year old play tester couldn't //get a snake this long snakeX = new int[200]; snakeY = new int[200]; //our starting snake getSnake(); //get an apple to munch getApple(); }
- Here is how we spawn a snake and an apple in our coordinate system. In the
getSnake
method, we place the snake's head in the approximate center of the screen by initializingsnakeX[0]
andsnakeY[0]
to the number of blocks high and wide divided by 2. We then place a body segment and the tail segment immediately behind. Notice that we don't need to make any special arrangement for the different types of segments. As long as the drawing code knows that the first segment is a head, the last segment is a tail, and everything in between is a body, then that will do. In thegetApple
method, the integer variablesappleX
andappleY
are initialized to random locations within our game grid. This method is called from the constructor, as we saw in the previous step. It will also be called to place a new apple every time our snake manages to eat an apple, as we will see. Type this code after the code from the previous step:public void getSnake(){ snakeLength = 3; //start snake head in the middle of screen snakeX[0] = numBlocksWide / 2; snakeY[0] = numBlocksHigh / 2; //Then the body snakeX[1] = snakeX[0]-1; snakeY[1] = snakeY[0]; //And the tail snakeX[1] = snakeX[1]-1; snakeY[1] = snakeY[0]; } public void getApple(){ Random random = new Random(); appleX = random.nextInt(numBlocksWide-1)+1; appleY = random.nextInt(numBlocksHigh-1)+1; }
- Next comes the
run
method, which controls the flow of the game. Type the following code after the code from the previous step:@Override public void run() { while (playingSnake) { updateGame(); drawGame(); controlFPS(); } }
- Now we will look at
updateGame
, the most complex method of the entire app. Having said that, it is probably slightly less complex than the same method in our squash game. This is because of our coordinate system, which leads to simpler collision detection. Here is the code forupdateGame
. Study it carefully, and we will dissect it line by line at the end:public void updateGame() { //Did the player get the apple if(snakeX[0] == appleX && snakeY[0] == appleY){ //grow the snake snakeLength++; //replace the apple getApple(); //add to the score score = score + snakeLength; soundPool.play(sample1, 1, 1, 0, 0, 1); } //move the body - starting at the back for(int i=snakeLength; i >0 ; i--){ snakeX[i] = snakeX[i-1]; snakeY[i] = snakeY[i-1]; } //Move the head in the appropriate direction switch (directionOfTravel){ case 0://up snakeY[0] --; break; case 1://right snakeX[0] ++; break; case 2://down snakeY[0] ++; break; case 3://left snakeX[0] --; break; } //Have we had an accident boolean dead = false; //with a wall if(snakeX[0] == -1)dead=true; if(snakeX[0] >= numBlocksWide) dead = true; if(snakeY[0] == -1)dead=true; if(snakeY[0] == numBlocksHigh) dead = true; //or eaten ourselves? for (int i = snakeLength-1; i > 0; i--) { if ((i > 4) && (snakeX[0] == snakeX[i]) && (snakeY[0] == snakeY[i])) { dead = true; } } if(dead){ //start again soundPool.play(sample4, 1, 1, 0, 0, 1); score = 0; getSnake(); } }
- We have worked out where our game objects are on the screen, so now we can draw them. This code is easy to understand as we have seen most of it before:
public void drawGame() { if (ourHolder.getSurface().isValid()) { canvas = ourHolder.lockCanvas(); //Paint paint = new Paint(); canvas.drawColor(Color.BLACK);//the background paint.setColor(Color.argb(255, 255, 255, 255)); paint.setTextSize(topGap/2); canvas.drawText("Score:" + score + " Hi:" + hi, 10, topGap-6, paint); //draw a border - 4 lines, top right, bottom , left paint.setStrokeWidth(3);//3 pixel border canvas.drawLine(1,topGap,screenWidth-1,topGap,paint); canvas.drawLine(screenWidth-1,topGap,screenWidth-1,topGap+(numBlocksHigh*blockSize),paint); canvas.drawLine(screenWidth-1,topGap+(numBlocksHigh*blockSize),1,topGap+(numBlocksHigh*blockSize),paint); canvas.drawLine(1,topGap, 1,topGap+(numBlocksHigh*blockSize), paint); //Draw the snake canvas.drawBitmap(headBitmap, snakeX[0]*blockSize, (snakeY[0]*blockSize)+topGap, paint); //Draw the body for(int i = 1; i < snakeLength-1;i++){ canvas.drawBitmap(bodyBitmap, snakeX[i]*blockSize, (snakeY[i]*blockSize)+topGap, paint); } //draw the tail canvas.drawBitmap(tailBitmap, snakeX[snakeLength-1]*blockSize, (snakeY[snakeLength-1]*blockSize)+topGap, paint); //draw the apple canvas.drawBitmap(appleBitmap, appleX*blockSize, (appleY*blockSize)+topGap, paint); ourHolder.unlockCanvasAndPost(canvas); } }
- Here is the
controlFPS
method, unchanged from our squash game'scontrolFPS
method, except that we have a different target frame rate. Type this code after the code from the preceding step:public void controlFPS() { long timeThisFrame = (System.currentTimeMillis() - lastFrameTime); long timeToSleep = 100 - timeThisFrame; if (timeThisFrame > 0) { fps = (int) (1000 / timeThisFrame); } if (timeToSleep > 0) { try { ourThread.sleep(timeToSleep); } catch (InterruptedException e) { } } lastFrameTime = System.currentTimeMillis(); }
- Here are our unchanged
pause
andresume
methods. Type the following code after the code from the previous step:public void pause() { playingSnake = false; try { ourThread.join(); } catch (InterruptedException e) { } } public void resume() { playingSnake = true; ourThread = new Thread(this); ourThread.start(); }
- Then we have the
onTouchEvent
method, similar to that of our squash game. There are no new concepts here, but the way it works in this game is as follows. We switch on theACTION_UP
event. This is broadly the same as detecting a click. We then check whether the press was on the left or the right. If it was on the right, we incrementdirectionOfTravel
. If it was on the left, we decrementdirectionOfTravel
. If you looked carefully at theupdateGame
method, you would have seen thatdirectionOfTravel
indicates the direction in which to move the snake. Remember, the snake never stops. This is why we did it differently from our squash game. Type this code after the code from the previous step:@Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_UP: if (motionEvent.getX() >= screenWidth / 2) { //turn right directionOfTravel ++; //no such direction if(directionOfTravel == 4) //loop back to 0(up) directionOfTravel = 0; } else { //turn left directionOfTravel--; if(directionOfTravel == -1) {//no such direction //loop back to 0(up) directionOfTravel = 3; } } } return true; }
- Back in the
GameActivity
class, we now handle the Android lifecycle methods and the "back" button functionality. Type this code after the code from the preceding step:@Override protected void onStop() { super.onStop(); while (true) { snakeView.pause(); break; } finish(); } @Override protected void onResume() { super.onResume(); snakeView.resume(); } @Override protected void onPause() { super.onPause(); snakeView.pause(); } public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { snakeView.pause(); Intent i = new Intent(this, MainActivity.class); startActivity(i); finish(); return true; } return false; }
- Here is our
loadSound
method, which simply tidies up theonCreate
method by moving all of the sound initialization to here. Type this code after the code from the previous step:public void loadSound(){ soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0); try { //Create objects of the 2 required classes AssetManager assetManager = getAssets(); AssetFileDescriptor descriptor; //create our three fx in memory ready for use descriptor = assetManager.openFd("sample1.ogg"); sample1 = soundPool.load(descriptor, 0); descriptor = assetManager.openFd("sample2.ogg"); sample2 = soundPool.load(descriptor, 0); descriptor = assetManager.openFd("sample3.ogg"); sample3 = soundPool.load(descriptor, 0); descriptor = assetManager.openFd("sample4.ogg"); sample4 = soundPool.load(descriptor, 0); } catch (IOException e) { //Print an error message to the console Log.e("error", "failed to load sound files); } }
- Then we have the
configureDisplay
method, which is called fromonCreate
and does the entire setup of bitmaps and screen size calculations. We will look at this in more detail later. Type the following code after the code from the previous step:public void configureDisplay(){ //find out the width and height of the screen Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); screenWidth = size.x; screenHeight = size.y; topGap = screenHeight/14; //Determine the size of each block/place on the game board blockSize = screenWidth/40; //Determine how many game blocks will fit into the //height and width //Leave one block for the score at the top numBlocksWide = 40; numBlocksHigh = ((screenHeight - topGap ))/blockSize; //Load and scale bitmaps headBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head); bodyBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.body); tailBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.tail); appleBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.apple); //scale the bitmaps to match the block size headBitmap = Bitmap.createScaledBitmap(headBitmap, blockSize, blockSize, false); bodyBitmap = Bitmap.createScaledBitmap(bodyBitmap, blockSize, blockSize, false); tailBitmap = Bitmap.createScaledBitmap(tailBitmap, blockSize, blockSize, false); appleBitmap = Bitmap.createScaledBitmap(appleBitmap, blockSize, blockSize, false); }
- Now run the app. The game is much more playable on an actual device than it is on the emulator.
We covered the code as we progressed, but as usual, here is a piece-by-piece dissection of a few of the more complicated methods, starting with the updateGame
method.
First, we check whether the player has eaten an apple. More specifically, is the snake's head in the same grid location as the apple? The if
statement checks whether this has occurred, and then does the following:
- Increases the length of the snake
- Puts another apple on the screen by calling
getApple
- Adds a value to the player's score, relative to the length of the snake, making each apple worth more than the previous one
- Plays a beep
Here is the code for the actions that we have just described:
public void updateGame() { //Did the player get the apple if(snakeX[0] == appleX && snakeY[0] == appleY){ //grow the snake snakeLength++; //replace the apple getApple(); //add to the score score = score + snakeLength; soundPool.play(sample1, 1, 1, 0, 0, 1); }
Now we simply move each segment of the snake, starting from the back, to the position of the segment in front of it. We do this with a for
loop:
//move the body - starting at the back for(int i = snakeLength; i >0 ; i--){ snakeX[i] = snakeX[i-1]; snakeY[i] = snakeY[i-1]; }
Of course, we better move the head too! We move the head last because the leading section of the body would move to the wrong place if we move the head earlier. As long as the entire move is made before any drawing is done, all will be well. Our run
method ensures that this is always the case. Here is the code to move the head in the direction determined by directionOfTravel
. As we saw, directionOfTravel
is manipulated by the player in the onTouchEvent
method:
//Move the head in the appropriate direction switch (directionOfTravel){ case 0://up snakeY[0] --; break; case 1://right snakeX[0] ++; break; case 2://down snakeY[0] ++; break; case 3://left snakeX[0] --; break; }
Next, we check for a collision with a wall. We saw this code when we looked at collision detection earlier. Here is the complete solution, starting with the left wall, then right, then top, and then bottom:
//Have we had an accident boolean dead = false; //with a wall if(snakeX[0] == -1)dead=true; if(snakeX[0] >= numBlocksWide)dead=true; if(snakeY[0] == -1)dead=true; if(snakeY[0] == numBlocksHigh)dead=true;
Then we check whether the snake has collided with itself. Initially, this seemed awkward, but as we previously saw, we just loop through our snake array to check whether any of the segments are in the same place as the head, in both x and y coordinates:
//or eaten ourselves? for (int i = snakeLength-1; i > 0; i--) { if ((i > 4) && (snakeX[0] == snakeX[i]) && (snakeY[0] == snakeY[i])) { dead = true; } }
If any part of our collision detection code sets dead
to true
, we simply play a sound, set the score
to 0
, and get a new baby snake:
if(dead){ //start again soundPool.play(sample4, 1, 1, 0, 0, 1); score = 0; getSnake(); } }
Now we take a closer look at the drawGame
method. First, we get ready to draw by clearing the screen:
public void drawGame() { if (ourHolder.getSurface().isValid()) { canvas = ourHolder.lockCanvas(); //Paint paint = new Paint(); canvas.drawColor(Color.BLACK);//the background paint.setColor(Color.argb(255, 255, 255, 255)); paint.setTextSize(topGap/2);
Now we draw the text for the player's score, just above topGap
that we define in configureDisplay
:
canvas.drawText("Score:" + score + " Hi:" + hi, 10, topGap-6, paint);
Now, using drawLine
, we draw a visible border around our game grid:
//draw a border - 4 lines, top right, bottom, left paint.setStrokeWidth(3);//4 pixel border canvas.drawLine(1,topGap,screenWidth-1,topGap,paint); canvas.drawLine(screenWidth-1,topGap,screenWidth-1,topGap+(numBlocksHigh*blockSize),paint); canvas.drawLine(screenWidth-1,topGap+(numBlocksHigh*blockSize),1,topGap+(numBlocksHigh*blockSize),paint); canvas.drawLine(1,topGap, 1,topGap+(numBlocksHigh*blockSize), paint);
Next, we draw the snake's head:
//Draw the snake canvas.drawBitmap(headBitmap, snakeX[0]*blockSize, (snakeY[0]*blockSize)+topGap, paint);
The snake's head will be followed by all the body segments. Look at the condition of the for
loop. This starts at 1
, which means it is not redrawing the head position, and ends at snakeLength - 1
, which means it is not drawing the tail segment. Here is the code used to draw the body section:
//Draw the body for(int i = 1; i < snakeLength-1; i++){ canvas.drawBitmap(bodyBitmap, snakeX[i]*blockSize, (snakeY[i]*blockSize)+topGap, paint); }
Here, we draw the tail of the snake:
//draw the tail canvas.drawBitmap(tailBitmap, snakeX[snakeLength- 1]*blockSize, (snakeY[snakeLength-1]*blockSize)+topGap, paint);
Finally, we draw the apple as follows:
//draw the apple canvas.drawBitmap(appleBitmap, appleX*blockSize, (appleY*blockSize)+topGap, paint); ourHolder.unlockCanvasAndPost(canvas); } }
Next, we will go through the configureDisplay
method.
First, we get the screen resolution and store the results in screenWidth
and screenHeight
as normal:
public void configureDisplay(){ //find out the width and height of the screen Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); screenWidth = size.x; screenHeight = size.y;
Here, we define a gap called topGap
. It will be a space at the top of the screen and will not be a part of the game area. This gap is used for the score. We saw topGap
used fairly extensively in the drawGame
method. After this, we calculate the width and height of the remaining area in blocks:
topGap = screenHeight/14; //Determine the size of each block/place on the game board blockSize = screenWidth/40; //Determine how many game blocks will fit into the height and width //Leave one block for the score at the top numBlocksWide = 40; numBlocksHigh = (screenHeight - topGap )/blockSize;
In the following part of the code, we load all our image files into Bitmap
objects:
//Load and scale bitmaps headBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.head); bodyBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.body); tailBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.tail); appleBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.apple);
Finally, we scale each bitmap to be the same width and height as blockSize
:
//scale the bitmaps to match the block size headBitmap = Bitmap.createScaledBitmap(headBitmap, blockSize, blockSize, false); bodyBitmap = Bitmap.createScaledBitmap(bodyBitmap, blockSize, blockSize, false); tailBitmap = Bitmap.createScaledBitmap(tailBitmap, blockSize, blockSize, false); appleBitmap = Bitmap.createScaledBitmap(appleBitmap, blockSize, blockSize, false); }
Now we can take a quick look at a few different ways we can improve the game.
Enhancing the game
Here is a series of questions and answers to lead us to an improved version of our Snake game. It doesn't matter if you can't answer some (or even all) of the questions. Just take a look at the questions and answers, after which you can take a look at the new game and the code.
Self-test questions
Q1) What can be used to provide a visual improvement for our game screen? Can we use a nice light green, grassy background instead of just black?
Q2) How about some nice flowers?
Q3) If you're feeling brave, make the flowers sway. Think about what we have learned about sprite sheets. The theory is exactly the same as that of the animated snake head. We just need a few lines of code to control the frame rate separately from the game frame rate.
Q4) We could set up another counter and use our snake head animation in GameActivity
, but it wouldn't be that useful because the subtle tongue movements would be barely visible at the smaller size. But could we swish the tail segment?
Q5) Here is a slightly trickier enhancement. You can't help notice that when the snake sprites are headed in three out of the four possible directions, they don't look right. Can you fix this?
Summary
This is the end of yet another successful game project. You now know how to create and animate sprite sheets to add more realism to our games. Now we have an enhanced Snake game.
In the next chapter, we will see how simple it is to add leaderboards and achievements. This will make our game social and compelling by letting the player see the high scores and achievements of their friends and compare them with their own.
- OpenStack Cloud Computing Cookbook(Third Edition)
- iOS Game Programming Cookbook
- Apache ZooKeeper Essentials
- Spring Boot開發(fā)與測試實(shí)戰(zhàn)
- Android開發(fā)精要
- 單片機(jī)C語言程序設(shè)計(jì)實(shí)訓(xùn)100例:基于STC8051+Proteus仿真與實(shí)戰(zhàn)
- Android 9 Development Cookbook(Third Edition)
- 人臉識(shí)別原理及算法:動(dòng)態(tài)人臉識(shí)別系統(tǒng)研究
- INSTANT Sinatra Starter
- 新一代SDN:VMware NSX 網(wǎng)絡(luò)原理與實(shí)踐
- Flask開發(fā)Web搜索引擎入門與實(shí)戰(zhàn)
- Socket.IO Cookbook
- Processing開發(fā)實(shí)戰(zhàn)
- Docker on Windows
- Build Your Own PaaS with Docker