6
\$\begingroup\$

I’ve written a basic implementation of Breakout using Java 8 & Slick (~400 lines of code). Please let me know of any design/OOP improvements that can be made (any improvements in general are welcome, but I’m specifically looking for design & OOP improvements).

Game - the main class

package breakout;

import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame;

public class Game extends StateBasedGame{
    public static final String gameName = "Breakout!";
    public static final int play = 0;
    public static final int gameWon = 1;
    public static final int gameOver = 2;

    public static final int FRAME_HEIGHT = 500;
    public static final int FRAME_WIDTH = 640;

    public Game(String gameName){
        super(gameName);
        UserInfo userInfo = new UserInfo();
        this.addState(new Play(userInfo));
        this.addState(new GameWon(userInfo));
        this.addState(new GameOver(userInfo));
    }

    @Override
    public void initStatesList(GameContainer gc) {
        try {
            this.getState(play).init(gc, this);
            this.enterState(play);
        }catch(SlickException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // a game container that displays the game as a stand alone application
        AppGameContainer appGC;
        try{
            appGC = new AppGameContainer(new Game(gameName), FRAME_WIDTH, FRAME_HEIGHT, false);
            appGC.setVSync(true);   // sets FPS to screen's refresh rate
            appGC.start();
        }catch(SlickException e){
            e.printStackTrace();
        }
    }
}

Ball class - sets the position and deals with collisions with bricks

package breakout;

import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;

public class Ball implements Collision {
    private Image image;
    private int ballDiameter;

    private int positionX = 450;
    private int positionY = 250;
    private int velocityX;
    private int velocityY;


    public Ball(String imageLocation) throws SlickException{
        float scalingFactor = 0.06f;
        image = new Image(imageLocation);
        image = image.getScaledCopy((int) (scalingFactor * image.getWidth()), (int) (scalingFactor * image.getHeight()));
        ballDiameter = image.getWidth();

        velocityX = -3;
        velocityY = 3;
    }

    public void move(){
        positionX += velocityX;
        positionY += velocityY;
        collideWithVerticalWall();
        collideWithHorizontalWall();
    }

    public void collide(Brick brick){
        int tolerance = 3;
        if(!brick.isDestroyed()) {
            if (Collision.topSide(this, brick, tolerance) || Collision.bottomSide(this, brick, tolerance)) {
                flipVelocityY();
                brick.changeImage();
            }

            if (Collision.leftSide(this, brick, tolerance) || Collision.rightSide(this, brick, tolerance)) {
                flipVelocityX();
                brick.changeImage();
            }
        }
    }

    private void collideWithHorizontalWall(){
        if(positionY <= 0){     // ignore this comment: || positionY >= Game.FRAME_HEIGHT - ballDiameter
            flipVelocityY();
        }
    }

    private void collideWithVerticalWall(){
        if(positionX <= 0 || positionX >= Game.FRAME_WIDTH - ballDiameter){
            flipVelocityX();
        }
    }

    public void flipVelocityX(){
        velocityX = -velocityX;
    }

    public void flipVelocityY(){
        velocityY = -velocityY;
    }

    // GETTERS
    @Override
    public int getHeight(){
        return ballDiameter;
    }

    public Image getImage(){
        return image;
    }

    @Override
    public int getPositionX(){
        return positionX;
    }

    @Override
    public int getPositionY(){
        return positionY;
    }

    @Override
    public int getWidth(){
        return ballDiameter;
    }

}

Brick class - contains the images and etc for each brick

package breakout;

import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;

import java.util.stream.IntStream;

public class Brick implements Collision {
    public static final String[] imageLocations = {"res/brick_pink.png", "res/brick_pink_cracked.png",
            "res/brick_transparent.png"};
    public static final int[] points = {0, 10, 20};
    public static final int pointsPerBrick = IntStream.of(points).sum();

    private Image[] images;
    private int imageIndex = 0;
    private int brickHeight;
    private int brickWidth;
    private int positionX;
    private int positionY;

    private UserInfo userInfo;


    public Brick(String[] imageLocations, UserInfo userInfo, int positionX, int positionY) throws SlickException{
        float scalingFactor = 0.25f;
        images = new Image[imageLocations.length];

        for(int i = 0; i < imageLocations.length; i++) {
            images[i] = new Image(imageLocations[i]);
            images[i] = images[i].getScaledCopy((int) (scalingFactor * images[i].getWidth()),
                    (int) (scalingFactor * images[i].getHeight()));
        }

        brickHeight = images[imageIndex].getHeight();
        brickWidth = images[imageIndex].getWidth();

        this.userInfo = userInfo;
        this.positionX = positionX;
        this.positionY = positionY;
    }


