官术网_书友最值得收藏!

Chapter 7. Retro Squash Game

This chapter is where the fun starts. Although a retro squash game is obviously a step or two down from the latest big-budget game, it is the point when we start to look at some fundamentals—drawing, detecting when objects we have drawn bump into each other, and having animation that is actually controlled by us.

Once you can draw a pixel and move it, it only needs a bit of imagination and work and you have the potential to draw anything. Then, when we combine this knowledge with some really simple math to simulate the physics of collision and gravity, we will be close to being able to implement our squash game.

Tip

Sadly, this book does not have the time to go into the mathematics of turning a dot on the screen into realistic three-dimensional characters moving around in a three-dimensional world. Certainly, the technology and math behind big-budget titles is very advanced and complicated. However, the basics of turning pixels into lines and lines into triangles, texturing a triangle, building objects out of triangles, and positioning them in a three-dimensional world are within the grasp of anybody who has learned high-school-level math. Often, we hear that great graphics don't make a great game, which is true, but great graphics (at least for me) are one of the most exciting aspects of video games, even when they are displayed on a game that could be more fun to play by itself. If you want to see how to turn pixels into magical worlds, and start to appreciate what goes on behind the scenes of the top game engines and graphics libraries, you could start with Computer Graphics: Mathematical First Steps, P.A. Egerton and W.S Hall, Prentice Hall.

In this chapter, we will cover the following topics:

  • Explore the Android Canvas class, which makes drawing easy and fun
  • Write a simple Canvas demo app
  • Learn about detecting touches on the screen
  • Create the retro squash game
  • Implement the retro squash game

Drawing with Android Canvas

So far, we have been using the Android UI designer to implement all our graphics. This is fine when all we need are objects such as buttons and text.

It is true that there is more to the Android UI elements than we have explored so far. For example, we know we can do a lot more with the Animation class, and we very briefly saw that we can assign any image we like to represent one of the UI elements.

As an example, we could assign game characters such as spaceships to UI elements and animate them.

However, if we want smoothly moving spaceships with accurate collision detection, cute characters, and gruesome baddies with multiframe, cartoon-like animation, then we are going to need to move away from predefined UI elements.

We are going to need to start looking at and designing with individual pixels, lines, bitmaps, and sprite sheets. Fortunately, as you might have guessed, Android has some classes to make this nice and easy for us. We will be learning how to get started with the Canvas and Paint classes.

Bitmaps and sprite sheets will be covered in the next chapter. In this chapter, we will learn how to draw pixels and lines to make a simple, smoothly moving pong-style game of squash.

To achieve this, we will learn about the coordinate system we use to draw our pixels and lines. Then we will look at the Paint and Canvas classes themselves.

The Android coordinate system

A pixel is the smallest graphical element we can manipulate using the Paint and Canvas classes. It is essentially a dot. If your device resolution is 1920 x 1080, like some of the newer Google-branded tablets or high-end Samsung phones, then we have 1920 pixels across the longest length of the device and 1080 pixels across the width.

We can therefore think of our screen on which we will be drawing as a grid. We draw using the Canvas and Paint classes on a virtual canvas. We will do so by plotting points (pixels), lines, shapes, and text using coordinates on this grid.

The coordinate system starts in the top-left corner of the screen.

As an example, take a look at this line of code:

drawPoint(0, 0); //Not actual syntax (but very close)

In this, we would plot a single pixel in the top-left corner of the screen. Now look at the following code:

drawPoint(1920, 1080); //Not actual syntax (but very close)

If we use it like this, we could draw a point in the bottom-right corner of one of these high-end devices (while in the landscape position).

We could also draw lines by specifying a start and end coordinate position, a bit like this:

drawLine(0,0,1920, 1080); //Not actual syntax (but very close)

This would draw a line from the top-left corner of the screen to the bottom right.

You might have noticed some potential problems. Firstly, not all Android devices have such a high resolution; in fact, most are significantly lower. Even devices with high resolution will have totally different coordinates when held in landscape or portrait positions. How will we write code that adapts to these devices regardless of the screen resolution? We will see the solution soon.

Animating our pixels

Drawing shapes, lines, and pixels is all very well, but how do we make them appear to move? We will be using the same animation trick used in cartoons, movies, and other video games:

  1. Draw an object.
  2. Rub it out.
  3. Draw the object in its new position.
  4. Repeat fast enough to trick the player's brain that the game objects are moving.

The theory makes all of this sound more complicated than it is. Let's take a quick look at the Paint and Canvas classes and a quick introductory demo app. Then we can implement our retro squash game for real.

Getting started with Canvas and Paint

The aptly named Canvas class provides just what you would expect—a virtual canvas to draw our graphics on.

We can make a virtual canvas using the Canvas class from any Android UI element. In our demo app, we will draw on an ImageView, and when we make our game, we will draw straight on a special type of view, which will bring some extra advantages, as we will see.

To get started, we need a view to draw on. We already know how to get a view from our UI layout using Java code:

ImageView ourView = (ImageView) findViewById(R.id.imageView);

This line of code grabs a reference to an ImageView placed in the UI design and assigns it to our object in our Java code. As we have seen, the ImageView in the UI design has an assigned ID of imageView, and our controllable ImageView object in our Java code is called ourView.

Now we need a bitmap. A bitmap itself has a coordinate system like the screen. We are creating a bitmap to turn it into a canvas:

Bitmap ourBitmap = Bitmap.createBitmap(300,600, Bitmap.Config.ARGB_8888);

The previous line of code declares and creates an object of the Bitmap type. It will have a size of 300 by 600 pixels. We will keep this in mind when we draw on it shortly.

Tip

The last argument in the createBitmap method, Bitmap.Config.ARGB_8888, is simply a format, and we can create some great games without getting into the different options for bitmap formats.

Now we can prepare our bitmap for drawing by creating a Canvas object from it:

Canvas ourCanvas = new Canvas(ourBitmap);

Next, we get ourselves an object of the Paint type. We can think of this object as the brush and the paint for our virtual canvas:

Paint paint = new Paint();

At this point, we are ready to use our Paint and Canvas objects to do some drawing. The actual code to draw a pixel in the top-left corner of the screen will look like this:

ourCanvas.drawPoint(0, 0, paint);//How simple is that?

Let's now look at a working example.

Android Canvas demo app

Let's make an app that uses the Canvas and Paint classes and do a bit of drawing. This example will be completely static (no animation), so we can clearly see how to use Canvas and Paint without cluttering the code with things we will learn later.

