We will be returning to this class constantly over the course of this project. What we will do in this chapter is get the fundamentals set up ready to add the game objects (bat and ball) as well as collision detection and sound effects over the next two chapters.
To achieve this, first we will add a bunch of member variables, then we will add some code inside the constructor to set the class up when it is instantiated/created by PongActivity
.
After this, we will code a startNewGame
method that we can call every time we need to start a new game including the first time we start a game after the app is started by the user.
Following on, we get to code the draw
method which will reveal the new steps that we need to take to draw to the screen 60 times per second and we will also see some familiar code that uses our old friends Canvas
, Paint
and drawText
.
At this point, we will need to discuss some more theory. Things like how we will time the animations of the bat and ball, how do we lock these timings without interfering with the smooth running of Android. These last two topics, the game loop, and threads will then allow us to add the final code of the chapter and witness our game engine in action- albeit with just a bit of text.
Add the variables as shown below after the PongGame
declaration but before the constructor. When prompted, click OK to import the necessary extra classes.
// Are we debugging? private final boolean DEBUGGING = true; // These objects are needed to do the drawing private SurfaceHolder mOurHolder; private Canvas mCanvas; private Paint mPaint; // How many frames per second did we get? private long mFPS; // The number of milliseconds in a second private final int MILLIS_IN_SECOND = 1000; // Holds the resolution of the screen private int mScreenX; private int mScreenY; // How big will the text be? private int mFontSize; private int mFontMargin; // The game objects private Bat mBat; private Ball mBall; // The current score and lives remaining private int mScore; private int mLives;
Be sure to study the code and then we can talk about it.
The first thing to notice is that we are using the naming convention of adding m
before the member variable names. As we add local variables in the methods this will help distinguish them from each other.
Also, notice that all the variables are declared private
. You could happily delete all the private
access specifiers and the code will still work but as we have no need to access any of these variables from outside of this class it is sensible to guarantee it can never happen by declaring them private
.
The first member variable is DEBUGGING
. We have declared this as final
because we don't want to change its value during the game. Note that declaring it final
does not preclude us from switching its value manually when we wish to switch between debugging and not debugging.
The next three classes we declare instances of will handle the drawing to the screen. Notice the new one we have not seen before.
// These objects are needed to do the drawing
private SurfaceHolder mOurHolder;
private Canvas mCanvas;
private Paint mPaint;
The SurfaceHolder
class is required to enable drawing to take place. It literally is the object that holds the drawing surface. We will see the methods it allows us to use to draw to the screen when we code the draw
method in a minute.
The next two variables give us a bit of insight into what we will need to achieve our smooth and consistent animation. Here they are again.
// How many frames per second did we get? private long mFPS; // The number of milliseconds in a second private final int MILLIS_IN_SECOND = 1000;
Both are of type long
because they will be holding a huge number. Computers measure time in milliseconds since 1970. More on that when we talk about the game loop but for now we need to know that by monitoring and measuring the speed of each frame of animation is how we will make sure that the bat and ball move exactly as they should.
The first mFPS
will be reinitialized every frame of animation around 60 times per second. It will be passed into each of the game objects (every frame of animation) so that they calculate how much time has elapsed and can then derive how far to move.
The MILLIS_IN_SECOND
variable is initialized to 1000
. There are indeed 1000
milliseconds in a second. We will use this variable in calculations as it will make our code clearer than if we used the literal value 1000. It is declared final
because the number of milliseconds in a second will obviously never change.
The next piece of the code we just added is shown again next for convenience.
// Holds the resolution of the screen private int mScreenX; private int mScreenY; // How big will the text be? private int mFontSize; private int mFontMargin;
The variables mScreenX
and mScreenY
will hold the horizontal and vertical resolution of the screen. Remember that they are being passed in from PongActivity
into the constructor.
The next two, mFontSize
and mMarginSize
will be initialized based on the screen size in pixels, to hold a value in pixels to make formatting of our text neat and more concise than constantly doing calculations for each bit of text.
Notice we have also declared an instance of Bat
and Ball
(mBat
and mBall
) but we won't do anything with them just yet.
The final two lines of code declare two member variables mScore
and mLives
which will hold the player's score and how many chances they have left.
Just to be clear before we move on, these are the import
statements you should currently have at the top of the PongGame.java
code file.
import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.view.SurfaceHolder; import android.view.SurfaceView;
Now we can begin to initialize some of these variables in the constructor.
Add the highlighted code to the constructor. Be sure to study the code as well and then we can discuss it.
public PongGame(Context context, int x, int y) { // Super... calls the parent class // constructor of SurfaceView // provided by Android super(context); // Initialize these two members/fields // With the values passed in as parameters mScreenX = x; mScreenY = y; // Font is 5% (1/20th) of screen width mFontSize = mScreenX / 20; // Margin is 1.5% (1/75th) of screen width mFontMargin = mScreenX / 75; // Initialize the objects // ready for drawing with // getHolder is a method of SurfaceView mOurHolder = getHolder(); mPaint = new Paint(); // Initialize the bat and ball // Everything is ready so start the game startNewGame(); }
The code we just added to the constructor begins by using the values passed in as parameters (x
and y
) to initialize mScreenX
and mScreenY
. Our entire PongGame
class now has access to the screen resolution whenever it needs it. Here are the two lines again:
// Initialize these two members/fields // With the values passed in as parameters mScreenX = x; mScreenY = y;
Next, we initialize mFontSize
and mFontMargin
as a fraction of the screen width in pixels. These values are a bit arbitrary, but they work, and we will use various multiples of these variables to align text on the screen neatly. Here are the two lines of code I am referring to:
// Font is 5% (1/20th) of screen width mFontSize = mScreenX / 20; // Margin is 1.5% (1/75th) of screen width mFontMargin = mScreenX / 75;
Moving on, we initialize our Paint
and SurfaceHolder
objects. Paint
uses the default constructor as we have done previously but mHolder
uses the getHolder
method which is a method of the SurfaceHolder
class. The getHolder
method returns a reference which is initialized to mHolder
so mHolder
is now that reference. In short, mHolder
is now ready to be used. We have access to this handy method because, PongGame
is a SurfaceView
.
// Initialize the objects // ready for drawing with // getHolder is a method of SurfaceView mOurHolder = getHolder(); mPaint = new Paint();
We will need to do more preparation in the draw
method before we can use our Paint
and Canvas
classes as we have done before. We will see exactly what very soon.
Finally, we call startNewGame
.
// Initialize the bat and ball // Everything is ready to start the game startNewGame();
Notice the startNewGame
method call is underlined in red as an error because we haven't written it yet. Let's do that next. Also, notice the comment indicating where we will eventually get around to initialize the bat and the ball objects.
Add the method's code immediately after the constructor's closing curly brace but before the PongGame
class closing curly brace.
// The player has just lost // or is starting their first game private void startNewGame(){ // Put the ball back to the starting position // Reset the score and the player's chances mScore = 0; mLives = 3; }
This simple method sets the score back to zero and the player's lives back to three. Just what we need to start a new game.
Note the comment stating // Put the ball back to starting position
. This identifies that once we have a ball we will reset its position at the start of each game from this method.
Let's get ready to draw.
Add the draw
method shown next immediately after the startNewGame
method. There will be a couple of errors in the code. We will deal with them, then we will go into detail about how the draw
method will work in relation to SurfaceView
because there are some completely alien-looking lines of code in there as well as some familiar ones.
// Draw the game objects and the HUD private void draw() { // Draw the game objects and the HUD void draw() { if (mOurHolder.getSurface().isValid()) { // Lock the canvas (graphics memory) ready to draw mCanvas = mOurHolder.lockCanvas(); // Fill the screen with a solid color mCanvas.drawColor(Color.argb (255, 26, 128, 182)); // Choose a color to paint with mPaint.setColor(Color.argb (255, 255, 255, 255)); // Draw the bat and ball // Choose the font size mPaint.setTextSize(mFontSize); // Draw the HUD mCanvas.drawText("Score: " + mScore + " Lives: " + mLives, mFontMargin , mFontSize, mPaint); if(DEBUGGING){ printDebuggingText(); } // Display the drawing on screen // unlockCanvasAndPost is a method of SurfaceView mOurHolder.unlockCanvasAndPost(mCanvas); } }
We have two errors. One is that the Color
class needs importing. You can fix this in the usual way as described in the previous tip or add the next line of code manually.
Whichever method you choose, the following extra line needs to be added to the code at the top of the file:
import android.graphics.Color;
Let's deal with the other error.
The second error is the call to printDebuggingText
. The method doesn't exist yet. Let's add that now.
Add the code after the draw
method…
private void printDebuggingText(){ int debugSize = mFontSize / 2; int debugStart = 150; mPaint.setTextSize(debugSize); mCanvas.drawText("FPS: " + mFPS , 10, debugStart + debugSize, mPaint); }
The previous code uses the local variable debugSize
to hold a value that is half that of the member variable mFontSize
. This means that as mFontSize
(which is used for the HUD) is initialized dynamically based on the screen resolution, debugSize
will always be half that. The debugSize
variable is then used to set the size of the font before we start drawing the text. The debugStart
variable is just a guess at a good position vertically to start printing the debugging text.
These two values are then used to position a line of text on the screen that shows the current frames per second. As this method is called from draw
, which in turn will be called from the main game loop, this line of text will be constantly refreshed up to sixty times per second.
It is possible that on very high or very low-resolution screens you might need to change this value to something more appropriate for your screen. When we learn about the concept of viewports in the final project we will solve this ambiguity once and for all. This game is focussed on our first practical use of classes.
Let's explore those new lines of code in the draw
method and exactly how we can use SurfaceView
from which our PongGame
class is derived, to handle all our drawing requirements.
Starting in the middle of the method and working outwards for a change, we have a few familiar things like the calls to drawColor
, setTextSize
, and drawText
. We can also see the comment which indicates where we will eventually add code to draw the bat and the ball. These familiar method calls do the same thing they did in the previous project.
What is totally new however, is the code at the very start of the draw
method. Here it is again.
if (mOurHolder.getSurface().isValid()) { // Lock the canvas (graphics memory) ready to draw mCanvas = mOurHolder.lockCanvas();
… …
The if
statement contains a call to getSurface
and chains it with a call to isValid
. If this line returns true it confirms that the area of memory which we want to manipulate to represent our frame of drawing is available, the code continues inside the if
statement.
What goes on inside those methods (especially the first) is quite complex. They are necessary because all of our drawing and other processing (like moving the objects) will take place asynchronously with the code that detects the player input and listens for the operating system for messages. This wasn't an issue in the previous project because our code just sat there waiting for input, drew a single frame and then sat there waiting again.
Now we want to execute the code 60 times a second, we are going to need to confirm that we have access to the memory- before we access it.
This raises more questions about how does this code run asynchronously? That will be answered when we discuss threads shortly. For now, just know that the line of code checks if some other part of our code or Android itself is currently using the required portion of memory. If it is free, then the code inside the if
statement executes.
Furthermore, the first line of code to execute inside the if
statement calls lockCanvas
so that if another part of the code tries to access the memory while our code is accesing it, it won't be able to.
Then we do all our drawing.
Finally, in the draw
method, there is this next line (plus comments) right at the end.
// Display the drawing on screen // unlockCanvasAndPost is a method of SurfaceHolder mOurHolder.unlockCanvasAndPost(mCanvas);
The unlockCanvasAndPost
method sends our newly decorated Canvas
object (mCanvas
) for drawing to the screen and releases the lock so that other areas of code can use it again- albeit very briefly before the whole process starts again. This process happens every single frame of animation.
We now understand the code in the draw
method, however, we still don't have the mechanism which calls the draw
method over and over. In fact, we don't even call the draw
method once. We need to talk about the game loop and threads.
3.16.1.195