    public void changeImage(){
        if(imageIndex < images.length - 1) {
            imageIndex++;
            brickHeight = images[imageIndex].getHeight();
            brickWidth = images[imageIndex].getWidth();
            userInfo.incrementScore(points[imageIndex]);
        }
    }

    public boolean isDestroyed(){
        return imageIndex == images.length - 1;
    }


    // GETTERS
    @Override
    public int getHeight(){
        return brickHeight;
    }

    public Image getImage(){
        return images[imageIndex];
    }

    @Override
    public int getPositionX(){
        return positionX;
    }

    @Override
    public int getPositionY(){
        return positionY;
    }

    @Override
    public int getWidth(){
        return brickWidth;
    }
}

Collision interface - interface that determines whether objects collide (contains default methods)

package breakout;

// Deals with the collisions in this game. Any class that implements this interface have objects that are "Collidable"
// and can therefore use these methods.
public interface Collision {
    // TODO need to neaten these conditions up. Not sure how.
    static boolean bottomSide(Collision self, Collision other, int tolerance){
        // tolerance is included as the ball's velocity is 3, so the ball may not exactly touch the paddle when it
        // reaches it because it's moving in increments of 3.
        return other.getPositionX() + other.getWidth() >= self.getPositionX()
                && other.getPositionX() <= self.getPositionX() + self.getWidth()
                && other.getPositionY() <= self.getPositionY() + self.getHeight()
                && other.getPositionY() >= self.getPositionY() + self.getHeight() - tolerance;
    }

    static boolean leftSide(Collision self, Collision other, int tolerance){
        return other.getPositionY() + other.getHeight() >= self.getPositionY()
                && other.getPositionY() <= self.getPositionY() + self.getHeight()
                && other.getPositionX() + other.getWidth() >= self.getPositionX()
                && other.getPositionX() + other.getWidth() <= self.getPositionX() + tolerance;
    }

    static boolean rightSide(Collision self, Collision other, int tolerance){
        return other.getPositionY() + other.getHeight() >= self.getPositionY()
                && other.getPositionY() <= self.getPositionY() + self.getHeight()
                && other.getPositionX() <= self.getPositionX() + self.getWidth()
                && other.getPositionX() >= self.getPositionX() + self.getWidth() - tolerance;
    }

    static boolean topSide(Collision self, Collision other, int tolerance){
        return other.getPositionX() + other.getWidth() >= self.getPositionX()
                && other.getPositionX() <= self.getPositionX() + self.getWidth()
                && other.getPositionY() + other.getHeight() >= self.getPositionY()
                && other.getPositionY() + other.getHeight() <= self.getPositionY() + tolerance;
    }


    // GETTERS
    int getPositionX();

    int getPositionY();

    int getWidth();

    int getHeight();
}

GameWon class - game state that tells the user that they've completed the level

package breakout;

import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;

public class GameWon extends BasicGameState {
    private UserInfo userInfo;

    public GameWon(UserInfo userInfo){
        this.userInfo = userInfo;
    }

    @Override
    public int getID(){
        return Game.gameWon;
    }

    @Override
    public void init(GameContainer gc, StateBasedGame sbg){

    }

    @Override
    public void render(GameContainer gc, StateBasedGame sbg, Graphics g) {
        try{
            Image backgroundImage = new Image("res/background.jpg");
            g.drawImage(backgroundImage, 0, 0);
            g.drawString("Well done, you completed the game! Your score: " + userInfo.getScore(), 100, 100);
        }catch(SlickException e){
            e.printStackTrace();
        }
    }

    @Override
    public void update(GameContainer gc, StateBasedGame sbg, int delta){

    }
}

GameOver class - game state that tells the user their score after not being able to complete the level

package breakout;

import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;

public class GameOver extends BasicGameState {
    private UserInfo userInfo;

    public GameOver(UserInfo userInfo){
        this.userInfo = userInfo;
    }

    @Override
    public int getID(){
        return Game.gameOver;
    }

    @Override
    public void init(GameContainer gc, StateBasedGame sbg){

    }

    @Override
    public void render(GameContainer gc, StateBasedGame sbg, Graphics g) {
        try{
            Image backgroundImage = new Image("res/background.jpg");
            g.drawImage(backgroundImage, 0, 0);
            g.drawString("Game over. Your score: " + userInfo.getScore(), 150, 100);
        }catch(SlickException e){
            e.printStackTrace();
        }
    }

    @Override
    public void update(GameContainer gc, StateBasedGame sbg, int delta){

    }
}

Paddle class - contains all the methods and etc to do with the paddle

package breakout;

import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;

public class Paddle implements Collision {
    private Image image;
    private int paddleHeight;
    private int paddleWidth;

    private int positionX;
    private int positionY;
    private int velocity = 3;