In this demo app, we use some conceptually helpful variable names to help us grasp the role that each object is playing, but we will go through the whole thing at the end to make sure we know exactly what is going on at each stage. Of course, you don't have to type all of this. You can open the completed code files from the CanvasDemo folder in the Chapter7 folder of the download bundle:

  1. Start a new project and call it CanvasDemo. Tidy up the unnecessary imports and overrides if you want to.
  2. Open activity_main.xml in the editor. Drag an ImageView from the palette to the layout. The ImageView has an ID by default, which is imageView. Now we will use this ID in our code.
  3. Switch to MainActivity.java in the editor. First, we will create our Bitmap, Canvas, and Paint objects as we discussed earlier. Here is the first part of the code. Enter it directly after the call to the setContentView method:
    //Get a reference to our ImageView in the layout
    ImageView ourFrame = (ImageView) findViewById(R.id.imageView);
    
    //Create a bitmap object to use as our canvas
    Bitmap ourBitmap = Bitmap.createBitmap(300,600, Bitmap.Config.ARGB_8888);
    Canvas ourCanvas = new Canvas(ourBitmap);
    
    //A paint object that does our drawing, on our canvas
    Paint paint = new Paint();
  4. Here, we try out some of the cool things we can draw. Enter the code directly after the code in the previous step:
    //Set the background color
    ourCanvas.drawColor(Color.BLACK);
    
    //Change the color of the virtual paint brush
    paint.setColor(Color.argb(255, 255, 255, 255));
    
    //Now draw a load of stuff on our canvas
    ourCanvas.drawText("Score: 42 Lives: 3 Hi: 97", 10, 10, paint);
    ourCanvas.drawLine(10, 50, 200, 50, paint);
    ourCanvas.drawCircle(110, 160, 100, paint);
    ourCanvas.drawPoint(10, 260, paint);
    
    //Now put the canvas in the frame
      ourFrame.setImageBitmap(ourBitmap);
  5. Run the demo on an emulator or a device.

Your output will look like what is shown in the following screenshot:

Let's go through the code again. In steps 1 and 2, we created a new project and placed an ImageView object with an ID of imageView on our UI layout.

In step 3, we started by getting a reference to the ImageView object in our layout. However, we have done this often, usually with TextViews and Buttons. We named our ImageView ourFrame because it will hold our canvas:

ImageView ourFrame = (ImageView) findViewById(R.id.imageView);

Then we created a bitmap to be used to make a canvas:

Bitmap ourBitmap = Bitmap.createBitmap(300,600, Bitmap.Config.ARGB_8888);
Canvas ourCanvas = new Canvas(ourBitmap);

After that, we created our new Paint object:

Paint paint = new Paint();

In step 4, we were ready to draw, and we did so in a few different ways. First, we painted the entire canvas black:

ourCanvas.drawColor(Color.BLACK);

Then we chose the color with which we will be painting. (255, 255, 255, 255) is a numerical representation of white with full opacity (no transparency):

paint.setColor(Color.argb(255, 255, 255, 255));

Now we see something new, but it is quite easy to understand. We can also draw strings of text to the screen and position that text at precise screen coordinates, just like we can with a pixel.

You will notice that with the drawText method and all other drawing methods of the Canvas class, we always pass our Paint object as an argument. Just to make what is going on in the next line of code absolutely clear, I am stating that "Score: 42 Lives:3 Hi: 97" is the string that will be drawn on the screen, 10, 10 are the screen coordinates, and paint is our Paint object:

ourCanvas.drawText("Score: 42 Lives: 3 Hi: 97", 10, 10, paint);

Next, we draw a line. The argument list here can be described as follows: (start x coordinate, start y coordinate, end x coordinate, end y coordinate, our Paint object):

ourCanvas.drawLine(10, 50, 200, 50, paint);

Now we see that we can draw circles. We can also draw other shapes. The argument list here can be described as follows: (start x coordinate, start y coordinate, radius of circle, our Paint object):

ourCanvas.drawCircle(110, 160, 100, paint);

Then we draw a humble, lonely pixel (point). The arguments we use are in this format: (x coordinate, y coordinate, Paint object):

ourCanvas.drawPoint(10, 260, paint);

Finally, we place our bitmap canvas on our ImageView frame:

ourFrame.setImageBitmap(ourBitmap);

We still need to get smarter with managing screen resolution and orientation, and we will do so in our retro squash game. Also, we need to look for a system that will allow us to rub out and redraw our images at a set interval to create the illusion of movement. Actually, we already know one such system. Think about how we might use threads to achieve this illusion. First of all, let's take a look at how the player will control the game. After all, we are not going to have any handy UI buttons to press for this game.

Detecting touches on the screen

In our retro squash game, we will have no UI buttons, so we cannot use the OnClickListener interface and override the onClick method. This is not a problem, however. We will just use another interface to suit our situation. We will use OnTouchListener and override the onTouchEvent method. It works a bit differently, so let's take a look at implementing it before we dive into the game code.

We must implement the OnTouchListener interface for the activity we want to listen to touches in, like this:

public class MainActivity extends Activity implements View.OnTouchListener{

Then we can override the onTouchEvent method, perhaps a bit like this.

@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
  float x = motionEvent.getX();
  float y = motionEvent.getY();
  //do something with the x and y values
  return false;
}

The x variable will hold the horizontal value of the position on the screen that was touched, and y will hold the vertical position. It is worth noting that the motionEvent object parameter contains lots of information as well as the x and y location, for example, whether the screen was touched or released. We can make some really useful switch statements with this information, as we will see later.

Knowing exactly how we use this to achieve our goals in the squash game requires us to first consider the design of the game.

Preparing to make the retro squash game

Now we are ready to discuss the making of our next game. We actually know everything we need to. We just need to think about how to use the different techniques we have learned.

Let's first look at exactly what we want to achieve so that we have something to aim for.

The design of the game

Let's look at a screenshot of the game as a good starting point. When you design your own games, drawing sketches of the in-game objects and mechanics of the game will be an invaluable part of the design process. Here, we can cheat a bit by taking a look at the end result.

The UI

Starting from the top, we have Score. Every time the player successfully hits the ball, a point is added. Next, we have Lives. The player starts with three lives, and every time they let a ball go past their racket, they lose one life. When the player has zero lives, their score is set to zero, lives are set back to three, and the game begins again. Next to this, we have FPS. FPS stands for frames per second. It would be nice if we monitor on the screen the number of times our screen is being redrawn every second, as this is the first time we are animating our very own graphics.

Approximately in the middle of the previous screenshot is the ball. It is a square ball, in keeping with the traditional pong style. Squares are also easier when you have to perform realistic-looking collision detection.

Physics

We will detect when the ball hits any of the four sides of the screen as well as when it hits the racket. Depending on what the ball hits and its current direction at the time of the collision, we will determine what happens to the ball. Here is a rough outline of what each type of collision will do:

  • Hit the top of the screen: The ball will maintain the same horizontal (x) direction of travel but reverse the vertical (y) direction of travel.
  • Hit either side of the screen: The ball will maintain its y direction of travel but reverse its x direction.
  • Hit the bottom of the screen: The ball will disappear and restart at the top of the screen with a downward y direction of travel and a random x direction of travel.
  • Hit the player's racket: We will check whether the ball has hit the left or the right of the racket and alter the x direction of travel to match. We will also reverse the y direction of travel to send the ball back to the top again.

