Simple Coding a Snake Game for Android
As the title suggests, during this project we are going to build a straightforward snake vogue game. This form of game has been around since the middle 1970’s beneath different names like Worm and Surround.I contend this factor for several hours on the traditional weapon system M5 and also the “green screen” Sharp MZ80 back within the 1980’s. Snake finally reached international acclaim within the 2000s but, once it absolutely was equipped as customary with an entire generation of Nokia mobile phones.
This game can use a special engine to the opposite games on this web site because it can build a pre-determined range of “moves” every second, instead of enjoying as several frames of animation as doable and so temporal arrangement every frame for optimum smoothness. the rationale for this can be we are able to recreate a authentic blocky/jumpy animation.
Take a look at this image to see the game at the start.
One dot for the snake and one dot for Bob waiting to be eaten. Before anyone complains that snakes don’t eat Bobs, they don’t eat apples either. As the game continues and many Bobs are eaten the snake grows in length making it more likely that the player will trap or eat himself.
Let’s start coding.
Lets Coding the Snake Activity for now!
As usual, we are going to begin with AN Activity which can management a thread in an exceedingly category that controls the sport and handles input from the player. If you would like a additional in-depth discussion of the interaction between the Activity and therefore the main category then take a glance at the flight tutorial.
Create a replacement project in mechanical man Studio, use the Empty Activity guide, and decision it Snake. Leave the remainder of the settings at their defaults. decision the Activity SnakeActivity and amend its code to be identical as this.
1 | import android.app.Activity; import android.graphics.Point; import android.os.Bundle; import android.view.Display; public class SnakeActivity extends Activity { // Declare an instance of SnakeEngine // We will code this soon SnakeEngine snakeEngine; } |
Here we declare an instance of
1 | SnakeEngine |
called
1 | snakeEngine |
which doesn’t exist yet but it will soon. Now code the
1 | onCreate |
method of the
1 | SnakeActivity |
class to initialize the
1 | SnakeEngine |
object. Obviously there will be errors in our code but if we code SnakeActivity in full we won’t need to keep coming back to it. Add the following code and we will discuss it.
1 | import android.app.Activity; import android.graphics.Point; import android.os.Bundle; import android.view.Display; public class SnakeActivity extends Activity { // Declare an instance of SnakeEngine SnakeEngine snakeEngine; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get the pixel dimensions of the screen Display display = getWindowManager().getDefaultDisplay(); // Initialize the result into a Point object Point size = new Point(); display.getSize(size); // Create a new instance of the SnakeEngine class snakeEngine = new SnakeEngine(this, size); // Make snakeEngine the view of the Activity setContentView(snakeEngine); } } |
The
1 | onCreate |
method uses the
1 | Display |
class and an object of type
1 | Point |
to get the resolution of the device the sport is running on. Our SnakeEngine category can want a respect to the Activity and also the resolution thus we tend to pass them in to the SnakeEngine creator. The final thing we tend to do is use snakeEngine to be the read of the SnakeActivity.
1 | onPause |
and
1 | onResume |
methods then these methods call the relevant methods inside
1 | SnakeEngine |
to start and stop the thread which handles the entire game.
1 | // Start the thread in snakeEngine @Override protected void onResume() { super.onResume(); snakeEngine.resume(); } // Stop the thread in snakeEngine @Override protected void onPause() { super.onPause(); snakeEngine.pause(); } |
Making the game fullscreen and landscape
We want to use every pixel that the device has to offer so we will make changes to the app’s AndroidManifest.xml configuration file.
- In the project explorer pane in Android Studio double click on the manifests folder, this will open up the AndroidManifest.xml file in the code editor.
- In the AndroidManifest.xml file, locate the following line of code: android:name=”.SnakeActivity”>
- Place the cursor before the closing > shown above. Tap the enter key a couple of times to move the > a couple of lines below the rest of the line shown above.
- Immediately below ParallaxActivity but BEFORE the newly positioned > type or copy and paste these two lines to make the game run full screen and lock it in the landscape orientation.
1 | android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="landscape" |
Add the sound to the project
Download the sounds by right-clicking on the files listed below. Add them to the Snake project by using your operating system’s file browser go to the appsrcmain folder of the project and create a new folder called assets. Add your sound files to this folder. Here are my sound effects. Right-click and select Save link as… to download them.
snake_crash
eat_bob
We can now get rid of the errors by moving on to the SnakeEngine class.
Coding SnakeGame on android
Add a new class called SnakeEngine and amend the code as shown next so we have all the required imports.
1 | import android.content.Context; import android.content.res.AssetFileDescriptor; import android.graphics.Point; import android.media.AudioManager; import android.media.SoundPool; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import java.io.IOException; import java.util.Random; import android.content.res.AssetManager; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; class SnakeEngine extends SurfaceView implements Runnable { } |
When we extend
1 | SurfaceView |
so that the call to
1 | setContentView |
in the
1 | SnakeActivity |
class works and we implement the
1 | Runnable |
interface so we can later pass this class to the
1 | Thread |
constructor to create a
1 | Thread |
instance.
1 | Runnable |
has one method that we must implement and we will overide
1 | run |
soon.
The SnakeGame variables
Add all the member variables after the class declaration then they will be ready for use as we proceed through the rest of the code.
1 | // Our game thread for the main game loop private Thread thread = null; // To hold a reference to the Activity private Context context; // for plaing sound effects private SoundPool soundPool; private int eat_bob = -1; private int snake_crash = -1; // For tracking movement Heading public enum Heading {UP, RIGHT, DOWN, LEFT} // Start by heading to the right private Heading heading = Heading.RIGHT; // To hold the screen size in pixels private int screenX; private int screenY; // How long is the snake private int snakeLength; // Where is Bob hiding? private int bobX; private int bobY; // The size in pixels of a snake segment private int blockSize; // The size in segments of the playable area private final int NUM_BLOCKS_WIDE = 40; private int numBlocksHigh; // Control pausing between updates private long nextFrameTime; // Update the game 10 times per second private final long FPS = 10; // There are 1000 milliseconds in a second private final long MILLIS_PER_SECOND = 1000; // We will draw the frame much more often // How many points does the player have private int score; // The location in the grid of all the segments private int[] snakeXs; private int[] snakeYs; // Everything we need for drawing // Is the game currently playing? private volatile boolean isPlaying; // A canvas for our paint private Canvas canvas; // Required to use canvas private SurfaceHolder surfaceHolder; // Some paint for our canvas private Paint paint; |
We can now code the constructor.
Coding the SnakeGame constructor
Add this code next, be sure to add it inside the closing curly brace of the
1 | SnakeEngine |
class.
1 | public SnakeEngine(Context context, Point size) { super(context); context = context; screenX = size.x; screenY = size.y; // Work out how many pixels each block is blockSize = screenX / NUM_BLOCKS_WIDE; // How many blocks of the same size will fit into the height numBlocksHigh = screenY / blockSize; // Set the sound up soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0); try { // Create objects of the 2 required classes // Use m_Context because this is a reference to the Activity AssetManager assetManager = context.getAssets(); AssetFileDescriptor descriptor; // Prepare the two sounds in memory descriptor = assetManager.openFd("get_mouse_sound.ogg"); eat_bob = soundPool.load(descriptor, 0); descriptor = assetManager.openFd("death_sound.ogg"); snake_crash = soundPool.load(descriptor, 0); } catch (IOException e) { // Error } // Initialize the drawing objects surfaceHolder = getHolder(); paint = new Paint(); // If you score 200 you are rewarded with a crash achievement! snakeXs = new int[200]; snakeYs = new int[200]; // Start the game newGame(); } |
First, we initialize,
1 | context |
,
1 | screenX |
and
1 | screenY |
with the values passed in from
1 | SnakeActivity |
. Next, we divide the number of pixels by the final int
1 | NUM_BLOCKS_WIDE |
in order to determine the appropriate number of pixels in the width of
1 | blockSize |
. Now we can use this to work out, based on the number of vertical pixels, how many blocks high the playable area will be.
Next, the sound files are loaded and associated with an appropriately named identifier. They are now ready to play at will with
1 | soundPool.playSound |
.
What follows is we initialize
1 | surfaceHolder |
and
1 | paint |
.
After this, we initialized the two
1 | int |
arrays.
1 | snakeXs |
will hold the horizontal coordinate of each segment of the snake and
1 | snakeYs |
will hold each vertical coordinate.
The last part of the code we call the
1 | newGame |
method which unsurprisingly starts the game. We will code
1 | newGame |
shortly.
Making thread run the game loop and keep it up
All the within the run technique, as well as technique calls from the run technique, works during a separate thread to the robot UI. this may permit our game to run swimmingly at constant time as listening for player input. Add the run technique moreover as pause and resume and so we’ll remark them.
1 | @Override public void run() { while (isPlaying) { // Update 10 times a second if(updateRequired()) { update(); draw(); } } } public void pause() { isPlaying = false; try { thread.join(); } catch (InterruptedException e) { // Error } } public void resume() { isPlaying = true; thread = new Thread(this); thread.start(); } |
The
1 | pause |
and
1 | resume |
methods are called by
1 | SnakeActivity |
when Android or the player causes the app to call
1 | onPause |
or
1 | onResume |
. The
1 | resume |
method creates a new instance of
1 | Thread |
when required and
1 | pause |
stops the it when required. Now our instance of
1 | Thread |
will play nicely with Android.
Everything in, and called by the run method, will now happen in a separate thread.
The
1 | run |
method calls
1 | update |
and then
1 | draw |
. The whole thing is wrapped in a
1 | while |
loop that repeats continuously if
1 | isPlaying |
is set to
1 | true |
and the thread is running.
These calls are also contained within
1 | if(updateRequired()) |
. Only if this is
1 | true |
are the
1 | update |
and
1 | draw |
methods called. The
1 | updateRequired |
method can, therefore, control the frame rate of the game ensureing the blocky/authentic motion.
Some important methods
As we saw, the
1 | newGame |
method is called by the constructor it is also called when the snake crashes and a new game is required. Add the newGame method.
1 | public void newGame() { // Start with a single snake segment snakeLength = 1; snakeXs[0] = NUM_BLOCKS_WIDE / 2; snakeYs[0] = numBlocksHigh / 2; // Get Bob ready for dinner spawnBob(); // Reset the score score = 0; // Setup nextFrameTime so an update is triggered nextFrameTime = System.currentTimeMillis(); } |
In the
1 | newGame method |
, the snake is prepared. The length is set to just one block then the head of the snake is set to the center of the screen. The first position of each of the arrays holds the head. It is only the head that we will use when we code the collision detection. Next, Bob is prepared for a terrible demise by calling
1 | spawnBob |
and
1 | score |
is initialized to
1 |
.
The final bit of code in the
1 | newGame |
method sets
1 | nextFrameTime |
to whatever the current time is. This will cause the
1 | update |
and
1 | draw |
methods run.
Re-spawn and eating Bobby
The spawnBob method uses two random int values within the ranges of zero and NUM_BLOCKS_WIDE, zero and numBlocksHigh, then initializes the horizontal and vertical location of the mouse.
1 | public void spawnBob() { Random random = new Random(); bobX = random.nextInt(NUM_BLOCKS_WIDE - 1) + 1; bobY = random.nextInt(numBlocksHigh - 1) + 1; } |
Optimization tip: Instantiating a new instance of
1 | Random |
is slow and could be done in the constructor then just reused each time
1 | spawnBob |
is called. In this context, however it will not affect the smooth running of the game.
The
1 | eatBob |
method is simple too.
The snake’s length is increased by one block, a new mouse is spawned, 1 is added to the score and a sound effect is played.
Here is the code for the
1 | eatBob |
method to add after the
1 | spawnBob |
method.
1 | private void eatBob(){ // Got him! // Increase the size of the snake snakeLength++; //replace Bob // This reminds me of Edge of Tomorrow. Oneday Bob will be ready! spawnBob(); //add to the score score = score + 1; soundPool.play(eat_bob, 1, 1, 0, 0, 1); } |
The
1 | moveSnake |
method is quite long but doesn’t involve anything too tricky. Add the code and then we can go through it.
1 | private void moveSnake(){ // Move the body for (int i = snakeLength; i > 0; i--) { // Start at the back and move it // to the position of the segment in front of it snakeXs[i] = snakeXs[i — 1]; snakeYs[i] = snakeYs[i — 1]; // Exclude the head because // the head has nothing in front of it } // Move the head in the appropriate heading switch (heading) { case UP: snakeYs[0]--; break; case RIGHT: snakeXs[0]++; break; case DOWN: snakeYs[0]++; break; case LEFT: snakeXs[0]--; break; } } |
The
1 | for |
loop starts at the last block of the snake in
1 | snakeXs |
and
1 | snakeYs |
and advances it into the location previously occupied by the block ahead of it. When the
1 | for |
loop is complete the last position is in the place the block ahead used to be in and the block that was just behind the head is where the head used to be.
Therefore, as long as we handle the head properly all the other blocks will be correctly positioned too.
To move the head we
1 | switch |
based on the current value of heading and add or subtract 1 from either the heads vertical or horizontal position.
In the
1 | detectDeath |
method, we do collision detection. Notice in the code that follows we check for two things. Has the snake’s head bumped into the edge of the screen and has the snake’s head bumped into a block of the snake’s body?
1 | private boolean detectDeath(){ // Has the snake died? boolean dead = false; // Hit the screen edge if (snakeXs[0] == -1) dead = true; if (snakeXs[0] >= NUM_BLOCKS_WIDE) dead = true; if (snakeYs[0] == -1) dead = true; if (snakeYs[0] == numBlocksHigh) dead = true; // Eaten itself? for (int i = snakeLength — 1; i > 0; i--) { if ((i > 4) && (snakeXs[0] == snakeXs[i]) && (snakeYs[0] == snakeYs[i])) { dead = true; } } return dead; } |
If either of the collision possibilities happens then
1 | detectDeath |
returns
1 | true |
to the
1 | update |
method which takes further action.
Coding the online self-update methods
This method does three things:
- It checks if the head has touched/eaten a mouse. If it has then the
1eatBob
method handles things. - It calls the
1moveSnake
method which was coded previously. - It calls the
1detectDeath
method and if it returns
1true
a sound is played and the game begins again.
All this happens ten times per second because of the way
1 | updateRequired |
will work. We will code
1 | updateRequired |
in a minute. Add the code for the update method.
1 | public void update() { // Did the head of the snake eat Bob? if (snakeXs[0] == bobX && snakeYs[0] == bobY) { eatBob(); } moveSnake(); if (detectDeath()) { //start again soundPool.play(snake_crash, 1, 1, 0, 0, 1); newGame(); } } |
Lets Drawing the SneakGame
Add all the code for the
1 | draw |
method and then we will go through it.
1 | public void draw() { // Get a lock on the canvas if (surfaceHolder.getSurface().isValid()) { canvas = surfaceHolder.lockCanvas(); // Fill the screen with Game Code School blue canvas.drawColor(Color.argb(255, 26, 128, 182)); // Set the color of the paint to draw the snake white paint.setColor(Color.argb(255, 255, 255, 255)); // Scale the HUD text paint.setTextSize(90); canvas.drawText("Score:" + score, 10, 70, paint); // Draw the snake one block at a time for (int i = 0; i < snakeLength; i++) { canvas.drawRect(snakeXs[i] * blockSize, (snakeYs[i] * blockSize), (snakeXs[i] * blockSize) + blockSize, (snakeYs[i] * blockSize) + blockSize, paint); } // Set the color of the paint to draw Bob red paint.setColor(Color.argb(255, 255, 0, 0)); // Draw Bob canvas.drawRect(bobX * blockSize, (bobY * blockSize), (bobX * blockSize) + blockSize, (bobY * blockSize) + blockSize, paint); // Unlock the canvas and reveal the graphics for this frame surfaceHolder.unlockCanvasAndPost(canvas); } } |
First, we lock the surface which is required by Android. If this works, we clear the screen with
1 | drawColor |
and then change the color of all future objects we will draw by calling
1 | setColor |
. We do this once for the snake and once for Bob. Now we draw the text for the score.
We use a
1 | for |
loop to draw a block/square to represent each block of the snake. The code positions the blocks to screen coordinates by using their grid positions(contained in the array) multiplied by
1 | blockSize |
which was determined in the constructor based on screen resolution.
Now we can draw single block to represent Bob.
Coding updateRequired
We are almost done!
The
1 | updateRequired |
method will let us know if the
1 | nextFrameTime |
variable has been exceeded by the actual current time. If it has then a new time is retrieved and put back in
1 | nextFrameTime |
. The method then returns
1 | true |
allowing draw and update to execute. If not,
1 | false |
is returned and the next frame is delayed until it is time.
You can now add the updateRequired method.
1 | public boolean updateRequired() { // Are we due to update the frame if(nextFrameTime <= System.currentTimeMillis()){ // Tenth of a second has passed // Setup when the next update will be triggered nextFrameTime =System.currentTimeMillis() + MILLIS_PER_SECOND / FPS; // Return true so that the update and draw // functions are executed return true; } return false; } |
Handling screen touches (player input)
The final code handles the player removing their finger. Holding won’t work. The
1 | onTouchEvent |
method uses
1 | motionEvent.getAction |
to detect MotionEvent.ACTION_UP. This notifies us the player’s finger has left the screen. We then use motionEvent.getX() to determine if that action was on the left or the right of the screen.
If they faucet on the left facet of the screen then the snake moves to ensuing direction within the enumeration going anti-clockwise if they faucet on the correct then it’s dextral.
It’s meant to be awkward, it’s authentic to the initial. Add this code to handle touches on the screen.
1 | @Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_UP: if (motionEvent.getX() >= screenX / 2) { switch(heading){ case UP: heading = Heading.RIGHT; break; case RIGHT: heading = Heading.DOWN; break; case DOWN: heading = Heading.LEFT; break; case LEFT: heading = Heading.UP; break; } } else { switch(heading){ case UP: heading = Heading.LEFT; break; case LEFT: heading = Heading.DOWN; break; case DOWN: heading = Heading.RIGHT; break; case RIGHT: heading = Heading.UP; break; } } } return true; } |
You can now play the game!