    public Paddle(String imageLocation) throws SlickException{
        double scalingFactor = 0.2;
        this.image = new Image(imageLocation);
        image = image.getScaledCopy((int) (scalingFactor * image.getWidth()), (int) (scalingFactor * image.getHeight()));
        paddleHeight = image.getHeight();
        paddleWidth = image.getWidth();
        positionX = (Game.FRAME_WIDTH - image.getWidth()) / 2;
        positionY = Game.FRAME_HEIGHT - image.getHeight();
    }


    public void collide(Ball ball){
        int tolerance = 3;
        // collide with top side
        if(Collision.topSide(this, ball, tolerance) || Collision.bottomSide(this, ball, tolerance)){
            ball.flipVelocityY();
        }
        // collide with left or right side
        if(Collision.leftSide(this, ball, tolerance) || Collision.rightSide(this, ball, tolerance)){
            ball.flipVelocityX();
        }
    }

    public void move(Input input){
        // Prevents paddle from moving outside of the frame
        if(getPositionX() > 0) {
            moveLeft(input);
        }
        if (getPositionX() < Game.FRAME_WIDTH - paddleWidth){
            moveRight(input);
        }
    }

    private void moveLeft(Input input){
        if(input.isKeyDown(Input.KEY_LEFT)){
            adjustPositionX(-velocity);
        }
    }

    private void moveRight(Input input){
        if(input.isKeyDown(Input.KEY_RIGHT)){
            adjustPositionX(velocity);
        }
    }


    // GETTERS
    @Override
    public int getHeight(){
        return paddleHeight;
    }

    public Image getImage(){
        return image;
    }

    @Override
    public int getPositionX(){
        return positionX;
    }

    @Override
    public int getPositionY(){
        return positionY;
    }

    @Override
    public int getWidth(){
        return paddleWidth;
    }


    // SETTERS
    public void adjustPositionX(float delta){
        positionX += delta;
    }
}

Play class - game state when the user is playing the level

package breakout;

import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;


public class Play extends BasicGameState{
    private Image backgroundImage;
    private UserInfo userInfo;
    private Paddle paddle;
    private Ball ball;
    private Brick[] bricks;
    private int numBricks = 3;

    public Play(UserInfo userInfo){
        this.userInfo = userInfo;
    }


    @Override
    public int getID(){
        return Game.play;
    }

    @Override
    public void init(GameContainer gc, StateBasedGame sbg){
        bricks = new Brick[numBricks];

        try{
            backgroundImage = new Image("res/background.jpg");
            paddle = new Paddle("res/bat_yellow.png");
            ball = new Ball("res/ball_red.png");

            for(int i = 0; i < bricks.length; i++) {
                // the maths here is used to position the bricks in rows of 10
                bricks[i] = new Brick(Brick.imageLocations, userInfo, (i % 10) * 60 + 20, ((i / 10) + 1) * 40);
            }
        } catch(SlickException e){
            e.printStackTrace();
        }
    }

    @Override
    public void render(GameContainer gc, StateBasedGame sbg, Graphics g){
        g.drawImage(backgroundImage, 0, 0);
        g.drawImage(paddle.getImage(), paddle.getPositionX(), paddle.getPositionY());
        g.drawImage(ball.getImage(), ball.getPositionX(), ball.getPositionY());

        for(Brick brick : bricks) {
            g.drawImage(brick.getImage(), brick.getPositionX(), brick.getPositionY());
        }

        g.drawString("Score: " + userInfo.getScore(), 520, 10);
    }

    @Override
    public void update(GameContainer gc, StateBasedGame sbg, int delta){
        Input input = gc.getInput();
        paddle.move(input);
        ball.move();
        paddle.collide(ball);

        for(Brick brick : bricks) {
            ball.collide(brick);
        }

        // Player loses when ball goes out of screen
        if(ball.getPositionY() > Game.FRAME_HEIGHT){
            sbg.enterState(Game.gameOver);
        }

        // Player wins when all bricks have been destroyed
        if(userInfo.getScore() == numBricks * Brick.pointsPerBrick){
            sbg.enterState(Game.gameWon);
        }
    }
}

UserInfo class - a class that contains info about the user's game (currently only contains the score)

package breakout;

public class UserInfo {
    private int score;

    // GETTERS
    public int getScore(){
        return score;
    }