By enforcing these crude virtual rules of physics, we can simply create a ball that behaves almost as we would expect a real ball to do. We will add a few properties such as slightly increasing the ball speed after hitting the racket. These rules will work just as well in portrait or landscape orientations.

The player's racket will be a simple rectangle that the player can slide left by holding anywhere on the left half of the screen, and right by holding anywhere on the right of the screen.

For brevity, we will not be making a main menu screen to implement high scores. In our final game, which we start in the next chapter, we will go ahead and have an animated menu screen, online high scores, and achievements. However, this squash game will simply restart when the player reaches zero lives.

The structure of the code

Here, we will take a quick theoretical look at some aspects of the implementation that might be raising questions. When we finally get down to the implementation, we should find most of the code quite straightforward, with only a few bits that might need extra explanation.

We have discussed everything we need to know, and we will also discuss specifics in the code as we go through the implementation. We will go over the trickier parts of the code at the end of each phase of implementation.

As usual, all the completed code files can be found in the download bundle. The files encompassing all the phases of this project are in the Chapter7/RetroSquash folder.

We have learned that in an application using classes and their methods, different parts of the code will be dependent on other parts. Therefore, rather than jumping back and forth in the code, we will lay it out from the first line to the last in order. Of course, we will also refer to the related parts of code as we go along. I definitely recommend studying the code in its entirety to fully grasp what is going on and which parts of the code call which other parts.

To prevent this implementation from spreading into an enormous to-do list, it has been broken into four phases. This should provide convenient places to stop and take a break.

There is no layout file and only one .java file. This file is called MainActivity.java. The MainActivity.java file has a structure as indicated in the following overview of the code. I have indented some parts to show what parts are enclosed within others. This is a high-level view, and it omits quite a lot of detail:

Package name and various import statements
MainActivity class starts{
    Declare some member variables
    OnCreate method{
      Initialization and setup
    }
    SquashCourtView class{
      Constructor
      Multiple methods of SquashCourtView
    }
    Some Android lifecycle method overrides
}

As previously stated, we can see that everything is in the MainActivity.java file. As usual, at the top of our file, we will have a package name and a load of imports for the different classes we will be using.

Next, as per all our other projects, we have the MainActivity class. It encompasses everything else, even the SquashCourtView class. This makes the SquashCourtView class an inner class and will therefore be able to access the member variables of the MainActivity class, which will be essential in this implementation.

Before the SquashCourtView class, however, comes the declaration of all the member variables in the MainActivity class, followed by a fairly in-depth onCreate method.

We could implement the other Android lifecycle methods next, and you are welcome to do so. However, the code within the other Android lifecycle methods will make more sense once we have seen the code in the SquashCourtView class methods.

After onCreate, we will implement the SquashCourtView class. This has some fairly long methods in it, so we will break it into phases 2 and 3.

Finally, we will implement the remaining Android lifecycle methods. They are short but important.

The four implementation phases in detail

Let's take an even closer look at the implementation before we actually get to it. Here is how we will divide the implementation into the four phases, this time with a bit more detail as to what to expect in each:

  • Phase 1 – MainActivity and onCreate: In this phase, we will create the project itself as well as implement the following steps:
    • We will add our imports and create the body of our MainActivity class
    • Within this, we will declare the member variables that the game needs
    • We will implement our onCreate method, which does loads of setup work but nothing that is hard to understand
  • Phase 2 – SquashCourtView part 1: In this phase, we will start work on our key class, SquashCourtView. Specifically, we will:
    • Implement the declaration of the SquashCourtView class and its member variables.
    • Write a simple constructor.
    • Implement the run method to control the flow of the game.
    • Implement the lengthy but fairly easy-to-understand updateCourt method. This is the method that handles collision detection and keeps track of our ball and racket.
  • Phase 3 – SquashCourtView part 2: In this phase, we will finish the SquashCourtView class by implementing the following:
    • The drawCourt method, which unsurprisingly does all the drawing
    • The controlFPS method, which makes the game run at similar speeds on devices that have different CPUs
    • Next, we will quickly write a couple of methods that help the Android lifecycle methods with similar names—the pause and resume methods
    • Finally for this phase, we will easily handle the touch controls of the game by overriding the onTouchEvent method we looked at earlier
  • Phase 4 – Remaining lifecycle methods: In this short phase we will add the finishing touches:
    • Quickly implement what happens in the onPause, onResume, and onStop methods by overriding them
    • We will also handle what happens when the player presses the back button on their phone or tablet

Phase 1 – MainActivity and onCreate

Now that we have seen what we will do in each of the phases, let's actually get started with building our game by performing the following steps:

  1. Create a new project, just as we have before, but with one slight difference. This time, on the New Project dialog, change Minimum required SDK to API 13: Android 3.2 (Honeycomb). Call the project RetroSquash. Delete the unnecessary overridden methods if you like.
  2. Edit the AndroidManifest.xml file, just as we did at the end of if needed. Note that we are not locking orientation because this game is fun in both portrait and landscape. Here is the line of code to add:
    android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
  3. Make some sound effects using Bfxr, as we did in Chapter 5, Gaming and Java Essentials. Four will be enough, but there is nothing stopping you from adding more sounds. For authentic 1970s-style sounds, try the Blip/Select button shown in the following screenshot. Name the samples sample1.ogg, sample2.ogg, sample3.ogg, and sample4.ogg. Or you can just use my samples. They are in the assets folder of the folder named RetroSquash in the code bundle.
  4. In Project Explorer, create a directory called assets within the main directory. Copy the four sound files you created in the previous step to the newly created assets folder.
  5. Type the following import statements at the top of the MainActivity.java file but just after your package name, as shown in the following code:
    package com.packtpub.retrosquash.app;
    
    import android.app.Activity;
    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.AudioManager;
    import android.media.SoundPool;
    import android.os.Bundle;
    import android.view.Display;
    import android.view.KeyEvent;
    import android.view.MotionEvent;
    import android.view.SurfaceHolder;
    import android.view.SurfaceView;
    import java.io.IOException;
    import java.util.Random;
  6. Now type your class declaration and declare the following member variables. We will discuss the member variables in detail at the end of this phase:
    public class MainActivity extends Activity {
    
        Canvas canvas;
        SquashCourtView squashCourtView;
    
        //Sound
        //initialize sound variables
        private SoundPool soundPool;
        int sample1 = -1;
        int sample2 = -1;
        int sample3 = -1;
        int sample4 = -1;
    
        //For getting display details like the number of pixels
        Display display;
        Point size;
        int screenWidth;
        int screenHeight;
    
        //Game objects
        int racketWidth;
        int racketHeight;
        Point racketPosition;
    
        Point ballPosition;
        int ballWidth;
    
        //for ball movement
        boolean ballIsMovingLeft;
        boolean ballIsMovingRight;
        boolean ballIsMovingUp;
        boolean ballIsMovingDown;
    
        //for racket movement
        boolean racketIsMovingLeft;
        boolean racketIsMovingRight;
    
        //stats
        long lastFrameTime;
        int fps;
        int score;
        int lives;
  7. Next, we will enter the onCreate method in its entirety. We are initializing many of the member variables that we declared in the previous step, as well as creating an object from our SquashCourtView class, which we will begin to implement in the next phase. Perhaps the most notable line in this block of code is the somewhat different call to setContentView. Look at the argument for setContentView. We will learn more about this argument at the end of this phase. This phase also sets up SoundPool and loads the sound samples. Type the first part of the onCreate code:
    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            squashCourtView = new SquashCourtView(this);
            setContentView(squashCourtView);
    
            //Sound code
            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) {
                //catch exceptions here
            }
  8. Now we initialize the variables we created earlier. Notice that there are some good potential candidates for a bit of encapsulation. However, to keep the code readable, we will not do so at this stage. Enter this code:
            //Could this be an object with getters and setters
            //Don't want just anyone changing screen size.
            //Get the screen size in pixels
            display = getWindowManager().getDefaultDisplay();
            size = new Point();
            display.getSize(size);
            screenWidth = size.x;
            screenHeight = size.y;
    
    
            //The game objects
            racketPosition = new Point();
            racketPosition.x = screenWidth / 2;
            racketPosition.y = screenHeight - 20;
            racketWidth = screenWidth / 8;
            racketHeight = 10;
    
            ballWidth = screenWidth / 35;
            ballPosition = new Point();
            ballPosition.x = screenWidth / 2;
            ballPosition.y = 1 + ballWidth;
    
            lives = 3;
    
        }

