22. Breakout Revisited

In these notes you will learn about

  • Adding functionality to an existing class.
  • Using classes to reduce complexity in a program.
  • Mixing global variables, class variables, and local variables.
  • Determining the Size of an ArrayList.

22.1. Introduction

It is sometimes helpful to think of objects as organized in a hierarchy. For example, the Ball class we defined in the previous notes was a description of a gall that bounces around the screen, never falling off of the edge. If we wanted to have a ball with slightly different behaviour, say, bouncing off of a bat at the bottom of the screen, we can think of this new ball as a special kind of the general ball description. To demonstrate this, we will revisit the breakout program from assignment 1, this time using classes.

Since we now know how to use ArrayLists, we will also add bricks to our breakout game.

22.2. Planning the Program

As always, we start by thinking about what our program actually requires. We will need code for a ball and a bat. The ball will have to bounce around the screen, not falling of the edges, unless it hits the bottom and misses the bat.

Since we’re adding bricks, we will have to draw however many bricks we want on the screen, and write additional code for checking if the ball has hit one of the bricks and dealing with it. In assignment 2 you had to write code to determine whether a ball hit 3 rectangles. Imagine doing this for an arbitrary number of rectangles. That doesn’t sound very appealing!

The trick is to realize that we can transfer the responsibility of some of the hit detection to the bricks themselves. A brick object can simply check whether a point on the ball is in the object. So we should use classes to represent the bricks and the ball:

class Brick {

}

class BreakOutBall {

}

Notice that name of the class representing the ball is BreakOutBall.

We will also require global variables for the bat, for the state of the game, and for bricks:

class Brick {

}

class BreakOutBall {

}

float batX, batY; // bat (top-left) corner
float batW, batH; // bat width and height

boolean gameStarted; // has the game started?

ArrayList<Brick> bricks;
int totalNumBricks;

Notice the following:

  • Here we keep the usual information for the bat (x, y position, width and height).
  • We will use the gameStarted variable to decide whether to draw the ball in motion, or have it lying on the bat.
  • An ArrayList will hold all of the Brick objects that we will create.
  • The variable totalNumBricks will hold the total number of bricks that we would like to have on the screen.

We will also make use of the randomColor() function we wrote earlier to decide on colours for different objects in the game, so we’ll add that at the bottom of our source file.

22.3. Getting More out of the Ball Class

Like the Ball class of previous lectures, we want BreakOutBall to be responsible for drawing itself and bouncing around the screen. However, a ball in breakout should behave a little differently than what is defined in the Ball class. Specifically, it should only bounce off the bat when it is near the bottom. So we cannot simply use the Ball class.

One thing we could do is write an entirely new class definition for BreakOutBall, and then write for it a new set of variables x and y, diam its own render function, constructor etc. But this seems redundant once we realize that what we really want is to extend the functionality of the original Ball class. That is, we’d be happy if our BreakOutBall had some of the properties that a Ball has, but we want a little extra.

Luckily, we can do just that in processing using the extends keyword. For this to work, we need to have a source file containing the code for class Ball in the same directory as the source file we are currently working on.

Warning

make sure that the file you copy only contains the definition for the ball class. Otherwise, you might get complaints from Processing.

We then modify the class definition for BreakOutBall as follows:

// .. Brick class

class BreakOutBall extends ball {

}

// .. global variables defined here

What we are saying here is that the class BreakOutBall has all of the properties and capabilities of the class Ball. The class Ball is said to be a superclass of BreakOutBall, and the class BreakOutBall is said to be a subclass of Ball.

It is as if we made a copy of class Ball and renamed it BreakOutBall. So by writing this line an object of type BreakOutBall comes with its own x, y , diam, ballColor, and dx and dy variables. So we don’t need to define them.

If we wanted to declare and initialize an object from this class now, we could write something like this:

BreakOutBall b = new BreakOutBall();

However, if we wanted to pass in the ball’s location and diameter to the construction, we would have to write an additional constructor, as we did with the Ball class:

class BreakOutBall extends Ball {
    BreakOutBall(float initX, float initY, float initDiam) {
        x = initX;
        y = initY;
        diam = initDiam;
        ballColor = randomColor();
    }
}

Here our constructor behaves somewhat differently from the Ball constructor. Note that it doesn’t take in a color parameter, and sets the ballColor randomly.

All that is left to add now is function for the BreakOutBall to draw itself. The way we will do this is by overriding the method redner() that we defined in the Ball class:

class BreakOutBall extends Ball {
    BreakOutBall(float initX, float initY, float initDiam) {
        x = initX;
        y = initY;
        diam = initDiam;
        ballColor = randomColor();
    }