    // SETTERS
    public synchronized void incrementScore(int value){
        score += value;
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ You have a lot of code here but very little description. Can you include images from your game? Is your game playable somewhere? \$\endgroup\$ Apr 26, 2015 at 14:28

1 Answer 1

4
\$\begingroup\$

In Game:

public static final int play = 0;
public static final int gameWon = 1;
public static final int gameOver = 2;

Use an enum for this.

In Block:

public boolean isDestroyed(){
    return imageIndex == images.length - 1;
}

Don't abuse state variables. If you have fire blocks that need to loop through an animation, this will give you the strangest of bugs. Store something like health or hitsRemaining to keep track of destruction progress.

public interface Collision {

That's a naming violation. Interfaces shouldn't be nouns. This is a thing, not a capability. Try Collidable (Although your spellchecker will tell you it's not a word... it might be). This naming thing really tripped me up.

static boolean bottomSide(Collision self, Collision other, int tolerance){
    // tolerance is included as the ball's velocity is 3, so the ball may not exactly touch the paddle when it
    // reaches it because it's moving in increments of 3.
    return other.getPositionX() + other.getWidth() >= self.getPositionX()
            && other.getPositionX() <= self.getPositionX() + self.getWidth()
            && other.getPositionY() <= self.getPositionY() + self.getHeight()
            && other.getPositionY() >= self.getPositionY() + self.getHeight() - tolerance;
}

static boolean leftSide(Collision self, Collision other, int tolerance){
    return other.getPositionY() + other.getHeight() >= self.getPositionY()
            && other.getPositionY() <= self.getPositionY() + self.getHeight()
            && other.getPositionX() + other.getWidth() >= self.getPositionX()
            && other.getPositionX() + other.getWidth() <= self.getPositionX() + tolerance;
}

static boolean rightSide(Collision self, Collision other, int tolerance){
    return other.getPositionY() + other.getHeight() >= self.getPositionY()
            && other.getPositionY() <= self.getPositionY() + self.getHeight()
            && other.getPositionX() <= self.getPositionX() + self.getWidth()
            && other.getPositionX() >= self.getPositionX() + self.getWidth() - tolerance;
}

static boolean topSide(Collision self, Collision other, int tolerance){
    return other.getPositionX() + other.getWidth() >= self.getPositionX()
            && other.getPositionX() <= self.getPositionX() + self.getWidth()
            && other.getPositionY() + other.getHeight() >= self.getPositionY()
            && other.getPositionY() + other.getHeight() <= self.getPositionY() + tolerance;
}

Hmm.

Have you considered defining these functions?

int getTopY();
int getBottomY();
int getLeftX();
int getRightX();

Also... you're dealing with rectangles here. That's a very big assumption to make - keep that in mind, or you'll have circles colliding blocks they clearly went past.

In Paddle:

public class Paddle implements Collision {
    private Image image;
    private int paddleHeight;
    private int paddleWidth;

Paddle.paddleHeight is redundant. Use Paddle.height instead. There are a few exceptions, but most of the time you should strip the class name if it's in front of a variable. One of those exceptions would be Line.lineNumber if you had a parser of some sort - Line.number would be too arbitrary.

public void move(Input input){
    // Prevents paddle from moving outside of the frame
    if(getPositionX() > 0) {
        moveLeft(input);
    }
    if (getPositionX() < Game.FRAME_WIDTH - paddleWidth){
        moveRight(input);
    }
}

private void moveLeft(Input input){
    if(input.isKeyDown(Input.KEY_LEFT)){
        adjustPositionX(-velocity);
    }
}

private void moveRight(Input input){
    if(input.isKeyDown(Input.KEY_RIGHT)){
        adjustPositionX(velocity);
    }
}

Flip your functions around. Perform the keyboard checks in move, and the boundary checks in moveLeft and moveRight. Then you don't have to pass Input around.

In Play:

    for(Brick brick : bricks) {
        ball.collide(brick);
    }

Function naming, again. To me this reads as "Ball (moves to collide or) collides with brick". It's not. It's a hitTest that, if hits, handles collisions. Consider finding a better name for this function. I don't have any suggestions since this function is doing two things (checking collision and handling the aftermath), but splitting it up seems like it would require to do the collision testing twice, which seems like a shame.

    // Player wins when all bricks have been destroyed
    if(userInfo.getScore() == numBricks * Brick.pointsPerBrick){

Contrast this snippet:

private boolean checkPlayerWon(){
    for(Brick b : bricks){
        if(!b.isDestroyed()){
            return false;
        }
    }
    return true;
}

This way you actually check if all the blocks are destroyed, rather than tie up your scoring mechanics for a victory condition.

\$\endgroup\$
2
  • \$\begingroup\$ Great, thanks for this! This is excellent advice. With regards to the circle colliding with rectangles, is there an easy way of determining whether they collide w/o treating the circle as a square? If a circle collides with the corner, then the direction of the circle will change depending on where on the circle the corner touches. \$\endgroup\$ Apr 27, 2015 at 12:25
  • 1
    \$\begingroup\$ @Calculus5000 google pixel perfect hittest. One of the ways I know that flash uses is to draw the two objects on separate color channels of an image, then checking if any mixed colors appear. I think it's best if you look for a more knowledgable source for that than me. \$\endgroup\$
    – Pimgd
    Apr 28, 2015 at 7:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.