Phase 1 code explained

Let's look at what we did. From steps 1 to 4, we simply created a project and some sound files. Then we added the sound files to the assets folder as we have done before on other projects. In step 5, we added all the necessary imports for the classes we will be using.

In step 6, we created a whole load of member variables. Let's take a closer look at them. We declared an object of the Canvas type called canvas. We will use this object to set up our drawing system. We also declared an instance of SquashCourtView called squashCourtView. This will be underlined as an error because we haven't implemented the class yet.

Here, we declared and initialized variables to be references to our sound files, just as we did in other projects. After this, we did something new:

//For getting display details like the number of pixels
Display display;
Point size;
int screenWidth;
int screenHeight;

We declared a Display object and a Point object. We see these in action in our onCreate method in a minute, alongside the two int variables, screenWidth and screenHeight. We use them to get the screen size in pixels so that we can make our game work on a screen with any resolution.

Here, we declared some variables whose purpose is plain from their names. Their actual usage becomes clearer when we initialize them in step 8 and use them throughout our SquashCourtView class:

//Game objects
int racketWidth;
int racketHeight;
Point racketPosition;

Point ballPosition;
int ballWidth;

Here, we have a bunch of Boolean variables to control the logic of the movement of both the racket and the ball. Notice that there is a variable for each possible direction for both the racket and the ball. Notice also that the racket can move in two directions—left and right—and the ball in four. Of course, the ball can travel in two directions at the same time. All will become clear when we write the updateCourt method in phase 2. Here is that code again:

//for ball movement
boolean ballIsMovingLeft;
boolean ballIsMovingRight;
boolean ballIsMovingUp;
boolean ballIsMovingDown;

//for racket movement
 boolean racketIsMovingLeft;
 boolean racketIsMovingRight;

In the last part of step 6, we declared two fairly obvious variables, lives and score. But what about lastFrameTime and fps? These will be used in the controlFPS method, which we will write in phase 3. They will be used along with some local variables to measure how fast our game loop runs. We can then lock it to run at a consistent rate so that players on devices with different CPU speeds get a similar experience.

In step 7, we entered the onCreate method, but this time, things are different. We initialize squashCourtView as a new SquashCourtView object. It's fine so far, but then we seem to be telling setContentView to make this the entire view that the player will see, instead of the usual view created in the Android Studio designer, which we have become used to. We are not using any Android UI components in this game, so the visual designer and all of its generated XML are of no use to us. As you will see right at the start of phase 2, our SquashCourtView class extends (inherits from) SurfaceView.

We created an object with all the facilities of a SurfaceView. We will just customize it to play our squash game. Neat! Therefore, it is perfectly acceptable and logical to set our squashCourtView object as the entire view that the player will see:

squashCourtView = new SquashCourtView(this);
setContentView(squashCourtView);

We then set up our sound effects as we have done before.

In step 8, we initialized many of the variables that we declared in step 6. Let's look at the values and the order in which we initialized them. You might have noticed that we don't initialize every variable here; some will be initialized later. Remember that we don't have to initialize member variables and that they also have default values.

In the following code, we get the number of pixels (wide and high) for the device. The display object holds the details of the display after the first line has been executed. Then we create a new object called size of the Point type. We send size as an argument to the display.getSize method. The Point type has an x and y member variable, and so does the size object, which now holds the width and height (in pixels) of the display. These values are then assigned to screenWidth and screenHeight respectively. We will use screenWidth and screenHeight quite extensively in the SquashCourtView class:

display = getWindowManager().getDefaultDisplay();
size = new Point();
display.getSize(size);
screenWidth = size.x;
screenHeight = size.y;

Next, we initialize the variables that determine the size and position of the ball and racket. Here, we initialize our racketPosition object, which is of the Point type. Remember that it has an x and a y member variable:

racketPosition = new Point();

We initialize racketPosition.x to be whatever the current screen width in pixels might be, but divided by two, so the racket will start in a horizontal and central position regardless of the resolution of the screen:

racketPosition.x = screenWidth / 2;

In the next line of code, racketPosition.y is put at the bottom of the screen with a small 20-pixel gap:

racketPosition.y = screenHeight - 20;

We make the width of the racket to one-eighth the width of the screen. We will see when we get to run the game that this is a fairly effective size, but we could make it bigger by dividing it by a lower number, or smaller by dividing it by a larger number. The point is that it will be the same fraction of screenWidth regardless of the resolution of the device:

racketWidth = screenWidth / 8;

In the following line of code, we choose an arbitrary height for our racket:

racketHeight = 10;

Then we make our ball as small as 1/35th of the screen. Again, we could make it larger or smaller:

ballWidth = screenWidth / 35;

In the next line of code, we will create a new point object to hold the position of the ball:

ballPosition = new Point();

As we did with the racket, we start the ball in the center of the screen, like this:

ballPosition.x = screenWidth / 2;

However, we set it to start at the top of the screen just far enough to see the top of the ball:

ballPosition.y = 1 + ballWidth;

The player starts the game with three lives:

lives = 3;

Phew! That was a fairly chunky section. Take a break if you like, and then we will move on to phase 2.

Phase 2 – SquashCourtView part 1

