Android Java Project

Coding a Breakout Game for Android

Breakout has a good claim to being the game that invented single-player gaming. When Atari released it in 1976, every arcade game before it required either a second player or a fixed sequence of enemies to defeat. Breakout dispensed with both. It was just you, a ball, a paddle, and a wall of bricks — and the skill was entirely in reading the angles and timing your moves. Steve Wozniak and Steve Jobs built the original hardware. Jobs took the credit; Wozniak did most of the work. Some things never change.

It's also a near-perfect learning project. The rules are simple enough to hold in your head, but implementing them properly introduces several important ideas: object-oriented class design, access specifiers, getters and setters, frame-rate-independent movement, and RectF-based collision detection. By the end of this tutorial you'll have a working, playable Breakout clone running on your Android device.

The project uses four Java files:

The finished Breakout game running on Android showing the blue background, orange bricks, white paddle, and ball in play.
The finished game — orange bricks, white paddle and ball, score and lives in the top-left.

Project Setup

Creating the Project

Open Android Studio and choose New Project → Empty Views Activity. Set the language to Java and the minimum SDK to API 21. Name the Activity BreakoutGame.

Configuring the Manifest

Open AndroidManifest.xml, find the <activity> element for BreakoutGame, and add two attributes:

<activity
    android:name=".BreakoutGame"
    android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
    android:screenOrientation="landscape"
    android:exported="true">
    ...
</activity>

Sound Assets

Right-click the main folder in the Project pane and choose New → Folder → Assets Folder. Drop these five .ogg files into app/src/main/assets — right-click each link to save:

Note: Theme.NoTitleBar.Fullscreen requires the activity to extend android.app.Activity, not AppCompatActivity. Check your class declaration if you get a theme-related crash.

The Game Engine Shell

This project has a slightly different structure from the Snake game. Instead of putting the game engine in a separate file, BreakoutView is an inner class of BreakoutGame. Both live in BreakoutGame.java. The benefit is that BreakoutView can directly access the Activity's getWindowManager() to read the screen resolution without needing it passed in as a parameter.

Here is the complete shell — the game loop architecture without any game objects yet. We'll fill in the empty update() and draw() methods as we go:

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class BreakoutGame extends Activity {

    BreakoutView breakoutView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        breakoutView = new BreakoutView(this);
        setContentView(breakoutView);
    }

    @Override
    protected void onResume() {
        super.onResume();
        breakoutView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        breakoutView.pause();
    }

    class BreakoutView extends SurfaceView implements Runnable {

        Thread gameThread = null;
        SurfaceHolder ourHolder;
        volatile boolean playing;
        boolean paused = true;

        Canvas canvas;
        Paint paint;

        long fps;
        private long timeThisFrame;

        // Screen dimensions — filled in the constructor
        int screenX;
        int screenY;

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

            // Read screen resolution from the Activity's window manager
            android.graphics.Point size = new android.graphics.Point();
            getWindowManager().getDefaultDisplay().getSize(size);
            screenX = size.x;
            screenY = size.y;
        }

        @Override
        public void run() {
            while (playing) {
                long startFrameTime = System.currentTimeMillis();

                if (!paused) {
                    update();
                }

                draw();

                timeThisFrame = System.currentTimeMillis() - startFrameTime;
                if (timeThisFrame >= 1) {
                    fps = 1000 / timeThisFrame;
                }
            }
        }

        public void update() {
            // Game logic goes here
        }

        public void draw() {
            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();
                canvas.drawColor(Color.argb(255, 26, 128, 182));
                paint.setColor(Color.argb(255, 255, 255, 255));
                // Drawing calls go here
                ourHolder.unlockCanvasAndPost(canvas);
            }
        }

        public void pause() {
            playing = false;
            try {
                gameThread.join();
            } catch (InterruptedException e) {
                Log.e("Error:", "joining thread");
            }
        }

        public void resume() {
            playing = true;
            gameThread = new Thread(this);
            gameThread.start();
        }

        @Override
        public boolean onTouchEvent(MotionEvent motionEvent) {
            switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            return true;
        }
    }
}

The game loop works the same way as the Snake project: resume() creates and starts a fresh thread; run() loops calling update() and draw(); pause() stops the loop and waits for the thread to finish with join(). The FPS is measured each frame and passed to game object update methods so movement stays consistent regardless of device speed. The game starts paused — the first screen touch will unpause it.