    // draw and animate this BreakOutBall
    // this overrides the render() function from class Ball
    void render() {
        fill(ballColor);
        noStroke();
        ellipse(x, y, diam, diam);

        x += dx;
        y += dy;

        // hit top?
        if (y - diam / 2 <= 0) {
            y = diam / 2;
            dy = -dy;
        }

        // hit bottom?
        if (y + diam / 2 >= batY) {
            if (batX <= x && x <= batX + batW) {
                y = batY  - diam / 2;
                dy = -dy;
                dx = map(x, batX, batX + batW, -1, 1);
            } else {
                gameStarted = false;
            }
        }

        // hit left?
        if (x - diam / 2 <= 0) {
            x = diam / 2;
            dx = -dx;
        }

        // hit right?
        if (x + diam / 2 >= width - 1) {
            x = width - 1 - diam / 2;
            dx = -dx;
        }
    }
}

Notice that we’ve only really changed the code for dealing with the case of the ball hitting the bat at the bottom.

22.4. Baking Bricks

We aught to put some thought into what attributes and behaviour we expect a brick in breakout to have:

  • The brick will have an (x, y) position.
  • It will have width and height.
  • It will have colour.
  • A breakout brick exists in one of two states: either it was hit by the ball, or it wasn’t.
  • As we discussed in the planning stage, our brick will be responsible for distinguishing between the two states.

With these requirements in mind, we can specify the class variables, as we as a constructor:

class Brick {

    // (x, y) position of the brick
    float x, y;

    // width and height
    float w, h;

    // color of this brick
    color brickColor;

    // this brick knows it was hit.
    boolean wasHit;

    Brick(float initX, float initY, float initWidth, float initHeight) {
        x = initX;
        y = initY;
        w = initWidth;
        h = initHeight;

        brickColor = randomColor();

        wasHit = false;
    }
}

As with the Ball class, each Brick object will draw itself using a render() function. However, a Brick will only draw itself to the screen if it wasn’t hit by the ball. After drawing itself, the brick should check whether it was hit:

class Brick {

    // class variables
    // and constructor

    void render() {
        if (!wasHit) {
            fill(brickColor);
            stroke(0);

            rect(x, y, w, h);

            //updating wasHit will be done here
        }
    }
}

Now there are several ways to get the brick to detect whether it was hit by the ball. We will use a simple four-point approach that relies on the direction that the ball is travelling in:

  • If ball.dy is negative, then the ball is going up. We should check whether the top of the ball has hit the bottom of the brick
  • On the other hand, if ball.dy is positive, then the ball is going down the screen. We should compare the bottom of the ball to the top of the brick.
  • By similar reasoning, if ball.dx is negative, we’ll check the ball’s left with the bricks right.
  • Symmetrically, if ball.dy is positive, we’ll check the ball’s right with the brick’s left.

Combining these four scenarios gives us a point on the circumference of the circle that we can check against the brick. Notice that since a brick is essentially a rectangle, we are checking whether a point is in a rectangle. So we’ll implement a function similar to the good old pointInRect. This time however, we don’t to do as much work:

class Brick {

    // class attributes
    // and constructor

    // render method

    boolean ballHitMe() {
        float ballX;
        float ballY;

        if (ball.dx < 0) {
            ballX = ball.x - ball.diam / 2;
        } else {
            ballX = ball.x + ball.diam / 2;
        }

        if (ball.dy < 0) {
            ballY = ball.y - ball.diam / 2;
        } else {
            ballY = ball.y + ball.diam / 2;
        }

        return pointInMyRect(ballX, ballY);
    }

    // Return true if the point (a, b) is in the rectangle defined
    // by this brick.
    boolean pointInMyRect(float a, float b) {
         return (x <= a && a <= x + w) && (y <= b && b <= y + h);
    }
}

Note

Notice that we are accessing the global variable ball inside the class Brick. Remember that global variables are so named because they can be accessed from anywhere in your program. The class variables on the other hand (x, y, etc) are only available inside the class. This why the BreakOutBall and Brick classes both have x and y variables without conflict.

The entire Brick class now looks like this:

class Brick {

    // (x, y) position of the brick
    float x, y;

    // width and height
    float w, h;

    // color of this brick
    color brickColor;

    // this brick knows it was hit.
    boolean wasHit;

    Brick(float initX, float initY, float initWidth, float initHeight) {
        this.x = initX;
        this.y = initY;
        this.w = initWidth;
        this.h = initHeight;

        brickColor = randomColor();

        this.wasHit = false;
    }

    void render() {
        if (!wasHit) {
            fill(brickColor);
            stroke(0);

            rect(this.x, this.y, this.w, this.h);

            // see if I was hit.
            wasHit = ballHitMe();
            if (wasHit) {
                ball.dx = -ball.dx;
                ball.dy = -ball.dy;
            }
        }

    }