Finally, we get to the secret weapon of our game—the SquashCourtView class. The first three methods are presented here, and explained more fully once we have implemented them:

  1. Here is a class declaration that extends SurfaceView, giving our class all the methods and properties of SurfaceView. It also implements Runnable, which allows it to run in a separate thread. As you will see, we will put the bulk of the functionality in the run method. After the declaration, we have a constructor. Remember that the constructor is a method that has the same name as the class and is called when we initialize a new object of its type. The code in the constructor initializes some objects and then sends the ball off in a random direction. We will look at that part in detail after we have implemented this phase. Enter the following code before the closing curly brace of the MainActivity class:
    class SquashCourtView extends SurfaceView implements Runnable {
            Thread ourThread = null;
            SurfaceHolder ourHolder;
            volatile boolean playingSquash;
            Paint paint;
    
            public SquashCourtView(Context context) {
                super(context);
                ourHolder = getHolder();
                paint = new Paint();
                ballIsMovingDown = true;
    
                //Send the ball in random direction
                Random randomNumber = new Random();
                int ballDirection = randomNumber.nextInt(3);
                switch (ballDirection) {
                    case 0:
                        ballIsMovingLeft = true;
                        ballIsMovingRight = false;
                        break;
    
                    case 1:
                        ballIsMovingRight = true;
                        ballIsMovingLeft = false;
                        break;
    
                    case 2:
                        ballIsMovingLeft = false;
                        ballIsMovingRight = false;
                        break;
                }
    
    
            }
  2. Now we have this short and sweet overriding of the run method. Remember that the run method contains the functionality of the thread. In this case, it has three calls, one to each of updateCourt, drawCourt, and controlFPS, the three key methods of our class. Enter this code:
    @Override
            public void run() {
                while (playingSquash) {
                    updateCourt();
                    drawCourt();
                    controlFPS();
    
                }
    
            }
  3. We will implement just one more method in this phase (updateCourt), but it is quite long. We will split it into chunks and briefly mention what is going on in each chunk before we type the code. We will perform a closer examination of how it works when the phase is implemented. In this next chunk of code, we handle the left and right movement of the racket as well as detecting and reacting when the ball hits either the left or the right of the screen. Enter the following code after the code from the previous step:
    public void updateCourt() {
                if (racketIsMovingRight) {
                    racketPosition.x = racketPosition.x + 10;
                }
    
                if (racketIsMovingLeft) {
                    racketPosition.x = racketPosition.x - 10;
                }
    
    
                //detect collisions
    
                //hit right of screen
                if (ballPosition.x + ballWidth > screenWidth) {
                    ballIsMovingLeft = true;
                    ballIsMovingRight = false;
                    soundPool.play(sample1, 1, 1, 0, 0, 1);
                }
    
                //hit left of screen
                if (ballPosition.x < 0) {
                    ballIsMovingLeft = false;
                    ballIsMovingRight = true;
                    soundPool.play(sample1, 1, 1, 0, 0, 1);
                }
  4. In this next chunk of code, we check whether the ball has hit the bottom of the screen, that is, the player has failed to return the ball. Enter this code directly after the code in the previous step:
    //Edge of ball has hit bottom of screen
                if (ballPosition.y > screenHeight - ballWidth) {
                    lives = lives - 1;
                    if (lives == 0) {
                        lives = 3;
                        score = 0;
                        soundPool.play(sample4, 1, 1, 0, 0, 1);
                    }
                    ballPosition.y = 1 + ballWidth;//back to top of screen
    
                    //what horizontal direction should we use
                    //for the next falling ball
                    Random randomNumber = new Random();
                    int startX = randomNumber.nextInt(screenWidth - ballWidth) + 1;
                    ballPosition.x = startX + ballWidth;
    
                    int ballDirection = randomNumber.nextInt(3);
                    switch (ballDirection) {
                        case 0:
                            ballIsMovingLeft = true;
                            ballIsMovingRight = false;
                            break;
    
                        case 1:
                            ballIsMovingRight = true;
                            ballIsMovingLeft = false;
                            break;
    
                        case 2:
                            ballIsMovingLeft = false;
                            ballIsMovingRight = false;
                            break;
                    }
                }
  5. In this chunk of code, we handle whether the ball has hit the top of the screen. We also calculate all the possible movements of the ball for this frame. Now type the following code:
    //we hit the top of the screen
                if (ballPosition.y <= 0) {
                    ballIsMovingDown = true;
                    ballIsMovingUp = false;
                    ballPosition.y = 1;
                    soundPool.play(sample2, 1, 1, 0, 0, 1);
                }
    
                //depending upon the two directions we should
                //be moving in adjust our x any positions
                if (ballIsMovingDown) {
                    ballPosition.y += 6;
                }
    
                if (ballIsMovingUp) {
                    ballPosition.y -= 10;
                }
    
                if (ballIsMovingLeft) {
                    ballPosition.x -= 12;
                }
    
                if (ballIsMovingRight) {
                    ballPosition.x += 12;
                }
  6. Finally, we handle collision detection and the reaction of the racket and the ball. We also close the updateCourt method, and this is the last chunk of code for this phase. Enter the following after your code from the previous step:
    //Has ball hit racket
                if (ballPosition.y + ballWidth >= (racketPosition.y - racketHeight / 2)) {
                    int halfRacket = racketWidth / 2;
                    if (ballPosition.x + ballWidth > (racketPosition.x - halfRacket)
                        && ballPosition.x - ballWidth < (racketPosition.x + halfRacket)) {
                        //rebound the ball vertically and play a sound
                        soundPool.play(sample3, 1, 1, 0, 0, 1);
                        score++;
                        ballIsMovingUp = true;
                        ballIsMovingDown = false;
                        //now decide how to rebound the ball horizontally
                        if (ballPosition.x > racketPosition.x) {
                            ballIsMovingRight = true;
                            ballIsMovingLeft = false;
    
                        } else {
                            ballIsMovingRight = false;
                            ballIsMovingLeft = true;
                        }
    
                    }
                }
            }
    }

Phase 2 code explained

The code in this phase was lengthy, but there is nothing too challenging when we break it down. Possibly, the only challenge is in unravelling some of those nested if statements. We will do this now.

In step 1, we declare our SquashCourView class. This implements the Runnable interface. You might remember from Chapter 5, Gaming and Java Essentials, that Runnable provides us with a thread. All we need to do is override the run method, and whatever is within it will work in a new thread.

Then we created a new Thread object called ourThread, and a SurfaceHolder object to hold our surface and enable us to control or lock our surface for use within our thread. Next, we have playingSquash of the boolean type. This wraps the inside of our overridden run method to control when the game is running. The odd-looking volatile modifier means that we will be able to change its value from the outside and inside of our thread.

Lastly, for the currently discussed block of code, we declare an object of the Paint type, called paint, to do our painting:

class SquashCourtView extends SurfaceView implements Runnable {
        Thread ourThread = null;
        SurfaceHolder ourHolder;
        volatile boolean playingSquash;
        Paint paint;

Next, we implemented the constructor of our class, so that when we initialized a new SquashCourtView object back in onCreate, this is the code that runs. First, we see that we run the constructor of the superclass. Then we initialize ourHolder using the getHolder method. Next, we initialize our paint object:

        public SquashCourtView(Context context) {
            super(context);
            ourHolder = getHolder();
            paint = new Paint();

Now, still within the constructor, we get things moving. We set our ballIsMovingDown variable to true. At the start of each game, we always want the ball to be moving down. We will see soon that the updateCourt method will perform the ball movement. Next, we send the ball in a random horizontal direction. This is achieved by getting a random number between 0 and 2. We then switch for each possible case: 0, 1, or 2. In each case statement, we set the Boolean variables that control horizontal movement differently. In case 0, the ball moves left, and in case 1 and case 3, the ball will move right and straight down, respectively. Then we close our constructor:

            ballIsMovingDown = true;

            //Send the ball in random direction
            Random randomNumber = new Random();
            int ballDirection = randomNumber.nextInt(3);
            switch (ballDirection) {
                case 0:
                    ballIsMovingLeft = true;
                    ballIsMovingRight = false;
                    break;

                case 1:
                    ballIsMovingRight = true;
                    ballIsMovingLeft = false;
                    break;

                case 2:
                    ballIsMovingLeft = false;
                    ballIsMovingRight = false;
                    break;
            }


        }

In step 2, we have some really simple code, but this is the code that runs everything else. The overridden run method is what ourThread calls at defined intervals. As you can see, the code is wrapped in a while block controlled by our playingSquash variable of the boolean type. Then the code simply calls updateCourt, which controls movement and collision detection; drawCourt, which will draw everything; and controlFPS, which will lock our game to a consistent frame rate. That's it for run:

@Override
        public void run() {
            while (playingSquash) {
                updateCourt();
                drawCourt();
                controlFPS();

            }

        }

Then in step 3, we begin the updateCourt method. It was quite long, so we broke it down into a few manageable chunks. The first two if blocks check to see whether either the racketIsMovingRight or the racketIsMovingLeft Boolean variables is true. If one of them is true, the blocks add 10 to or subtract 10 from racketPosition.x. The effect of this will be seen by the player when the racket is drawn in the drawCourt method. How the Boolean variables are manipulated in the onTouchEvent method will be discussed soon:

public void updateCourt() {
            if (racketIsMovingRight) {
                racketPosition.x = racketPosition.x + 10;
            }

            if (racketIsMovingLeft) {
                racketPosition.x = racketPosition.x - 10;
            }

Now, still in the updateCourt method, we detect and handle collisions with the left and right side of the screen. Checking whether ballPosition.x is larger than screenWidth would be enough to see whether the ball bounces back the other way. However, by being a bit more precise and testing for ballPosition.x + ballWidth > screenWidth, we are testing whether the right edge of the ball hits the right side of the screen. This creates a much more pleasing effect as it looks more real. When a collision occurs with the right side, we simply reverse the direction of our ball and play a sound. The reason that the if code for the left-side detection is simpler is because we have drawn the ball using drawRect, so ballPosition.x is the precise left side of the ball. When the ball collides with the left side, we simply reverse its direction and play a beep sound:

            //detect collisions

            //hit right of screen
            if (ballPosition.x + ballWidth > screenWidth) {
                ballIsMovingLeft = true;
                ballIsMovingRight = false;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }

            //hit left of screen
            if (ballPosition.x < 0) {
                ballIsMovingLeft = false;
                ballIsMovingRight = true;
                soundPool.play(sample1, 1, 1, 0, 0, 1);
            }

In step 4, we implemented what happens when the ball hits the bottom of the screen. This occurs when the player fails to return the ball, so a fair few things need to happen here. However, there is nothing overly complicated in this section. First comes the collision test. We check whether the underside of the ball has hit the bottom of the screen:

//Edge of ball has hit bottom of screen
if (ballPosition.y > screenHeight - ballWidth) {

If it has hit, we deduct a life. Then we check whether the player has lost all their lives:

   lives = lives - 1;
   if (lives == 0) {

If all lives are lost, we start the game again by resetting lives to 3 and score to 0. We also play a low beep sound:

          lives = 3;
          score = 0;
          soundPool.play(sample4, 1, 1, 0, 0, 1);
       }

As of now, we are still within the if block because the ball hit the bottom of the screen, but outside the if block for the player who has zero lives. Whether the player has zero lives or still has some lives left, we need to put the ball back at the top of the screen and send it in a downward trajectory and a random horizontal direction. This code is similar to but not the same as the code we have seen in the constructor to start the ball moving at the beginning of the game:

ballPosition.y = 1 + ballWidth;//back to top of screen
//what horizontal direction should we use
//for the next falling ball
Random randomNumber = new Random();
int startX = randomNumber.nextInt(screenWidth - ballWidth) + 1;
                ballPosition.x = startX + ballWidth;

                int ballDirection = randomNumber.nextInt(3);
                switch (ballDirection) {
                    case 0:
                        ballIsMovingLeft = true;
                        ballIsMovingRight = false;
                        break;

                    case 1:
                        ballIsMovingRight = true;
                        ballIsMovingLeft = false;
                        break;

                    case 2:
                        ballIsMovingLeft = false;
                        ballIsMovingRight = false;
                        break;
                }
            }

In step 5, we handle the event of the ball hitting the top of the screen. Reverse the values held by ballIsMovingDown and ballIsMovingUp to reverse the direction of the ball. Tweak the ball position with ballPosition.y = 1. This stops the ball from getting stuck and plays a nice beep:

//we hit the top of the screen
            if (ballPosition.y <= 0) {
                ballIsMovingDown = true;
                ballIsMovingUp = false;
                ballPosition.y = 1;
                soundPool.play(sample2, 1, 1, 0, 0, 1);
            }

Now, after all this collision detection and switching around of our Boolean variables, we actually move the ball. For each direction that is true, we add to or subtract from the ballPosition.x and ballPosition.y accordingly. Notice that the ball travels up faster than it travels down. This is done to shorten the time the player is waiting to get back into the action, and also crudely simulates the act of acceleration after the ball is hit by the racket:

            //depending upon the two directions we should be
            //moving in adjust our x any positions
            if (ballIsMovingDown) {
                ballPosition.y += 6;
            }

            if (ballIsMovingUp) {
                ballPosition.y -= 10;
            }

            if (ballIsMovingLeft) {
                ballPosition.x -= 12;
            }

            if (ballIsMovingRight) {
                ballPosition.x += 12;
            }

Tip

You might have noticed that by hardcoding the number of pixels the ball moves, we create an inconsistent speed for the ball between high-resolution and low-resolution screens. Take a look at the self-test questions at the end of the chapter to see how we can solve this.

We have one last bit of collision detection to do. Has the ball hit the racket? This detection is done in a couple of stages. First, we check whether the underside of the ball has reached or gone past the top side of the racket:

if (ballPosition.y + ballWidth >= (racketPosition.y - racketHeight / 2)) {

If this condition is true, we perform some more tests. First, we declare and initialize an int variable called halfRacket to hold half the width of the racket. We will use this in the upcoming tests:

int halfRacket = racketWidth / 2;

The next if block checks whether the right-hand side of the ball is greater than the far left corner of the racket, and whether it is touching it. Using the AND operator (&&), the block verifies that the ball's left edge is not past the far right of the racket. If this condition is true, we definitely have a hit and can think about how to handle the rebound:

if (ballPosition.x + ballWidth > (racketPosition.x - halfRacket)
  && ballPosition.x - ballWidth < (racketPosition.x + halfRacket)) {

The first bit of code inside the if block, which determined a definite hit, is simple. Play a sound, increase the score, and set the ball on an upwards trajectory, like this:

//rebound the ball vertically and play a sound
                    soundPool.play(sample3, 1, 1, 0, 0, 1);
                    score++;
                    ballIsMovingUp = true;
                    ballIsMovingDown = false;

Now we have an if-else condition, which simply checks whether the left-hand edge of the ball is past the center of the racket. If it is, we send the ball to the right. Otherwise, we send the ball to the left:

                    //now decide how to rebound the ball horizontally
                    if (ballPosition.x > racketPosition.x) {
                        ballIsMovingRight = true;
                        ballIsMovingLeft = false;

                    } else {
                        ballIsMovingRight = false;
                        ballIsMovingLeft = true;
                    }

                }
            }
        }

Phase 3 – SquashCourtView part 2

In this phase, we will complete our SquashCourtView class. There are two methods remaining that are called from the run method, drawCourt and controlFPS. Then there are a few short methods to interact with the Android lifecycle methods that we will implement in the fourth and final phase:

  1. Here is the code that draws, in the following order, the text at the top of the screen, the ball, and the bat. All is contained within the drawCourt method, which is called from the run method, right after the call to updateCourt. Here is the code for drawCourt. Type the following code before the closing curly brace of the SquashCourtView class:
    public void drawCourt() {
    
                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(45);
                    canvas.drawText("Score:" + score + " Lives:" + lives + " fps:" + fps, 20, 40, paint);
    
    
                    //Draw the squash racket
                    canvas.drawRect(racketPosition.x - (racketWidth / 2),
                      racketPosition.y - (racketHeight / 2), racketPosition.x + (racketWidth / 2),
                          racketPosition.y + racketHeight, paint);
    
                    //Draw the ball
                    canvas.drawRect(ballPosition.x, ballPosition.y,
                            ballPosition.x + ballWidth, ballPosition.y + ballWidth, paint);
    
    
                    ourHolder.unlockCanvasAndPost(canvas);
                }
    
            }
  2. And now the controlFPS method locks our frame rate to something smooth and consistent. We will soon go through its exact working. Type the following code after the code in the previous step:
    public void controlFPS() {
                long timeThisFrame = (System.currentTimeMillis() - lastFrameTime);
                long timeToSleep = 15 - timeThisFrame;
                if (timeThisFrame > 0) {
                    fps = (int) (1000 / timeThisFrame);
                }
                if (timeToSleep > 0) {
    
                    try {
                        ourThread.sleep(timeToSleep);
                    } catch (InterruptedException e) {
                    }
                    
                }
    
                lastFrameTime = System.currentTimeMillis();
            }
  3. Next, we write the code for pause and resume. These are called by their related Android lifecycle methods (onPause and onResume). We ensure that our thread is ended or started safely when the player has finished or resumed our game, respectively. Now type this code after the code in the previous step:
    public void pause() {
                playingSquash = false;
                try {
                    ourThread.join();
                } catch (InterruptedException e) {
                }
    
            }
    
            public void resume() {
                playingSquash = true;
                ourThread = new Thread(this);
                ourThread.start();
            }
  4. Finally, we have the method that controls what happens when the player touches our customized SurfaceView. Remember that when we discussed the design of the game, we said that a press anywhere on the left of the screen would move the racket to the left, and a press anywhere on the right will move the racket to the right. Type the following code after the code in the preceding step:
    @Override
            public boolean onTouchEvent(MotionEvent motionEvent) {
    
                switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                    case MotionEvent.ACTION_DOWN:
    
                        if (motionEvent.getX() >= screenWidth / 2) {
                            racketIsMovingRight = true;
                            racketIsMovingLeft = false;
                        } else {
                            racketIsMovingLeft = true;
                            racketIsMovingRight = false;
                        }
    
                        break;
    
    
                    case MotionEvent.ACTION_UP:
                        racketIsMovingRight = false;
                        racketIsMovingLeft = false;
                        break;
                }
                return true;
            }
    
    
        }

Phase 3 code explained

In step 1, we do all the drawing. We have seen what all the different drawing methods of the Canvas class can do, and their names are self-explanatory as well. However, the manner in which we arrived at the coordinates needs some explanation. First, inside drawCourt, we use ourHolder to get a drawing surface, and we check its validity (usability). Then we initialize our canvas and paint objects:

public void drawCourt() {

            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();
                //Paint paint = new Paint();

Next, we clear the screen from the previous frame of drawing:

       canvas.drawColor(Color.BLACK);//the background

Now we set the paint color to white:

        paint.setColor(Color.argb(255, 255, 255, 255));

This is new but simple to explain—we set a size for our text:

                paint.setTextSize(45);

Now we can draw a line of text at the top of the screen. It shows the score and lives variables. We have already seen how to control their values. It also shows the value of the fps variable. We will see how we can assign a value to that when we look at the next method, controlFPS:

  canvas.drawText("Score:" + score + " Lives:" + lives + " fps:" +fps, 20, 40, paint);

Then we draw the racket. Notice that we calculate the x start position by subtracting half the racket width from racketPosition.x, and the x end position by adding the width to x. This makes our collision detection code simple because racketPosition.x refers to the center of the racket:

//Draw the squash racket
  canvas.drawRect(racketPosition.x - (racketWidth / 2),
                  racketPosition.y - (racketHeight / 2), 
                  racketPosition.x + (racketWidth / 2),
                  racketPosition.y + racketHeight, paint);

Next, we draw the ball. Notice that the starting x and y coordinates are the same as the values held in ballPosition.x and ballPosition.y. Therefore, these coordinates correspond to the top-left corner of the ball. This is just what we need for our simple collision detection code:

                //Draw the ball
                canvas.drawRect(ballPosition.x, ballPosition.y,
                  ballPosition.x + ballWidth, ballPosition.y + ballWidth, paint);

This final line draws what we have just done to the screen:

                ourHolder.unlockCanvasAndPost(canvas);
            }

        }

In step 2, we essentially pause the game. We want to decide the number of times we recalculate the position of our objects and redraw them. Here is how it works.

First, we enter the controlFPS method when it is called from the run method. We declare and initialize a long variable with the time in milliseconds, and then take away the time that the last frame took in milliseconds. The time is calculated in the previous run through this method, at the end, as we will see:

public void controlFPS() {
long timeThisFrame = (System.currentTimeMillis() - lastFrameTime);

We then calculate how long we want to pause between frames, and initialize that value to timeToSleep, a new long variable. Here is how the calculation works: 15 milliseconds of pause gives us around 60 frames per second, which works well for our game and provides a very smooth animation. Therefore, 15 - timeThisFrame equals the number of milliseconds we should pause for to make the frame last for 15 milliseconds:

long timeToSleep = 15 - timeThisFrame; 

Of course, some devices will not cope with this speed. Neither do we want to pause for a negative number, nor do we want to calculate the frames per second when timeThisFrame is equal to zero. Next, we wrap the calculation of frames per second within an if statement that prevents us from dividing by zero or a negative number:

            if (timeThisFrame > 0) {
                fps = (int) (1000 / timeThisFrame);
            }

Likewise, we wrap the instruction to our thread to pause within a similar cautionary if statement:

            if (timeToSleep > 0) {

                try {
                    ourThread.sleep(timeToSleep);
                } catch (InterruptedException e) {
                }
                
            }

Finally, we see how we initialize lastFrameTime, ready for the next time controlFPS is called:

            lastFrameTime = System.currentTimeMillis();
        }

In step 3, we quickly implement two methods. They are pause and resume. These are not to be confused with the Android Activity lifecycle methods called onPause and onResume. However, the pause and resume methods are called from their near-namesakes. They handle stopping and starting ourThread, respectively. We should always clean up our threads. Otherwise, they can keep running even after the activity has finished:

public void pause() {
            playingSquash = false;
            try {
                ourThread.join();
            } catch (InterruptedException e) {
            }

        }

        public void resume() {
            playingSquash = true;
            ourThread = new Thread(this);
            ourThread.start();
        }

In step 4, we handle touches on the screen. This is how we initialize our racketIsMovingLeft and racketIsMovingRight Boolean variables, which the updateCourt method uses to decide whether to slide the player's racket left or right, or to keep it still. We have talked about the onTouchEvent method before, but let's see how we set the values in those variables.

First, we override the method and switch to get the type of event and the x, y coordinates of the event:

@Override
    public boolean onTouchEvent(MotionEvent motionEvent) {

    switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {

If the event type is ACTION_DOWN, that is, the screen has been touched, we enter this case:

    case MotionEvent.ACTION_DOWN:

Then we handle the coordinates. If the player has touched a position on the screen with an x coordinate greater than screenWidth / 2, then it means they have touched the right-hand side of the screen, so we set isMovingRight to true and isMovingLeft to false. The updateCourt method will handle changes in the necessary coordinates, and the drawCourt method will draw the racket in the appropriate place:

    if (motionEvent.getX() >= screenWidth / 2) {
        racketIsMovingRight = true;
        racketIsMovingLeft = false;

The else statement sets our two Boolean variables in the opposite manner because a touch must have occurred on the left of the screen:

                } else {
                    racketIsMovingLeft = true;
                    racketIsMovingRight = false;
                }

                    break;

Now we handle the case for the ACTION_UP event. But why do we care about two events? With the buttons, we just cared about a click and that was all, but by handling the ACTION_UP event, we can enable the functionality that allows our player to hold the screen to slide left or right, just as we discussed in the section The design of the game of this chapter. Thus, the ACTION_DOWN case sets the racket moving one way or the other, and the ACTION_UP case simply stops the slide completely:

                case MotionEvent.ACTION_UP:
                    racketIsMovingRight = false;
                    racketIsMovingLeft = false;
                    break;
            }
            return true;
        }


    }

Notice that we don't care about the y coordinate. Anywhere on the left we go left, anywhere on the right we go right.

Note

Notice also that all of the code will work whether a device is held in the portrait or landscape form, and will function the same regardless of the resolution of the device. However (and it is quite an important "however"), the game will be slightly harder on low-resolution screens. The solution to this problem is quite complicated and will not be discussed until the final chapter, but it might well help us make some decisions about the future path to learn Android, gaming, and Java.

Phase 4 – Remaining lifecycle methods

We are nearly there; just a few more steps and we will have a working retro squash game. I can almost smell the nostalgia! As these remaining methods are quite straightforward, we will explain them as we write them:

  1. As we previously learned, the onStop method is called by the Android system when the app is stopped. It is implemented for us already. The only reason we override it here is to ensure that our thread is stopped. We do so with the line highlighted. Enter the following code before the closing curly brace of the MainActivity class:
    @Override
        protected void onStop() {
            super.onStop();
    
            while (true) {
                squashCourtView.pause();
                break;
            }
    
            finish();
        }
  2. The onPause method is called by the Android system when the app is paused. This too is implemented for us already, and the only reason we override it here is to ensure that our thread is stopped. We do so with the line highlighted. Enter this code after the preceding code:
    @Override
        protected void onPause() {
            super.onPause();
            squashCourtView.pause();
        }
  3. The onResume method is called by the Android system when the app is resumed. Again, this method is implemented for us already. The only reason we override it here is to ensure that our thread is resumed, and we do so with the line highlighted. Enter the following code after the code in the previous step:
    @Override
        protected void onResume() {
            super.onResume();
            squashCourtView.resume();
        }
  4. Finally, we do something completely new. We handle what happens should the player press the back button on their device. As you might have guessed, there is a method we can override to achieve this— onKeyDown. We pause our thread, just as we did in the overridden lifecycle methods, and then call finish(), which ends the activity and our app. Enter this code after the code in the previous step:
    public boolean onKeyDown(int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                squashCourtView.pause();
                finish();
                return true;
            }
            return false;
        }

We covered the code in this phase as we went through it, and this was the shortest phase so far. So why didn't we encapsulate everything?

Good object-oriented design

Perhaps simple games are not the best way to demonstrate good object-oriented design in action, but a simple code design with fewer private variables actually enhances the project. It certainly makes the teaching aspects of coding games simpler to explain.

However, when a game becomes more complex and more people work on the code, principles of object-oriented programming become more necessary.

Self-test questions

Q1) Can you explain how to make ball speed relative between different screen resolutions?

Summary

I hope you enjoyed animating your first game. You achieved a lot to get to this point. You learned not only all the Java topics but also the way the different classes of Android can be used to make games relatively simple.

In the next chapter, we will move on to a new, more complex game. I hope you are ready.

主站蜘蛛池模板: 南汇区| 南江县| 斗六市| 孝昌县| 通辽市| 桐庐县| 刚察县| 长汀县| 呼伦贝尔市| 东丰县| 光泽县| 甘洛县| 丰都县| 曲水县| 贵德县| 荔浦县| 常山县| 贵阳市| 衢州市| 宁明县| 利川市| 民丰县| 江源县| 北京市| 马边| 巴林右旗| 蓝田县| 兴业县| 马公市| 广丰县| 英超| 伊宁市| 慈溪市| 东源县| 涿鹿县| 太仓市| 醴陵市| 香格里拉县| 依安县| 宁蒗| 太谷县|