The Paddle Class

Create a new Java class: right-click the java folder in Android Studio's Project pane, choose New → Java Class, and name it Paddle. This creates Paddle.java as a peer of BreakoutGame.java.

import android.graphics.RectF;

public class Paddle {

    // RectF holds four float coordinates — perfect for a rectangle we can draw and collide with
    private RectF rect;

    private float length;
    private float height;
    private float x;
    private float y;
    private float paddleSpeed;

    // Direction constants — public so BreakoutView can refer to them by name
    public final int STOPPED = 0;
    public final int LEFT    = 1;
    public final int RIGHT   = 2;

    // Only this class controls the actual movement state
    private int paddleMoving = STOPPED;

    public Paddle(int screenX, int screenY) {
        length = 130;
        height = 20;

        // Start roughly centre-bottom of the screen
        x = screenX / 2;
        y = screenY - 20;

        rect = new RectF(x, y, x + length, y + height);

        paddleSpeed = 350;   // pixels per second
    }

    // Getter — lets BreakoutView read the rectangle for drawing and collision detection
    public RectF getRect() {
        return rect;
    }

    // Setter — lets BreakoutView change direction without touching the internals
    public void setMovementState(int state) {
        paddleMoving = state;
    }

    public void update(long fps) {
        if (paddleMoving == LEFT) {
            x -= paddleSpeed / fps;
        }
        if (paddleMoving == RIGHT) {
            x += paddleSpeed / fps;
        }
        rect.left  = x;
        rect.right = x + length;
    }
}

There's a deliberate design lesson built into this class. Most of the variables are private: rect, x, y, paddleSpeed, paddleMoving. Nothing outside Paddle can read or change them directly. Instead, the class exposes two controlled access points — getRect() and setMovementState() — that provide exactly the access other classes need and nothing more. This pattern, encapsulation, keeps each class responsible for its own behaviour and prevents one part of the code from accidentally breaking another.

The three direction constants (STOPPED, LEFT, RIGHT) are public final. They can't be changed, and they're accessible from BreakoutView as paddle.LEFT etc, making the intent obvious wherever they're used.

update(fps) moves the paddle by paddleSpeed / fps pixels per frame. If the frame rate is 60, the paddle moves 350 / 60 ≈ 5.8 pixels per frame, covering 350 pixels per second regardless of device speed.

Wiring the Paddle into BreakoutView

Add the following imports at the top of BreakoutGame.java, then make four additions to the BreakoutView inner class:

import android.graphics.Point;
import android.view.Display;

1. Declare the paddle — add after the screenY field:

Paddle paddle;

2. Instantiate it — add at the end of the BreakoutView constructor:

paddle = new Paddle(screenX, screenY);

3. Update it — add as the first line of update():

paddle.update(fps);

4. Draw it — add inside draw() after paint.setColor:

canvas.drawRect(paddle.getRect(), paint);

5. Handle touch input — replace the empty onTouchEvent with this:

@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
    switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            paused = false;   // First touch starts the game
            if (motionEvent.getX() > screenX / 2) {
                paddle.setMovementState(paddle.RIGHT);
            } else {
                paddle.setMovementState(paddle.LEFT);
            }
            break;
        case MotionEvent.ACTION_UP:
            paddle.setMovementState(paddle.STOPPED);
            break;
    }
    return true;
}

Touching the right half of the screen sets the paddle moving right; touching the left half moves it left; lifting the finger stops it. The first touch also sets paused = false, which triggers the if (!paused) check in run() and starts game updates.

The Breakout game running with a white paddle visible at the bottom of the blue screen.
After adding the paddle — tap to unpause and hold left or right to steer it.

The Ball Class

Create another new Java class named Ball. The ball needs to move freely in two dimensions, bounce off surfaces, reset to its starting position, and expose methods that let the game engine handle the physics responses it can't detect itself:

import android.graphics.RectF;
import java.util.Random;

public class Ball {

    RectF rect;

    float xVelocity;
    float yVelocity;
    float ballWidth  = 10;
    float ballHeight = 10;

    public Ball(int screenX, int screenY) {
        xVelocity = 200;
        yVelocity = -400;   // Negative = moving upward
        rect = new RectF();
    }