    boolean ballHitMe() {
        float ballX;
        float ballY;

        if (ball.dx < 0) {
            ballX = ball.x - ball.diam / 2;
        } else {
            ballX = ball.x + ball.diam / 2;
        }

        if (ball.dy < 0) {
            ballY = ball.y - ball.diam / 2;
        } else {
            ballY = ball.y + ball.diam / 2;
        }

        return pointInMyRect(ballX, ballY);
    }

    boolean pointInMyRect(float a, float b) {
        return (x <= a && a <= x + w) && (y <= b && b <= y + h);
    }
}

22.5. Putting it All Together

Having defined the two main objects in our program, as well as the global variables we need. We initialize the program. We won’t put the bricks up just yet:

// ball class

// brick class

float batX, batY;
float batW, batH;

boolean gameStarted;
ArrayList<Brick> bricks;
int totalNumBricks;

void setup() {
    size(500, 500);
    gameStarted = false;

    ball = new BreakOutBall(0, 0, 30, color(255, 0, 0));
    batW = 100;
    batH = 15;
    batY = height - batH - 20;

    bricks = new ArrayList<Brick>();
    totalNumBricks = 10;
}

In setup, after initializing the screen and ensuring that gameStarted is false, we initialize the ball variable. Notice that when we cal the constructor we set the ball’s position at (0, 0). This is because the when the game hasn’t started, the ball’s position depends entirely on the bat, and we will update this in draw(). Then, we have bricks pointing at an empty ArrayList that will contain Brick objects. Finally we have no bricks up on the screen yet, and the total number of bricks we will have is 10.

The draw() function will need to draw the bat and get things going with the bricks. We will start the game when the user clicks a mouse button:

void draw() {

    background(255);

    // draw bat
    fill(color(0, 100, 0));
    noStroke();
    rect(batX, batY, batW, batH, 10); // give the bat rounded corners.

    // draw ball
    if (!gameStarted) {
        ball.x = batX + batW / 2;
        ball.y = batY - ball.diam / 2;

        if (bricks.size() < totalNumBricks) {
            bricks.add(new Brick((bricks.size() + 1)*40, 100, 40, 15));
        }
    }

    // tell the ball to draw.
    ball.render();

    // tell the bricks to draw
    for (Brick b : bricks) {
        b.render();
    }
}

void mouseClicked() {
    if (!gameStarted) {
        gameStarted = true;
        ball.dx = 0;
        ball.dy = -4;
    }
}

Note the following:

  1. Depending on the value of gameStarted, draw() will either have the ball rest on the bat, or move independently around the screen.

  2. If the game hasn’t started, the bricks are added to the screen one by one, as long as there are less than 10 bricks. The size() function of ArrayList returns the number of elements inside the ArrayList.

    Note

    There is a bug here. Can you find it?

  3. The draw function then tells the ball to render and, using a for-each loop, tells each brick to render.

22.6. Questions

  1. What error will Processing give you if you placed two .pde files in the same folder, both having a setup() function?

  2. What does the keyword extends mean?

  3. In the following definition, which is the superclass and which is the subclass?

    class RedBrick extends Brick {
    }
    
  4. Write a class definition for a PrettyString object that extends the String class.

22.7. Programming Questions

  1. Notice that the only difference between the render() method of class Ball and that of class BreakOutBall is the way the latter checks and deals with the ball when it is near the bottom of the screen. So we can change the structure of render() in Ball a little so that we don’t have to override it in BreakOutBall. Consider first the following version of render(), inside Ball:

    class Ball {
    
        // other code
    
        void render() {
            fill(ballColor);
            noStroke();
            ellipse(x, y, diam, diam);
    
            x += dx;
            y += dy;
    
            topHitCheck();
            bottomHitCheck();
            leftHitCheck();
            rightHitCheck();
        }
    
        void topHitCheck() {
            if (y - diam / 2 <= 0) {
                y = diam / 2;
                dy = -dy;
            }
        }
    
        void bottomHitCheck() {
            if (y + diam / 2 >= height - 1) {
                y = height - 1 - diam / 2;
                dy = -dy;
            }
        }
    
        void leftHitCheck() {
            if (x - diam / 2 <= 0) {
                x = diam / 2;
                dx = -dx;
            }
        }
    
        void rightHitCheck() {
            if (x + diam / 2 >= width - 1) {
                x = width - 1 - diam / 2;
                dx = -dx;
            }
        }
    }
    

Here, we’ve written the functionality for edge detection in four separate functions, topHitCheck(), bottomHitCheck(), leftHitCheck(), and rightHitCheck().

