Coding the game engine

Let's get started with the most significant class of this project- SnakeGame. This will be the game engine for the Snake game.

Coding the members

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.

Coding the constructor

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.

Coding the newGame method

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.

Coding the run method

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.

Coding the updateRequired method

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.

Coding the update method

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.

Coding the draw method

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.

  • Check if the Surface is valid
  • Lock the Canvas
  • Fill the screen with a color
  • Do the drawing
  • Unlock the Canvas and reveal our glorious drawings

In 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.

Coding onTouchEvent

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.

Coding pause and resume

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.6.118