    public RectF getRect() {
        return rect;
    }

    public void update(long fps) {
        rect.left   = rect.left + (xVelocity / fps);
        rect.top    = rect.top  + (yVelocity / fps);
        rect.right  = rect.left + ballWidth;
        rect.bottom = rect.top  + ballHeight;
    }

    public void reverseYVelocity() {
        yVelocity = -yVelocity;
    }

    public void reverseXVelocity() {
        xVelocity = -xVelocity;
    }

    // After hitting the paddle, randomly keep or flip horizontal direction
    // so the ball doesn't always bounce the same way
    public void setRandomXVelocity() {
        Random generator = new Random();
        if (generator.nextInt(2) == 0) {
            reverseXVelocity();
        }
    }

    // Unstick the ball from a horizontal obstacle (paddle, floor, ceiling)
    public void clearObstacleY(float y) {
        rect.bottom = y;
        rect.top    = y - ballHeight;
    }

    // Unstick the ball from a vertical obstacle (left/right walls)
    public void clearObstacleX(float x) {
        rect.left  = x;
        rect.right = x + ballWidth;
    }

    // Place the ball just above the paddle in the centre of the screen
    public void reset(int screenX, int screenY) {
        rect.left   = screenX / 2f;
        rect.top    = screenY - 20;
        rect.right  = rect.left + ballWidth;
        rect.bottom = rect.top  + ballHeight;
    }
}

The ball's velocity is split into separate X and Y components. Bouncing off a horizontal surface (bricks, paddle, floor, ceiling) reverses yVelocity. Bouncing off a vertical surface (left and right walls) reverses xVelocity. The game engine decides when to bounce; the ball class just provides the methods to do it.

clearObstacleY and clearObstacleX solve a common problem in simple collision detection: the ball can overlap an object by several pixels before the intersection is detected. If you simply reverse velocity without moving the ball out of the object, the next frame detects another collision and reverses it back — the ball gets stuck oscillating. Calling clearObstacleY immediately repositions the ball flush against the surface, preventing the trap.

setRandomXVelocity randomly flips horizontal direction when the ball hits the paddle. This produces varied bounce angles without any trigonometry, giving the game a livelier feel than a perfectly predictable bounce.

Wiring the Ball into BreakoutView

Add a Ball ball; field alongside Paddle paddle;, then initialise it in the constructor:

ball = new Ball(screenX, screenY);

Add ball.update(fps); in update() after the paddle update, and draw it in draw():

canvas.drawRect(ball.getRect(), paint);

We also need a method to reset the game — both at startup and after the player clears the screen or runs out of lives. Add this inside BreakoutView, right after the constructor:

public void createBricksAndRestart() {
    ball.reset(screenX, screenY);
}

Call it as the very last line of the constructor: createBricksAndRestart();. Run the game now and you'll see the ball fly upward and off the top of the screen. That's expected — we haven't added boundary checks yet.

The Brick Class

Create a new Java class named Brick. A brick needs three things: a rectangle (position and size), a visibility flag, and a handful of accessor methods:

import android.graphics.RectF;

public class Brick {

    private RectF    rect;
    private boolean  isVisible;

    public Brick(int row, int column, int width, int height) {
        isVisible = true;
        int padding = 1;
        rect = new RectF(
                column * width  + padding,
                row    * height + padding,
                column * width  + width  - padding,
                row    * height + height - padding);
    }

    public RectF getRect() {
        return rect;
    }

    public void setInvisible() {
        isVisible = false;
    }

    public boolean getVisibility() {
        return isVisible;
    }
}

Each brick's position is calculated directly from its row and column index, multiplied by the brick's width and height. A one-pixel padding on all sides creates a thin gap between bricks so they read as individual objects rather than a solid wall.

Building the Brick Wall

Back in BreakoutView, add these fields alongside the other declarations:

Brick[] bricks = new Brick[200];
int numBricks = 0;

Then expand createBricksAndRestart() to build the wall after resetting the ball:

public void createBricksAndRestart() {
    ball.reset(screenX, screenY);

    int brickWidth  = screenX / 8;
    int brickHeight = screenY / 10;

    numBricks = 0;
    for (int column = 0; column < 8; column++) {
        for (int row = 0; row < 3; row++) {
            bricks[numBricks] = new Brick(row, column, brickWidth, brickHeight);
            numBricks++;
        }
    }

    if (lives == 0) {
        score = 0;
        lives = 3;
    }
}