Rewrite the BreakOutBall class so that it doesn’t override the render() function, but instead only overrides the bottomHitCheck() function.

  1. Add functionality to the breakout program so that instead of one row of 10 bricks, there are two rows of 10 bricks each.

  2. Add functionality to the breakout program so that the game restarts if the user managed to hit all of the bricks.

  3. Our implementation of hit-detection in bricks leaves something to be desired. It misses a lot of cases, causing the ball to pass through bricks sometimes. The problem is that we are only ever checking a small number of points inside the ball to see whether any of them is in the brick. We can use loops to check if, among all points of the brick, at least one is in the ball, but that’s a little lumbering.

    1. Write code inside ballHitBrick() that declares two floats closestX and closestY and sets closestX to the x value of the point in the brick that is closest to the centre of the brick, and closestY to the y of that point.

      Hint: This can be done in two lines using no loops. The constrain function will come in handy here.

    2. Use the closest point you computed to decide whether the brick and the ball have a point in common.

22.8. The Breakout Program

class Brick {

    // (x, y) position of the brick
    float x, y;

    // width and height
    float w, h;

    // color of this brick
    color brickColor;

    // this brick knows it was hit.
    boolean wasHit;

    Brick(float initX, float initY, float initWidth, float initHeight) {
        this.x = initX;
        this.y = initY;
        this.w = initWidth;
        this.h = initHeight;

        brickColor = randomColor();

        this.wasHit = false;
    }

    void render() {
        if (!wasHit) {
            fill(brickColor);
            stroke(0);

            rect(this.x, this.y, this.w, this.h);

            // see if I was hit.
            wasHit = ballHitMe();
            if (wasHit) {
                ball.dx = -ball.dx;
                ball.dy = -ball.dy;
            }
        }

    }

    boolean ballHitMe() {
        float ballY;
        float ballX;

        if (ball.dx < 0) {
            ballX = ball.x - ball.diam / 2;
        } else {
            ballX = ball.x + ball.diam / 2;
        }

        if (ball.dy < 0) {
            ballY = ball.y - ball.diam / 2;
        } else {
            ballY = ball.y + ball.diam / 2;
        }

        return pointInMyRect(ballX, ballY);
    }

    boolean pointInMyRect(float a, float b) {
        return (x <= a && a <= x + w) && (y <= b && b <= y + h);
    }
}

class BreakOutBall extends Ball {

    // our own constructor
    BreakOutBall(float initX, float initY, float initDiam, color initColor) {
        x = initX;
        y = initY;
        diam = initDiam;
        ballColor = randomColor();
    }

    // draw this ball on the screen.
    void render() {
        fill(ballColor);
        noStroke();
        ellipse(x, y, diam, diam);

        x += dx;
        y += dy;

        // hit top?
        if (y - diam / 2 <= 0) {
            y = diam / 2;
            dy = -dy;
        }

        // hit bottom?
        if (y + diam / 2 >= batY) {
            if (batX <= x && x <= batX + batW) {
                y = batY  - diam / 2;
                dy = -dy;
                dx = map(x, batX, batX + batW, -1, 1);
            } else {
                gameStarted = false;
            }
        }

        // hit left?
        if (x - diam / 2 <= 0) {
            x = diam / 2;
            dx = -dx;
        }

        // hit right?
        if (x + diam / 2 >= width - 1) {
            x = width - 1 - diam / 2;
            dx = -dx;
        }
    }
}

float batX, batY;
float batW, batH;

boolean gameStarted;
ArrayList<Brick> bricks;
int totalNumBricks;

Ball ball;

void setup() {
    size(500, 500);
    background(255);
    gameStarted = false;

    ball = new BreakOutBall(0, 0, 30, color(255, 0, 0));
    batW = 100;
    batH = 15;
    batY = height - batH - 20;

    bricks = new ArrayList<Brick>();
    totalNumBricks = 10;
}

void draw() {
    background(255);

    if (mouseX + batW < width) {
        batX = mouseX;
    } else {
        batX = width - batW;
    }

    // draw bat
    fill(color(0, 200, 0));
    noStroke();
    rect(batX, batY, batW, batH, 10);

    // draw ball
    if (!gameStarted) {
        ball.x = batX + batW / 2;
        ball.y = batY - ball.diam / 2;

        if (bricks.size() < totalNumBricks) {
            bricks.add(new Brick((bricks.size() + 1)*40, 100, 40, 15));
        }
    }
    ball.render();

    for (Brick b : bricks) {
        b.render();
    }

 }

void mouseClicked() {
    if (!gameStarted) {
        gameStarted = true;
        ball.dx = 0;
        ball.dy = -4;
    }
}

color randomColor() {
    return color(random(0, 255), random(0, 255), random(0, 255));
}