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:
- BreakoutGame.java — the Activity, plus an inner class
BreakoutViewthat drives the whole game loop - Paddle.java — the player-controlled bat
- Ball.java — the bouncing ball
- Brick.java — a single destructible brick
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:
beep1.ogg— paddle hitbeep2.ogg— top wall bouncebeep3.ogg— side wall bounceloseLife.ogg— ball missedexplode.ogg— brick destroyed
Note:
Theme.NoTitleBar.Fullscreenrequires the activity to extendandroid.app.Activity, notAppCompatActivity. 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 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 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
- More brick rows. Change the loop to
row < 6or more. Thescore == numBricks * 10win condition automatically accounts for however many bricks you add. - Increasing ball speed. Start
yVelocityat-400and nudge it up by 10% each time the screen is cleared — the game gets progressively harder without any other changes. - Brick colour by row. Pass the
rownumber toBrickand store it, then use it in the draw loop to pick a colour per row — classic Breakout used red at the top, through orange, yellow, green. - Paddle width shrinks with lives. Reduce
Paddle.lengtheach time the player loses a life, making the game harder as it progresses. - High score with SharedPreferences. The Snake project shows the pattern — save the high score in
pause()and load it in the constructor.