This creates 24 bricks in an 8-column × 3-row grid, each sized as a fraction of the screen so the layout works on any device. The if (lives == 0) block resets score and lives when called after a game over — but leaves them intact when called between waves, so clearing the screen doesn't wipe the player's score.

Add the draw call inside draw(), switching colour to orange before the bricks:

// Draw bricks in orange
paint.setColor(Color.argb(255, 249, 129, 0));
for (int i = 0; i < numBricks; i++) {
    if (bricks[i].getVisibility()) {
        canvas.drawRect(bricks[i].getRect(), paint);
    }
}
// Switch back to white for everything else
paint.setColor(Color.argb(255, 255, 255, 255));

Sound and Scoring

Add the following imports to BreakoutGame.java:

import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.SoundPool;
import java.io.IOException;

Add these fields to BreakoutView:

SoundPool soundPool;
int beep1ID    = -1;
int beep2ID    = -1;
int beep3ID    = -1;
int loseLifeID = -1;
int explodeID  = -1;

int score = 0;
int lives = 3;

Load the sounds in the BreakoutView constructor, before the call to createBricksAndRestart():

soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
try {
    AssetManager assetManager = context.getAssets();
    AssetFileDescriptor descriptor;

    descriptor  = assetManager.openFd("beep1.ogg");
    beep1ID     = soundPool.load(descriptor, 0);

    descriptor  = assetManager.openFd("beep2.ogg");
    beep2ID     = soundPool.load(descriptor, 0);

    descriptor  = assetManager.openFd("beep3.ogg");
    beep3ID     = soundPool.load(descriptor, 0);

    descriptor  = assetManager.openFd("loseLife.ogg");
    loseLifeID  = soundPool.load(descriptor, 0);

    descriptor  = assetManager.openFd("explode.ogg");
    explodeID   = soundPool.load(descriptor, 0);

} catch (IOException e) {
    Log.e("error", "failed to load sound files");
}

Add the HUD to the end of draw(), before unlockCanvasAndPost:

paint.setTextSize(40);
canvas.drawText("Score: " + score + "   Lives: " + lives, 10, 50, paint);

if (score == numBricks * 10) {
    paint.setTextSize(90);
    canvas.drawText("YOU HAVE WON!", 10, screenY / 2, paint);
}
if (lives <= 0) {
    paint.setTextSize(90);
    canvas.drawText("YOU HAVE LOST!", 10, screenY / 2, paint);
}

Collision Detection

All collision logic goes inside update(), after the ball and paddle updates. Work through it in sections:

Ball vs Bricks

for (int i = 0; i < numBricks; i++) {
    if (bricks[i].getVisibility()) {
        if (RectF.intersects(bricks[i].getRect(), ball.getRect())) {
            bricks[i].setInvisible();
            ball.reverseYVelocity();
            score += 10;
            soundPool.play(explodeID, 1, 1, 0, 0, 1);
        }
    }
}

RectF.intersects() is a static method — you call it on the class itself rather than on any particular object, because it's a utility that acts on two rectangles rather than belonging to either one. When an overlap is detected the brick disappears, the ball reverses vertical direction, and the score increases by 10.

Ball vs Paddle

if (RectF.intersects(paddle.getRect(), ball.getRect())) {
    ball.setRandomXVelocity();
    ball.reverseYVelocity();
    ball.clearObstacleY(paddle.getRect().top - 2);
    soundPool.play(beep1ID, 1, 1, 0, 0, 1);
}

On a paddle hit, horizontal direction is randomised (to vary the bounce angle), vertical velocity is reversed, and clearObstacleY nudges the ball two pixels above the paddle top to prevent it embedding and bouncing repeatedly.

Ball vs Walls and Floor

// Ball missed the paddle — hits the floor
if (ball.getRect().bottom > screenY) {
    ball.reverseYVelocity();
    ball.clearObstacleY(screenY - 2);
    lives--;
    soundPool.play(loseLifeID, 1, 1, 0, 0, 1);
    if (lives == 0) {
        paused = true;
        createBricksAndRestart();
    }
}

// Top wall
if (ball.getRect().top < 0) {
    ball.reverseYVelocity();
    ball.clearObstacleY(12);   // 12px clears the ball height from the top
    soundPool.play(beep2ID, 1, 1, 0, 0, 1);
}

