Let's get started with the most significant class of this project- SnakeGame
. This will be the game engine for the Snake game.
In the SnakeGame
class that you created previously, add the following import statements along with all the member variables shown next. Study the names and the types of the variables as you add them because they will give a good insight into what we will be coding in this class.
import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.SoundPool; import android.os.Build; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import java.io.IOException; class SnakeGame extends SurfaceView implements Runnable{ // Objects for the game loop/thread private Thread mThread = null; // Control pausing between updates private long mNextFrameTime; // Is the game currently playing and or paused? private volatile boolean mPlaying = false; private volatile boolean mPaused = true; // for playing sound effects private SoundPool mSP; private int mEat_ID = -1; private int mCrashID = -1; // The size in segments of the playable area private final int NUM_BLOCKS_WIDE = 40; private int mNumBlocksHigh; // How many points does the player have private int mScore; // Objects for drawing private Canvas mCanvas; private SurfaceHolder mSurfaceHolder; private Paint mPaint; // A snake ssss private Snake mSnake; // And an apple private Apple mApple; }
Let's run through those variables. Many of them will be familiar. We have mThread
which is our Thread
object, but we also have a new long
variable called mNextFrameTime
. We will use this variable to keep track of when we want to call the update
method. This is a little different to previous projects because previously we just looped around update
and draw
as quickly as we could and depending on how long the frame took updated the game objects accordingly.
What we will do in this game is only call update
at specific intervals to make the snake move one block at a time rather than smoothly glide like all the moving game objects we have created up until now. How this works will become clear soon.
We have two boolean
variables mPlaying
and mPaused
which will be used to control the thread and when we call the update
method, so we can start and stop the gameplay.
Next, we have a SoundPool
and a couple of int
variables for the related sound effects.
Following on we have a final int
(which can't be changed during execution) called NUM_BLOCKS_WIDE
. This variable has been assigned the value 40
. We will use this variable in conjunction with others (most notably the screen resolution) to map out the grid onto which we will draw the game objects. Notice that after NUM_BLOCKS_WIDE
there is mNumBlocksHigh
which will be assigned a value dynamically in the constructor.
The member mScore
is an int
that will keep track of the player's current score.
The next three variables mCanvas
, mSurfaceHolder
and mPaint
are for the exact same use as they were in previous projects and are the classes of the Android API that enable us to do our drawing. What is different, as mentioned previously is that we will be passing references of these to the classes representing the game objects, so they can draw themselves.
Finally, we declare an instance of a Snake
called mSnake
and an Apple called mApple
. Clearly, we haven't coded these classes yet, but we did create empty classes to avoid this code showing an error at this stage.
As usual, we will use the constructor method to set up the game engine. Much of the code that follows will be familiar to you, like the fact that the signature allows for a Context
object and the screen resolution to be passed in. Also, familiar will be the way that we setup the SoundPool
and load all the sound effects. Furthermore, we will initialize our Paint
and SurfaceHolder
methods just as we have done before. There is some new code, however at the start of the constructor method. Be sure to read the comments and examine the code as you add it.
Add the constructor to the SnakeGame
class and we will then examine the two lines of new code.
// This is the constructor method that gets called // from SnakeActivity public SnakeGame(Context context, Point size) { super(context); // Work out how many pixels each block is intblockSize = size.x / NUM_BLOCKS_WIDE; // How many blocks of the same size will fit into the height mNumBlocksHigh = size.y / blockSize; // Initialize the SoundPool if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.LOLLIPOP) { AudioAttributesaudioAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes .CONTENT_TYPE_SONIFICATION) .build(); mSP = new SoundPool.Builder() .setMaxStreams(5) .setAudioAttributes(audioAttributes) .build(); } else { mSP = new SoundPool(5, AudioManager.STREAM_MUSIC, 0); } try { AssetManager assetManager = context.getAssets(); AssetFileDescriptor descriptor; // Prepare the sounds in memory descriptor = assetManager.openFd("get_apple.ogg"); mEat_ID = mSP.load(descriptor, 0); descriptor = assetManager.openFd("snake_death.ogg"); mCrashID = mSP.load(descriptor, 0); } catch (IOException e) { // Error } // Initialize the drawing objects mSurfaceHolder = getHolder(); mPaint = new Paint(); // Call the constructors of our two game objects }
Here are those two new lines again for your convenience:
// Work out how many pixels each block is intblockSize = size.x / NUM_BLOCKS_WIDE; // How many blocks of the same size will fit into the height mNumBlocksHigh = size.y / blockSize;
A new, local (on the Stack) int
called blockSize
is declared and then initialized by dividing the width of the screen in pixels by NUM_BLOCKS_WIDE
. The variable blockSize
now represents the number of pixels that one position (block) of the grid we use to draw the game. For example, a snake segment and an apple will be scaled using this value.
Now we have the size of a block we can initialize mNumBlocksHigh
by dividing the number of pixels vertically by the variable we just initialized. It would have been possible to initialize mNumBlocksHigh
without using blockSize
in just a single line of code but doing it as we did makes our intentions and the concept of a grid made of blocks much clearer.
This method only has two lines of code in it for now, but we will add more as the project proceeds. Add the newGame
method to the SnakeGame
class.
// Called to start a new game public void newGame() { // reset the snake // Get the apple ready for dinner // Reset the mScore mScore = 0; // Setup mNextFrameTime so an update can triggered mNextFrameTime = System.currentTimeMillis(); }
As the name suggests this method will be called each time the player starts a new game. For now, all that happens is the score is set to zero and the mNextFrameTime
variable is set to the current time. Next, we will see how we can use mNextFrameTime
to create the blocky/juddering updates that this game needs to be authentic looking. In fact, by setting mNextFrameTime
to the current time we are setting things up for an update to be triggered at once.
This method has some differences to the way we have handled the run
method in previous projects. Add the method and examine the code and then we will discuss it.
// Handles the game loop @Override public void run() { while (mPlaying) { if(!mPaused) { // Update 10 times a second if (updateRequired()) { update(); } } draw(); } }
Inside the run
method which is called by Android repeatedly while the thread is running, we first check if mPlaying
is true
. If it is we next check to make sure the game is not paused. Finally, nested inside both these checks we call if(updateRequired())
. If this method (that we code next) returns true only then does the update
method get called.
Note the position of the call to the draw
method. This position means it will be constantly called all the time that mPlaying
is true.
Also, in the newGame
method, you can see some comments that hint at some more code we will be adding later in the project.
The updateRequired
method is what makes the actual update
method execute only ten times per second and creates the blocky movement of the snake. Add the updateRequired
method.
// Check to see if it is time for an update public boolean updateRequired() { // Run at 10 frames per second final long TARGET_FPS = 10; // There are 1000 milliseconds in a second final long MILLIS_PER_SECOND = 1000; // Are we due to update the frame if(mNextFrameTime<= System.currentTimeMillis()){ // Tenth of a second has passed // Setup when the next update will be triggered mNextFrameTime =System.currentTimeMillis() + MILLIS_PER_SECOND / TARGET_FPS; // Return true so that the update and draw // methods are executed return true; } return false; }
The updateRequired
method declares a new final
variable called TARGET_FPS
and initializes it to 10
. This is the frame rate we are aiming for. The next line of code is a variable created for the sake of clarity. MILLIS_PER_SECOND
is initialized to 1000
because there are one thousand milliseconds in a second.
The if
statement that follows is where the method gets its work done. It checks if mNextFrameTime
is less than or equal to the current time. If it is the code inside the if
statement executes. Inside the if
statement mNextFrameTime
is updated by adding MILLIS_PER_SECOND
divided by TARGET_FPS
onto the current time.
Next, mNextFrameTime
is set to one-tenth of a second ahead of the current time ready to trigger the next update. Finally, inside the if
statement return true
will trigger the code in the run
method to call the update
method.
Note that had the if
statement not executed then mNextFrameTime
would have been left at its original value and return false
would have meant the run
method would not call the update
method – yet.
Code the empty update
method and look at the comments to see what we will be coding in this method soon.
// Update all the game objects public void update() { // Move the snake // Did the head of the snake eat the apple? // Did the snake die? }
The update
method is empty, but the comments give a hint as to what we will be doing later in the project. Make a mental note that it is only called when the thread is running, the game is playing, it is not paused and when updateRequired
returns true.
Code and examine the draw
method. Remember that the draw
method is called whenever the thread is running, and the game is playing even when update
does not get called.
// Do all the drawing public void draw() { // Get a lock on the mCanvas if (mSurfaceHolder.getSurface().isValid()) { mCanvas = mSurfaceHolder.lockCanvas(); // Fill the screen with a color mCanvas.drawColor(Color.argb(255, 26, 128, 182)); // Set the size and color of the mPaint for the text mPaint.setColor(Color.argb(255, 255, 255, 255)); mPaint.setTextSize(120); // Draw the score mCanvas.drawText("" + mScore, 20, 120, mPaint); // Draw the apple and the snake // Draw some text while paused if(mPaused){ // Set the size and color of mPaint for the text mPaint.setColor(Color.argb(255, 255, 255, 255)); mPaint.setTextSize(250); // Draw the message // We will give this an international upgrade soon mCanvas.drawText("Tap To Play!", 200, 700, mPaint); } // Unlock the Canvas to show graphics for this frame mSurfaceHolder.unlockCanvasAndPost(mCanvas); } }
The draw
method is mostly just as we have come to expect.
Surface
is validCanvas
Canvas
and reveal our glorious drawingsIn the "Do the drawing" phase mentioned in the list we scale the text size with setTextSize
and then draw the score in the top left of the screen. Next, in this phase, we check whether the game is paused and if it is draw a message to the center of the screen, "Tap To Play!". We can almost run the game. Just a few more short methods.
Next on our to-do list is onTouchEvent
which is called by Android every time the player interacts with the screen. We will add more code here as we progress. For now, add the following code which, if mPaused
is true
, sets mPaused
to false
and calls the newGame
method.
@Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction() &MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_UP: if (mPaused) { mPaused = false; newGame(); // Don't want to process snake // direction for this tap return true; } // Let the Snake class handle the input break; default: break; } return true; }
The above code has the effect of toggling the game between paused and not paused with each screen interaction.
Add the familiar pause
and resume
methods. Remember that nothing happens if the thread has not been started. When our game is run by the player the Activity will call this resume
method and start the thread. When the player quits the game, the Activity will call pause
that stops the thread.
// Stop the thread public void pause() { mPlaying = false; try { mThread.join(); } catch (InterruptedException e) { // Error } } // Start the thread public void resume() { mPlaying = true; mThread = new Thread(this); mThread.start(); }
We can now test our code so far.
3.147.6.118