If you are lucky and NOT old enough to remember Pong then you should take a look at it and find out some more before continuing. We will be programming the further simplified, single-player, Squash-style version of the game. The player must bounce the ball off the “back” wall and he gets a point each time he manages to hit it. He will lose a “life” each time he misses it. To get started, create a new project called Pong, use the “Empty Activity” template and leave all the other settings as they are.
The image above shows what we are aiming for in this project. Everything you need can be copied & pasted from the pages that make up this tutorial.
Programming the Ball class
Create a new class called Ball. You can do this by right-clicking on the folder that contains the MainActivity class and selecting New Class… As the name suggests, this class will handle everything to do with the ball. We will code this class in a few short segments and explain things as we go.
The Ball member variables
First, add some variables. An RectF object is an object which holds four <float values that define the four coordinates of a rectangle. This is perfect for a ball that looks like a square. We also have four float variables to represent the speed of the ball in the horizontal(x) and vertical(y) axes as well as the width and height of the ball. Note that in this project I am using the convention of prefixing a m to the front of all the member variables of a class so they will be more distinct from all the local variables of the methods of the class.
Add the member variables to the Ball class.
public class Ball { private RectF mRect; private float mXVelocity; private float mYVelocity; private float mBallWidth; private float mBallHeight; }
The Ball constructor
In the Ball constructor method, the code initializes the width and height relative to the width of the current device’s screen which was passed in as a parameter. Then we initialize the horizontal and vertical speed relative to the height in pixels of the current device’s screen. The ball will be one-hundredth of the screen width and will travel at one-quarter of the screen height every second.
Note that this is a little bit different from what we did in the Snake project. We will see how we control this shortly.
Next, we call
new on our
RectF object but note that we don’t actually initialize any of its four member variables just yet.
Add the
Ball constructor method is shown below.
public Ball(int screenX, int screenY){ // Make the mBall size relative to the screen resolution mBallWidth = screenX / 100; mBallHeight = mBallWidth; /* Start the ball travelling straight up at a quarter of the screen height per second */ mYVelocity = screenY / 4; mXVelocity = mYVelocity; // Initialize the Rect that represents the mBall mRect = new RectF(); }
More Ball methods
Next, we will code a public method so we can get hold of the RectF (which is the graphical representation of the ball) from outside the Ball class. Go ahead and add the getRect method to the Ball class.
// Give access to the Rect public RectF getRect(){ return mRect; }
Next, we will code the update method. Note that the update method in the Ball class is distinct from the update method that we will write in our thread in the PongView class. Although the latter will call the former. This update method will be called once every frame of the game. It updates the top and left values of the ball based on the velocity member variables ( mXVelocity and mYVelocity) divided by the number of frames per second ( fps) that the device is managing to run the game.
We have yet to see how the frames per second are calculated but by moving the ball relative to the frames per second, it will move at a consistent speed regardless of how powerful(or puny) the device’s processor is.
After this the other points of mRect are updated relative to the top-left and the size of the ball.
Add the code for the update method to the Ball class.
// Change the position each frame public void update(long fps){ mRect.left = mRect.left + (mXVelocity / fps); mRect.top = mRect.top + (mYVelocity / fps); mRect.right = mRect.left + mBallWidth; mRect.bottom = mRect.top - mBallHeight; }
Next, we need a few more methods that will enable us to easily deal with various events that will occur throughout the game. At various times in the game, we need to be able to do the following:
- Reverse the vertical direction
- Reverse the horizontal direction
- Set a new random x velocity
- Speed up the ball
The reverseYVelocity, reverseXVelocity, setRandomXVelocity and increaseVelocity methods achieve these things by applying simple mathematics.
Add the four methods we have just discussed to the Ball class.
// Reverse the vertical heading public void reverseYVelocity(){ mYVelocity = -mYVelocity; } // Reverse the horizontal heading public void reverseXVelocity(){ mXVelocity = -mXVelocity; } public void setRandomXVelocity(){ // Generate a random number either 0 or 1 Random generator = new Random(); int answer = generator.nextInt(2); if(answer == 0){ reverseXVelocity(); } } // Speed up by 10% // A score of over 20 is quite difficult // Reduce or increase 10 to make this easier or harder public void increaseVelocity(){ mXVelocity = mXVelocity + mXVelocity / 10; mYVelocity = mYVelocity + mYVelocity / 10; }
Next, we will add some more methods that do the following things.
One which clears an obstacle on the vertical axis (
clearObstacleY)
Another which clears an obstacle on the horizontal axis (
clearObstacleX)
And one which resets the position of the ball in the bottom center of the screen (
reset).
Each of these methods adjusts/repositions the ball. Their usefulness will become apparent when we see them in action when we code the PongView class in a minute.
public void clearObstacleY(float y){ mRect.bottom = y; mRect.top = y - mBallHeight; } public void clearObstacleX(float x){ mRect.left = x; mRect.right = x + mBallWidth; } public void reset(int x, int y){ mRect.left = x / 2; mRect.top = y - 20; mRect.right = x / 2 + mBallWidth; mRect.bottom = y - 20 - mBallHeight; }
Now our ball can start bouncing. Let’s get the bat coded and then we can code the game engine.
Programming the Bat class
The class has a RectF for holding the bat’s four coordinates. We also have separate mXcoord and mYCoord floating point variables that hold the left and top positions.
We have a float variable for the speed ( mBatSpeed). Next, we have three final int members ( STOPPED, LEFT and RIGHT) which are public. We will be able to refer to these values from outside of the class to manipulate the bat’s direction.
We also have a private variable ( mBatMoving) which will always be assigned one of those three public final values. We begin by setting it to STOPPED. In the Bat class, we want to keep a permanent copy of the screen resolution (size in pixels) so we declare mScreenX and mScreenY which we will initialize soon.
Add the Bat member variables that we have just discussed.
public class Bat { // RectF is an object that holds four coordinates - just what we need private RectF mRect; // How long and high our mBat will be private float mLength; private float mHeight; // X is the far left of the rectangle which forms our mBat private float mXCoord; // Y is the top coordinate private float mYCoord; // This will hold the pixels per second speed that // the mBat will move private float mBatSpeed; // Which ways can the mBat move public final int STOPPED = 0; public final int LEFT = 1; public final int RIGHT = 2; // Is the mBat moving and in which direction private int mBatMoving = STOPPED; // The screen length and width in pixels private int mScreenX; private int mScreenY; }
Coding the Bat constructor
Now, we will add the constructor method. In the constructor method, we initialize mScreenX and mScreenY with the passed-in x and y values. We initialize the length of the bat to one-eighth of the screen width and the height to one-twenty-fifth.
Next, the code initializes mXCoord and mYCoord to roughly the bottom center of the screen. We set mBatSpeed it to the same value as mScreenX, which has the effect of setting the bat’s movement to be able to cover the entire screen in one second. This is not as fast as it might first seem. Of course, you can always come back here and make it slower (or even faster) if you want to.
Add the code for the Bat constructor.
// This is the constructor method // When we create an object from this class we will pass // in the screen width and mHeight public Bat(int x, int y){ mScreenX = x; mScreenY = y; // 1/8 screen width wide mLength = mScreenX / 8; // 1/25 screen mHeight high mHeight = mScreenY / 25; // Start mBat in roughly the sceen centre mXCoord = mScreenX / 2; mYCoord = mScreenY - 20; mRect = new RectF(mXCoord, mYCoord, mXCoord + mLength, mYCoord + mHeight); // How fast is the mBat in pixels per second mBatSpeed = mScreenX; // Cover entire screen in 1 second }
More Bat methods
Add the public method to return the RectF instance that represents the bat’s location.
// This is a getter method to make the rectangle that // defines our bat available in PongView class public RectF getRect(){ return mRect; }
The setMovementState method receives a int value as a parameter. We will call this method using one of the three public final int members, LEFT, RIGHT or STOPPED. This method will simply set that state to the mBatMoving member.
// This method will be used to change/set if the mBat is going // left, right or nowhere public void setMovementState(int state){ mBatMoving = state; }
The final method for the Bat class is its update method. First, it uses a couple of if statements to see if it is moving left or right. If it is, it moves the mXCoord by mBatSpeed divided by the current number frames per second ( fps), just like the Ball class did.
Next, it does two checks to see if the bat might be moving off the screen. If the bat is about to disappear off the left it prevents it by setting mXCoord to . If it is about to disappear off to the right it sets mXCoord to mScreenX, less the width of the bat.
Finally, based on the results of all those if statements the code updates the values held by the RectF ready for the game engine to make use of them when it calls getRect.
// This update method will be called from update in PongView // It determines if the Bat needs to move and changes the coordinates // contained in mRect if necessary public void update(long fps){ if(BatMoving == LEFT){ mXCoord = mXCoord - mBatSpeed / fps; } if(BatMoving == RIGHT){ mXCoord = mXCoord + mBatSpeed / fps; } // Make sure it's not leaving screen if(mRect.left < 0){ mXCoord = 0; } if(mRect.right > mScreenX){ mXCoord = mScreenX - // The width of the Bat (mRect.right - mRect.left); } // Update the Bat graphics mRect.left = mXCoord; mRect.right = mXCoord + mLength; }
Programming the MainActivity class
Most of the action will take place in the next class that we create. We will call that class PongView. So the job of MainActivity is to communicate with the events of the OS and pass on any relevant information to PongView. It also needs to create the PongView object for us. Notice in the next code there is indeed an object of the type PongView declared as a member. Obviously, this will show as an error because we haven’t coded it yet.
In the onCreate method, we use an Display object and the getWindowManager().getDefaultDisplay() chained methods to initialize it. Then we declare an object of the type Point. Using the Display object we can load the screen resolution into point using the getSize method.
We can now call the constructor of PongView to initialize pongView. Notice when we do that we pass in x and y, which is the horizontal and vertical screen resolution. It is from this constructor method in PongView that our Bat and Ball objects will eventually get hold of these values.
Finally, in onCreate we pass in our PongView reference as the argument to setContentView. Whatever we draw in PongView will be displayed as the visuals for the app. This is exactly what we need. PongView will extend SurfaceView from the Android API which not only allows us to have a Thread instance, it also implements onTouchListener allows us to attach an Canvas object to draw all the graphics.
Go ahead and add the variables and the new code to the onCreate method.
// pongView will be the view of the game // It will also hold the logic of the game // and respond to screen touches as well PongView pongView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Get a Display object to access screen details Display display = getWindowManager().getDefaultDisplay(); // Load the resolution into a Point object Point size = new Point(); display.getSize(size); // Initialize pongView and set it as the view pongView = new PongView(this, size.x, size.y); setContentView(pongView); }
We are almost finished with the
MainActivity class. In
MainActivity we will override the
onResume and
onPause methods. In these methods, we will call the
resume and
pause methods on our
PongView instance. In those methods, in the
PongView class, we will start and stop the thread which controls the game. This is just what we need as after our
PongView class is setup, as it’s constructor is called in
onCreate, then
onResume will run and set the thread going as well. Then when the player quits the app and the OS calls
onPause our pause method will be called and the thread will be stopped. Otherwise, our ball will still be bouncing and around the screen and beeping, perhaps while the player is receiving a call from his girlfriend.
Add the overridden
onResume and
onPause methods to the
MainActivity class.
// This method executes when the player starts the game @Override protected void onResume() { super.onResume(); // Tell the pongView resume method to execute pongView.resume(); } // This method executes when the player quits the game @Override protected void onPause() { super.onPause(); // Tell the pongView pause method to execute pongView.pause(); }
Now we can code the most important class of our game.
Programming the SurfaceView (PongView)
Create a new class called
PongView,
extend SurfaceView and
implement Runnable. I show you this code after we have discussed it a bit more.
We will need quite a few member variables. Let’s discuss them in turn:
- A Thread called mGameThread that we will start and stop from the pause and resume methods that we will implement soon. These methods are of course called by the onResume and onPause methods of the MainActivity class
- SurfaceHolder is what we need to allow us to do to our drawing
- Volatile boolean mPlaying will be true when the thread is running and false otherwise. It will be used to determine whether we enter a while loop that will control the whole game loop. The volatile the keyword is used because mPlaying can be accessed from outside and inside the thread.
- We have a boolean mPaused which will determine whether the game is currently paused.
- We have a Paint and an Canvas object which we will use to draw and draw on respectively.
- Next, we have a long variable mFPS which will hold the current number of frames per second that our game loop is achieving and of course, this is the value we will be passing into the update methods of Bat and Ball to allow them to move by the correct amount.
- Next, we declare mScreenX and mScreenY to hold the screen resolution which as we saw, is passed into the constructor from MainActivity when we instantiate an PongView object. We will code that constructor very soon.
- Now we get to the fun bits; a Ball object mBall and a Bat object mBat.
- Next up we have all the members that will take care of sound FX including a SoundPool and four int identifiers for sound FX.
- Finally mLives and mScore will keep track of the player’s score and how many lives they have left
Code the PongView class and its members as we have just discussed.
class PongView extends SurfaceView implements Runnable { // This is our thread Thread mGameThread = null; // We need a SurfaceHolder object // We will see it in action in the draw method soon. SurfaceHolder mOurHolder; // A boolean which we will set and unset // when the game is running- or not // It is volatile because it is accessed from inside and outside the thread volatile boolean mPlaying; // Game is mPaused at the start boolean mPaused = true; // A Canvas and a Paint object Canvas mCanvas; Paint mPaint; // This variable tracks the game frame rate long mFPS; // The size of the screen in pixels int mScreenX; int mScreenY; // The players mBat Bat mBat; // A mBall Ball mBall; // For sound FX SoundPool sp; int beep1ID = -1; int beep2ID = -1; int beep3ID = -1; int loseLifeID = -1; // The mScore int mScore = 0; // Lives int mLives = 3; }
Let’s discuss the constructor method for PongView.
We initialize mScreenX and mScreenY from the passed-in screen resolution. Then we initialize mOurHolder by calling getHolder and mPaint by calling the default Paint constructor. Next, we instantiate our Bat and Ball by calling their constructors and passing in the screen resolution as is required by the method signatures.
Almost all the rest of the code sets up the sound effects ready to be played. I have gone through the trouble in this code to detect the version of Android and use the appropriate code depending upon the version. The final line of code calls the setupAndRestart method to start a new game and we will code that method shortly.
Add the PongView constructor code.
/* When the we call new() on pongView This custom constructor runs */ public PongView(Context context, int x, int y) { /* The next line of code asks the SurfaceView class to set up our object. */ super(context); // Set the screen width and height mScreenX = x; mScreenY = y; // Initialize mOurHolder and mPaint objects mOurHolder = getHolder(); mPaint = new Paint(); // A new mBat mBat = new Bat(mScreenX, mScreenY); // Create a mBall mBall = new Ball(mScreenX, mScreenY); /* Instantiate our sound pool dependent upon which version of Android is present */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); sp = new SoundPool.Builder() .setMaxStreams(5) .setAudioAttributes(audioAttributes) .build(); } else { sp = new SoundPool(5, AudioManager.STREAM_MUSIC, 0); } try{ // Create objects of the 2 required classes AssetManager assetManager = context.getAssets(); AssetFileDescriptor descriptor; // Load our fx in memory ready for use descriptor = assetManager.openFd("beep1.ogg"); beep1ID = sp.load(descriptor, 0); descriptor = assetManager.openFd("beep2.ogg"); beep2ID = sp.load(descriptor, 0); descriptor = assetManager.openFd("beep3.ogg"); beep3ID = sp.load(descriptor, 0); descriptor = assetManager.openFd("loseLife.ogg"); loseLifeID = sp.load(descriptor, 0); descriptor = assetManager.openFd("explode.ogg"); explodeID = sp.load(descriptor, 0); }catch(IOException e){ // Print an error message to the console Log.e("error", "failed to load sound files"); } setupAndRestart(); }
Here is the
setupAndRestart method that we first call from the constructor. We will also call this method at the end of every game to start a new one. The code calls the
reset method on the
Ball instance to position it for the start of a game and if necessary, resets the
mScore and
mLives variables to
and
3 respectively.
Add the
setupAndRestart method to the
PongView class.
public void setupAndRestart(){ // Put the mBall back to the start mBall.reset(mScreenX, mScreenY); // if game over reset scores and mLives if(mLives == 0) { mScore = 0; mLives = 3; } }
Here we have the run method which is the code that is running in a thread. We have a while loop controlled by the value of our volatile boolean called mPlaying. This while loop wraps all the rest of the code inside the run method.
Inside the while loop, we get the system time in milliseconds (thousandths of a second) and initialize the startFrameTime variable with the result. Then we check if the game is currently paused if(!mPaused) and if the game isn’t paused we call the update method. Note this is the update method of the PongView class, not the Ball or Bat classes’ update methods. We will code this method soon.
Next, we call the draw method which will contain all our drawing code. Now we calculate the time the frame took to execute by getting the current system time again and subtracting startFrameTime from the result. We then put the result into mFPS which of course will be passed on to the update methods of the Ball and Bat classes when they are called. The reason we wrap the last bit of code in if (timeThisFrame >= 1) is because if timeThisFrame equals zero, trying to divide by zero will crash the game.
Code the overridden run method. @Override public void run() { while (mPlaying) { // Capture the current time in milliseconds in startFrameTime long startFrameTime = System.currentTimeMillis(); // Update the frame // Update the frame if(!mPaused){ update(); } // Draw the frame draw(); /* Calculate the FPS this frame We can then use the result to time animations in the update methods. */ long timeThisFrame = System.currentTimeMillis() - startFrameTime; if (timeThisFrame >= 1) { mFPS = 1000 / timeThisFrame; } } }
Programming the update method
The update method is quite long so we will go through and code it one piece at a time. Add the signature and the body of the update method and we will steadily add all the code to it.
// Everything that needs to be updated goes in here // Movement, collision detection etc. public void update(){ }
First, the code calls the update methods on our Ball and our Bat instances to handle any required movement.
// Everything that needs to be updated goes in here // Movement, collision detection etc. public void update() { // Move the mBat if required mBat.update(mFPS); mBall.update(mFPS); }
Now the ball and the bat are in their new positions for this frame, we can run some tests to see if anything we need to respond to has happened. The first test is to see if the ball has hit the bat. Using the getRect methods of both the Ball and Bat we pass the two returned results into the static intersects method of RectF. The intersects method returns true if the ball and the bat intersect/overlap (have collided) each other. If a collision is detected, execution enters the if block and does a number of things:
- Call setRandomXVelocity on the ball to choose a random horizontal direction for when the ball heads back up the screen
- Call reverseYVelocity to start to head back up the screen
- call clearObstacle which jumps the ball a few pixels and avoids the possibility of the ball getting stuck on the bat
- Increment mScore to increase the player’s score
- Play a beep sound from the SoundPool
Add the code we have just discussed to the update method.
// Check for mBall colliding with mBat if(RectF.intersects(mBat.getRect(), mBall.getRect())) { mBall.setRandomXVelocity(); mBall.reverseYVelocity(); mBall.clearObstacleY(mBat.getRect().top - 2); mScore++; mBall.increaseVelocity(); sp.play(beep1ID, 1, 1, 0, 0, 1); }
Next, we handle what happens if the ball hits the bottom of the screen. The test to see if this has happened works by calculating the position of the underside of the ball ( mBall.getRect.bottom) and comparing it to the height of the screen in pixels ( mScreenY). If the collision has occurred the following steps happen inside the if block.
- Reverse the velocity of the ball
- Jump a few pixels in case the ball gets stuck
- decrement mLives
- play a losing sound
- Check if that was the last life and if it is, then pause the game and call setupAndRestart
Add the code we have just discussed to the update method.
// Bounce the mBall back when it hits the bottom of screen if(mBall.getRect().bottom > mScreenY){ mBall.reverseYVelocity(); mBall.clearObstacleY(mScreenY - 2); // Lose a life mLives--; sp.play(loseLifeID, 1, 1, 0, 0, 1); if(mLives == 0){ mPaused = true; setupAndRestart(); } }
The next block of code uses the top of the ball and compares it to zero to see if it has reached the top of the screen. If it has it just reverses the ball on the vertical axis, clears any potential obstacles, and plays a beep.
Add the new code to the update method.
// Bounce the mBall back when it hits the top of screen if(mBall.getRect().top < 0){ mBall.reverseYVelocity(); mBall.clearObstacleY(12); sp.play(beep2ID, 1, 1, 0, 0, 1); }
The next block of code uses the left of the ball and compares it to zero to see if it has reached the left of the screen. If it has it reverses the ball on the horizontal axis, clears any obstacles and plays a beep.
Add the new code to the update method.
// If the mBall hits left wall bounce if(mBall.getRect().left < 0){ mBall.reverseXVelocity(); mBall.clearObstacleX(2); sp.play(beep3ID, 1, 1, 0, 0, 1); }
The next code block uses the right of the ball and compares it to mScreenX and sees if it has reached the right-hand side of the screen. If it has it just reverses the ball on the horizontal axis, clears any obstacles and as usual, plays a beep.
Add the new code we have just discussed to the update method.
// If the mBall hits right wall bounce if(mBall.getRect().right > mScreenX){ mBall.reverseXVelocity(); mBall.clearObstacleX(mScreenX - 22); sp.play(beep3ID, 1, 1, 0, 0, 1); } }
Programming the draw method
The first thing we have to do is attempt to get a lock on the surface to draw on and check it is valid. This is achieved with the following line of code. (don’t add it just yet)
// Make sure our drawing surface is valid or we crash if (mOurHolder.getSurface().isValid()) { // Draw everything here }
If the test returns true we are almost ready to draw. We just need this code before we start using our canvas (don’t add this yet).
// Lock the mCanvas ready to draw mCanvas = mOurHolder.lockCanvas();
Now we can get busy and actually draw things with mPaint and mCanvas. This is the order we will do things in the draw method:
- Make sure the surface is valid and lock the canvas as previously discussed
- Draw a background color
- Change the brush color
- Draw the bat as a rectangle by passing in getRect as the argument
- Draw the ball as a rectangle by calling getRect as the argument
- Change the brush color again
- Change the size of the text
- Draw the score and number of lives on the screen
- Call mOurHolder.unlockCanvasAndPost(mCanvas) to finish the drawing process for this frame
Add the draw method we have just discussed to the PongView class.
// Draw the newly updated scene public void draw() { // Make sure our drawing surface is valid or we crash if (mOurHolder.getSurface().isValid()) { // Draw everything here // Lock the mCanvas ready to draw mCanvas = mOurHolder.lockCanvas(); // Clear the screen with my favorite color mCanvas.drawColor(Color.argb(255, 120, 197, 87)); // Choose the brush color for drawing mPaint.setColor(Color.argb(255, 255, 255, 255)); // Draw the mBat mCanvas.drawRect(mBat.getRect(), mPaint); // Draw the mBall mCanvas.drawRect(mBall.getRect(), mPaint); // Change the drawing color to white mPaint.setColor(Color.argb(255, 255, 255, 255)); // Draw the mScore mPaint.setTextSize(40); mCanvas.drawText("Score: " + mScore + " Lives: " + mLives, 10, 50, mPaint); // Draw everything to the screen mOurHolder.unlockCanvasAndPost(mCanvas); } }
Now we can code our pause and resume methods that stop and start the thread. As we have already discussed, these methods are called by the MainActivity class in response to the Android OS.
// If the Activity is paused/stopped // shutdown our thread. public void pause() { mPlaying = false; try { mGameThread.join(); } catch (InterruptedException e) { Log.e("Error:", "joining thread"); } } // If the Activity starts/restarts // start our thread. public void resume() { mPlaying = true; mGameThread = new Thread(this); mGameThread.start(); }
Adding touch controls
One of the last important pieces of our Pong game is handling the user’s touches. To make the controls as easy as possible we will say that holding anywhere on the right will move the Bat right and anywhere on the left will move left.
When the onTouchEvent method is called, we switch based on the type of event. The first case that we handle is MotionEvent.ACTION_DOWN. This occurs when the player touches the screen. We can access the precise location with the motionEvent.getX method. Therefore in the code that follows we use the following if statement:
if(motionEvent.getX() > mScreenX / 2){ }
This detects if the screen has been touched at a position further to the right than the width of the screen divided by two (the right-hand side). If the above is true we simply call Bat.setMovementState(mBat.RIGHT) and the Bat class will take care of moving correctly the next time update is called. If the previous if statement is false then it must have been touched on the left and we call Bat.setMovementState(mBat.LEFT).
We also need to remember to stop the bat if the player removes their finger from the screen. We handle this in the
MotionEvent.ACTION_UP case of the switch block.
Add the overridden
onTouchEvent method.
// The SurfaceView class implements onTouchListener // So we can override this method and detect screen touches. @Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { // Player has touched the screen case MotionEvent.ACTION_DOWN: mPaused = false; // Is the touch on the right or left? if(motionEvent.getX() > mScreenX / 2){ mBat.setMovementState(mBat.RIGHT); } else{ mBat.setMovementState(mBat.LEFT); } break; // Player has removed finger from screen case MotionEvent.ACTION_UP: mBat.setMovementState(mBat.STOPPED); break; } return true; }
If you are wondering why we set
mPaused to
false in the
MotionEvent.ACTION_DOWN case it is because we pause the game when the player runs out of lives. When the player taps the screen this will then have the effect of starting it again.
We are nearly there now. You could actually run the game. 1972 here I come! Before you get you those flared trousers out and start playing there are two more quick things to do.
Adding the sound files to the project
The only assets we need for this project are some sound effects. Using your operating go to the “app/src/main” folder of the project, add a new folder, and name it “assets”. There are four sound files that we need and they are linked below.
Place these files into the “assets” directory you just made.
Lock the screen orientation and make full-screen
We can achieve this by editing the AndroidManifest.xml file as shown next.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.gamecodeschool.pong" > <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="landscape" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
And we’re ready to play!
Nice… Thanks for the tutorial!!
This didnt work for me. I tried to run the app and it just immediately crashes everytime.
Post the errors/messages from Visual Studio and I will try and help.
Mine did too in with this error “You need to use a Theme.AppCompat theme (or descendant) with this activity”. I figured out that to use some of the methods and classes in mainActivity, the intellisense had automatically extended AppCompatActivity on main. That meant I needed to chance the theme from “Theme.NoTitleBar.Fullscreen” to “@style/AppTheme”. Resource: https://stackoverflow.com/questions/39604889/how-to-fix-you-need-to-use-a-theme-appcompat-theme-or-descendant-with-this-a/39604946
It crashes for me as well, here is the Logcat
11-19 22:12:25.466 25803-25803/com.example.myapplication W/ResourcesManager: Resource getTopLevelResources for package com.example.myapplicationoverlayDirs =Null
11-19 22:12:25.476 25803-25803/com.example.myapplication W/System: ClassLoader referenced unknown path: /data/app/com.example.myapplication-1/lib/arm
11-19 22:12:25.476 25803-25803/com.example.myapplication D/ContextRelationManager: ContextRelationManager() : FEATURE_ENABLED=true
11-19 22:12:25.616 25803-25813/com.example.myapplication W/art: Suspending all threads took: 10.830ms
11-19 22:12:25.646 25803-25813/com.example.myapplication I/art: Background sticky concurrent mark sweep GC freed 38754(1876KB) AllocSpace objects, 0(0B) LOS objects, 91% free, 1044KB/12MB, paused 11.727ms total 75.300ms
11-19 22:12:25.736 25803-25803/com.example.myapplication W/art: Before Android 4.1, method android.graphics.PorterDuffColorFilter androidx.vectordrawable.graphics.drawable.VectorDrawableCompat.updateTintFilter(android.graphics.PorterDuffColorFilter, android.content.res.ColorStateList, android.graphics.PorterDuff$Mode) would have incorrectly overridden the package-private method in android.graphics.drawable.Drawable
11-19 22:12:25.806 25803-25826/com.example.myapplication V/MediaPlayer: decode(24, 51000, 12374)
11-19 22:12:25.816 25803-25803/com.example.myapplication E/error: failed to load sound files
11-19 22:12:25.816 25803-25803/com.example.myapplication D/AndroidRuntime: Shutting down VM
11-19 22:12:25.826 25803-25803/com.example.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.myapplication, PID: 25803
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.myapplication/com.example.myapplication.MainActivity}: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3322)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3418)
at android.app.ActivityThread.access$1100(ActivityThread.java:231)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1823)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:7422)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:555)
at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:518)
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:457)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:145)
at com.example.myapplication.MainActivity.onCreate(MainActivity.java:29)
at android.app.Activity.performCreate(Activity.java:6904)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1136)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3269)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3418)
at android.app.ActivityThread.access$1100(ActivityThread.java:231)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1823)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:7422)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
11-19 22:12:25.896 25803-25826/com.example.myapplication V/MediaPlayer: decode(27, 2628, 10188)
11-19 22:12:25.906 25803-25826/com.example.myapplication V/MediaPlayer: decode(29, 12868, 13270)
Is your Activity extending AppCompatActivity? It needs to extend plain old Activity.
Great article, it worked fine for me! Thanks
John, it does not work. There is no ball((
Here are some things to try.
Check the ball related parts:
Declaration:
// A mBall
Ball mBall;
Initialisation:
// Create a mBall
mBall = new Ball(mScreenX, mScreenY);
Update:
mBall.update(mFPS);
Draw:
// Draw the mBall
mCanvas.drawRect(mBall.getRect(), mPaint);
Try changing the size of the ball to make it huge and see if you can see it off screen.
Try using logging to output the coordinates of the ball in the ball.update method each frame and see if the values are reasonable screen coordinates.
Log.d(“ball x”, “” + mRect.left);// and simmilar for top etc
Mixa4y,
See my comment.
Hi, nice tutorial. I am a beginner with android games and it helped a lot. My problem: After coding all the classes and statements and let it run I have no bats. And the square is not bumping back from the top of the screen but running along the top to the left or right side. Can somebody help me?
Many thanks
Andreas
The bat is probably off- screen because of your device having more pixels than the app is showing. Adjust the y/vertical position of the bat by subtracting some pixels. The problem with running across the top might be solved by changing the code thatbounces the ball back by moving the ball down the screen a couple of extra pixels.
Hi John – first of all many thanks for your reply and trying to help me. I dealed with many values to let the ball bounce back from top, left or bottom. But as of now I did not find the right solution. Only on the right side the ball is bouncing back very smoothy. But here I did not change anything from your source code.
I figured out I have to change the value for the clearObstacleX() method – is that right? But I did not find the right values for my devices.
I even was not sucessful with the bat. Sometimes I can see it but then the behavior of sliding it to left or right is very strange. Sometimes it slides out of the right screen and it will never comming back!
Could it be it has all to do with the right screen aspect ratio? And would it be possible to set the right value due to the existing screen resolution?
I tried already like this
screenRatioX = 1920f / mScreenX;
screenRatioY = 1080f / mScreenY;
But as of now I was not very sucessful with that.
I tried the game with my mobile phone Samsung J5 which has a resolution of 720 x 1280 and also with my Samsung Tab S5e which has a resolution of 2560 x 1600. On both devices it does not work.
Can you help again?
Hi John,
for the bat I found a solution by doing this changings in the bat constructor:
mXCoord = (mScreenX / 2) – (mLength /2);
mYCoord = mScreenY – (mHeight * 6);
I am starting to think that something fundamental has changed in the Canvas API. I think I might need to work through all my projects and find out what has changed. If the results you are getting are common (we will see from comments soon I guess) then this is probably the case. I haven’t done anything on Android for about a year now and I probbably need to get back into it. Loads of people have had misiing bats over the years this tutorial has been out (usually because it was drawn just off-screen) and the handling and bouncing is very basic (for the sake of simplicity) but I am not sure what else is causing these issues. Sorry to not give you a solution. I can’t work at the moment and have already had to turn down a couple of books because the UK is in a Covid 19 lock down and I have a house full of people home-schooling/working from home and it is impossible to get anything significant done. I will redo this when I get a chance, however.
Hi, i have written full code exactly but it keeps crashing please help
Can you tell me the details. Errors etc.
Thank you for your tutorial.
I found some typos (e.g. the boolean “BatMoving” seems to mean “mBatMoving”?!)
Still good stuff. It is completely normal that some stuff is outdated.
Also there is this confusing programming language war going on between Google/Android and Oracle.
The official language of Android is now Kotlin (ugly name ^^’ ) even though Java is also official accepted.
For iOS (Apple) it’s the same. They used c# for a while but now it’s “Swift”.
Sometimes they change basic stuff so fast that even expert programmers are confused or annoyed.
So do not be frustrated anyone. Grab the basic stuff and adapt it. Code will always be outdated at some time point. Especially in programming for mobile phones this is within 2 or 3 years and can happen from one moment to the next with the “new update”. E.g. stuff that worked for Android 10 is outdated for Android 11 already especially regarding storage access. Read this in 2 years and you might laugh at “Android 10″ ^^’
For this reason there is no “perfect tutorial” as companies always have used “obsolesce” of any kind (hardware/software) as a money making tool oO
Life Saver!
the ball isn’t bumping back from the top but running along the top to the left or right side and I also want to change its orientation from horizontal to vertical.
I have just read your book and liked it, thanks.
There were two problems in addition to the one already mentioned. One bug is with the update method which uses mFPS, but is not ready at the first time it is called before it is actually calculated.
The second is just a visual bug which causes the ball bounce right and left when it is hitting the edge in a sharp angle. I fixed these, that was a good test to see if I understand the subject.
Hello. This is my first java and first Android project. Your tutorials have been extremely helpful, thank you. I want to modify your Android Pong game to become a clock with floating digits. I removed the Bat class & its references. The ball played by itself just fine. I increased the size of the Ball. (I plan to draw time digits on the ball rectangle later.)
The problem: the larger ball (mBallWidth = screenX / 30;) gets stuck on the left wall the first time it hits it. I don’t understand why. The xVelocity does get reversed but the ball is half off the left side, even though the left x of ball is positive. It travels up and down but never leaves the left side. Would you please help? I would appreciate even a hint on what is going on.
Thank you.