// Left wall
if (ball.getRect().left < 0) {
    ball.reverseXVelocity();
    ball.clearObstacleX(2);
    soundPool.play(beep3ID, 1, 1, 0, 0, 1);
}

// Right wall
if (ball.getRect().right > screenX - 10) {
    ball.reverseXVelocity();
    ball.clearObstacleX(screenX - 22);
    soundPool.play(beep3ID, 1, 1, 0, 0, 1);
}

// All bricks cleared
if (score == numBricks * 10) {
    paused = true;
    createBricksAndRestart();
}

Losing all three lives calls createBricksAndRestart() — the if (lives == 0) check inside that method resets the score and life count before rebuilding the wall. Clearing all bricks also calls it but with lives still greater than zero, so the score carries forward.

The Breakout game mid-play with several bricks destroyed, ball in the air, and paddle visible at the bottom.
Mid-game — bricks falling, score ticking up, ball in flight.

The Complete Listing

Here is the entire BreakoutGame.java in one place. Paddle.java, Ball.java, and Brick.java are as shown in their individual sections above.

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.graphics.RectF;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;

public class BreakoutGame extends Activity {

    BreakoutView breakoutView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        breakoutView = new BreakoutView(this);
        setContentView(breakoutView);
    }

    @Override
    protected void onResume() {
        super.onResume();
        breakoutView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        breakoutView.pause();
    }

    class BreakoutView extends SurfaceView implements Runnable {

        Thread gameThread = null;
        SurfaceHolder ourHolder;
        volatile boolean playing;
        boolean paused = true;

        Canvas canvas;
        Paint paint;

        long fps;
        private long timeThisFrame;

        int screenX;
        int screenY;

        Paddle    paddle;
        Ball      ball;
        Brick[]   bricks    = new Brick[200];
        int       numBricks = 0;

        SoundPool soundPool;
        int beep1ID    = -1;
        int beep2ID    = -1;
        int beep3ID    = -1;
        int loseLifeID = -1;
        int explodeID  = -1;

        int score = 0;
        int lives = 3;

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

            Display display = getWindowManager().getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);
            screenX = size.x;
            screenY = size.y;

            paddle = new Paddle(screenX, screenY);
            ball   = new Ball(screenX, screenY);

            soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
            try {
                AssetManager assetManager = context.getAssets();
                AssetFileDescriptor descriptor;

                descriptor  = assetManager.openFd("beep1.ogg");
                beep1ID     = soundPool.load(descriptor, 0);
                descriptor  = assetManager.openFd("beep2.ogg");
                beep2ID     = soundPool.load(descriptor, 0);
                descriptor  = assetManager.openFd("beep3.ogg");
                beep3ID     = soundPool.load(descriptor, 0);
                descriptor  = assetManager.openFd("loseLife.ogg");
                loseLifeID  = soundPool.load(descriptor, 0);
                descriptor  = assetManager.openFd("explode.ogg");
                explodeID   = soundPool.load(descriptor, 0);
            } catch (IOException e) {
                Log.e("error", "failed to load sound files");
            }

            createBricksAndRestart();
        }

        public void createBricksAndRestart() {
            ball.reset(screenX, screenY);

            int brickWidth  = screenX / 8;
            int brickHeight = screenY / 10;

            numBricks = 0;
            for (int column = 0; column < 8; column++) {
                for (int row = 0; row < 3; row++) {
                    bricks[numBricks] = new Brick(row, column, brickWidth, brickHeight);
                    numBricks++;
                }
            }

            if (lives == 0) {
                score = 0;
                lives = 3;
            }
        }

        @Override
        public void run() {
            while (playing) {
                long startFrameTime = System.currentTimeMillis();
                if (!paused) {
                    update();
                }
                draw();
                timeThisFrame = System.currentTimeMillis() - startFrameTime;
                if (timeThisFrame >= 1) {
                    fps = 1000 / timeThisFrame;
                }
            }
        }

        public void update() {
            paddle.update(fps);
            ball.update(fps);

            // Ball vs bricks
            for (int i = 0; i < numBricks; i++) {
                if (bricks[i].getVisibility()) {
                    if (RectF.intersects(bricks[i].getRect(), ball.getRect())) {
                        bricks[i].setInvisible();
                        ball.reverseYVelocity();
                        score += 10;
                        soundPool.play(explodeID, 1, 1, 0, 0, 1);
                    }
                }
            }

            // Ball vs paddle
            if (RectF.intersects(paddle.getRect(), ball.getRect())) {
                ball.setRandomXVelocity();
                ball.reverseYVelocity();
                ball.clearObstacleY(paddle.getRect().top - 2);
                soundPool.play(beep1ID, 1, 1, 0, 0, 1);
            }

            // Floor
            if (ball.getRect().bottom > screenY) {
                ball.reverseYVelocity();
                ball.clearObstacleY(screenY - 2);
                lives--;
                soundPool.play(loseLifeID, 1, 1, 0, 0, 1);
                if (lives == 0) {
                    paused = true;
                    createBricksAndRestart();
                }
            }

            // Ceiling
            if (ball.getRect().top < 0) {
                ball.reverseYVelocity();
                ball.clearObstacleY(12);
                soundPool.play(beep2ID, 1, 1, 0, 0, 1);
            }

            // Left wall
            if (ball.getRect().left < 0) {
                ball.reverseXVelocity();
                ball.clearObstacleX(2);
                soundPool.play(beep3ID, 1, 1, 0, 0, 1);
            }

            // Right wall
            if (ball.getRect().right > screenX - 10) {
                ball.reverseXVelocity();
                ball.clearObstacleX(screenX - 22);
                soundPool.play(beep3ID, 1, 1, 0, 0, 1);
            }

            // All bricks cleared
            if (score == numBricks * 10) {
                paused = true;
                createBricksAndRestart();
            }
        }

        public void draw() {
            if (ourHolder.getSurface().isValid()) {
                canvas = ourHolder.lockCanvas();

                canvas.drawColor(Color.argb(255, 26, 128, 182));
                paint.setColor(Color.argb(255, 255, 255, 255));

                canvas.drawRect(paddle.getRect(), paint);
                canvas.drawRect(ball.getRect(),   paint);

                paint.setColor(Color.argb(255, 249, 129, 0));
                for (int i = 0; i < numBricks; i++) {
                    if (bricks[i].getVisibility()) {
                        canvas.drawRect(bricks[i].getRect(), paint);
                    }
                }

                paint.setColor(Color.argb(255, 255, 255, 255));
                paint.setTextSize(40);
                canvas.drawText("Score: " + score + "   Lives: " + lives, 10, 50, paint);

                if (score == numBricks * 10) {
                    paint.setTextSize(90);
                    canvas.drawText("YOU HAVE WON!", 10, screenY / 2, paint);
                }
                if (lives <= 0) {
                    paint.setTextSize(90);
                    canvas.drawText("YOU HAVE LOST!", 10, screenY / 2, paint);
                }

                ourHolder.unlockCanvasAndPost(canvas);
            }
        }

        public void pause() {
            playing = false;
            try {
                gameThread.join();
            } catch (InterruptedException e) {
                Log.e("Error:", "joining thread");
            }
        }

        public void resume() {
            playing = true;
            gameThread = new Thread(this);
            gameThread.start();
        }

        @Override
        public boolean onTouchEvent(MotionEvent motionEvent) {
            switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    paused = false;
                    if (motionEvent.getX() > screenX / 2) {
                        paddle.setMovementState(paddle.RIGHT);
                    } else {
                        paddle.setMovementState(paddle.LEFT);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    paddle.setMovementState(paddle.STOPPED);
                    break;
            }
            return true;
        }
    }
}

Common Issues

Theme crash on launch. The activity must extend android.app.Activity, not AppCompatActivity. Change the class declaration if needed.

Blank screen, game never starts. Check that onResume() calls breakoutView.resume(). Without it the thread never starts.

Sound doesn't play. Confirm the five .ogg files are in app/src/main/assets (not res/raw) with exactly matching filenames including case.

Ball flies through bricks without colliding. The collision loop uses i < numBricks as its condition. If numBricks is 0 (e.g. createBricksAndRestart() wasn't called), no collisions will be tested. Check the constructor calls it last.

Ball gets stuck bouncing against the paddle. The clearObstacleY(paddle.getRect().top - 2) call should prevent this. If it persists, try increasing the offset to - 5.

Where to Take It Next