0
\$\begingroup\$

I started learning basic game dev with Java's LibGDX. For my first project I decided to code a Breakout clone so I can learn the basics. Currently the game fully works - the player can destroy all blocks, score is calculated, there are sound effects, etc. Before I move on to improve the project more, I want to refactor my code. The biggest and most convoluted class is my main class - so I will be posting it here. If you want to check out the rest of my code, here is the github: https://github.com/ViceroyFaust/LearningWithBreakout .

Since I just started learning game dev for the first time, please note that I might not be aware of some practices or patterns. Please let me know what should be refactored and what should I keep in mind in the future. Thank you!

import java.util.ArrayList;

public class MyGdxGame extends ApplicationAdapter {
    // Setting up game objects
    private ShapeRenderer shape;
    private SpriteBatch batch;
    private int ballStartX;
    private int ballStartY;
    private Ball ball;
    private Paddle paddle;
    private ArrayList<Block> blocks;
    private BitmapFont font;
    private GlyphLayout layout;
    private String gameOverMessage;
    private Sound paddleBeep;
    private Sound wallBeep;
    private Sound brickBeep;
    private Sound lossBeep;

    private Color[] blockColors = { new Color(0xd3869bff), new Color(0xb16286ff),
                                    new Color(0x83a598ff), new Color(0x458588ff),
                                    new Color(0x8ec07cff), new Color(0x689d6aff),
                                    new Color(0xb8bb26ff), new Color(0x98971aff) };
    private Color ballColor = new Color(0xfbf1c7ff);
    private Color paddleColor = new Color(0xd5c4a1ff);
    private Color fontColor = new Color(0xebdbb2ff);

    private boolean ballHitPaddle = false;
    private boolean start = false;
    private int score = 0;
    private int lives = 3;

    // Generates 13 x 8 blocks in the upper portion of the screen, leaving some empty space at the top
    private void generateBlocks() {
        int blockWidth = 60;
        int blockHeight = 15;

        int colorIndex = 7;
        for (int y = Gdx.graphics.getHeight() / 2 + 60; y < Gdx.graphics.getHeight() - 50; y += blockHeight + 2) {
            for (int x = 0; x < Gdx.graphics.getWidth(); x += blockWidth + 2) {
                blocks.add(new Block(x, y, blockWidth, blockHeight, 8 - colorIndex, blockColors[colorIndex]));
            }
            --colorIndex;
        }
    }

    private void handleBallWallCollision() {
        if (CollisionHelper.isLeftWallCollision(ball)) {
            ball.leftWallBounce();
            ballHitPaddle = false;
            wallBeep.play();
        } else if (CollisionHelper.isRightWallCollision(ball)) {
            ball.rightWallBounce();
            ballHitPaddle = false;
            wallBeep.play();
        } else if (CollisionHelper.isCeilingCollision(ball)) {
            ball.ceilingBounce();
            ballHitPaddle = false;
            wallBeep.play();
        } else if (CollisionHelper.isFloorCollision(ball)) {
            ball.setX(ballStartX);
            ball.setY(ballStartY);
            --lives;
            start = false;
            ballHitPaddle = false;
            lossBeep.play();
        }
    }

    private void handlePaddleWallCollision() {
        if (CollisionHelper.isLeftWallCollision(paddle)) {
            paddle.leftWallCollision();
        } else if (CollisionHelper.isRightWallCollision(paddle)) {
            paddle.rightWallCollision();
        }
    }

    // Check whether the paddle and the ball made contact and react accordingly
    private void handleBallPaddleCollision() {
        if (!ballHitPaddle && CollisionHelper.isColliding(paddle, ball)) {
            ball.paddleBounce(paddle);
            ballHitPaddle = true;
            paddleBeep.play();
        }
    }

    // Check whether the ball is making contact with one of the blocks and react accordingly
    private void handleBallBlockCollision() {
        // Offset the ball to check what side of the block it hit
        Rectangle offsetBall = ball.getHitbox();
        offsetBall.setX(offsetBall.x - ball.getxSpeed());
        for (Block b : blocks) {
            // Base case - there is nothing to do if the ball is destroyed or there is no collision
            if (b.isDestroyed() || !CollisionHelper.isColliding(ball, b)) continue;
            // Check whether the offset is colliding to determine whether to bounce horizontally or vertically
            if (!CollisionHelper.isColliding(offsetBall, b.getHitbox())) ball.horizontalBounce();
            else ball.verticalBounce();
            score += b.getPoints();
            b.destroy();
            ballHitPaddle = false;
            brickBeep.play();
            // Stop checking collision once one block is destroyed. Prevents destruction loops (bug).
            break;
        }
    }

    private void clearScreen() {
        // Set background color to  R50, G48, B47
        Gdx.gl.glClearColor(50/255f, 48/255f, 47/255f, 1);
        // Clear the screen each render to prevent trailing
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    }

    // Update and draw the ball
    private void ballLoop() {
        ball.draw(shape);
        ball.update();
        handleBallPaddleCollision();
        handleBallBlockCollision();
        handleBallWallCollision();
    }

    // Update and draw the paddle
    private void paddleLoop() {
        paddle.update();
        handlePaddleWallCollision();
        paddle.draw(shape);

    }

    // Draw the blocks
    private void blockLoop() {
        for (Block b : blocks)
            if (!b.isDestroyed()) b.draw(shape);
    }

    // Render shapeRenderer objects
    private void renderShapes() {
        shape.begin(ShapeRenderer.ShapeType.Filled); // Turn on the shape rendered

        // Do not move the ball until the game has started
        if (start) {
            ballLoop();
        } else if (lives > 0 && Gdx.input.isButtonPressed(Input.Buttons.LEFT)){
            start = true;
        }
        paddleLoop();
        blockLoop();

        shape.end(); // Turn off the shape renderer
    }

    private void drawScore() {
        font.draw(batch, "Score: " + score, 10, Gdx.graphics.getHeight() - 10);
    }

    private void drawLives() {
        font.draw(batch, "Balls: " + lives, 200, Gdx.graphics.getHeight() - 10);
    }

    private void drawGameOver() {
        font.draw(batch, gameOverMessage, (Gdx.graphics.getWidth()  - layout.width) / 2,
                (Gdx.graphics.getHeight() - layout.height)/ 2);
    }

    // Render the game's text
    private void renderText() {
        batch.begin();

        drawScore();
        drawLives();
        if (lives <= 0)
            drawGameOver();

        batch.end();
    }

    @Override
    public void create () { // Runs once at the beginning of the program
        // Set up the Shape and Sprite rendering batches
        shape = new ShapeRenderer();
        batch = new SpriteBatch();
        // Calculate the ball's starting coordinates
        ballStartX = Gdx.graphics.getWidth() / 2;
        ballStartY = Gdx.graphics.getHeight() / 2;
        // Generate ball, paddle, and blocks
        ball = new Ball(ballStartX, ballStartY, 6, 0, -5,
                ballColor);
        paddle = new Paddle(Gdx.graphics.getWidth() / 2 - 25, 40, 50, 10,
                paddleColor);
        blocks = new ArrayList<>();
        generateBlocks();
        // Load and configure game font
        font = new BitmapFont(Gdx.files.internal("classic_console_neue.fnt"));
        font.setColor(fontColor);
        layout = new GlyphLayout();
        gameOverMessage = "GAME OVER!";
        layout.setText(font, gameOverMessage);
        // Load sound effects
        paddleBeep = Gdx.audio.newSound(Gdx.files.internal("paddle.wav"));
        wallBeep = Gdx.audio.newSound(Gdx.files.internal("wall.wav"));
        brickBeep = Gdx.audio.newSound(Gdx.files.internal("brick.wav"));
        lossBeep = Gdx.audio.newSound(Gdx.files.internal("loss.wav"));
    }

    @Override
    public void dispose() {
        shape.dispose();
        batch.dispose();
        font.dispose();
        paddleBeep.dispose();
        wallBeep.dispose();
        brickBeep.dispose();
        lossBeep.dispose();
    }

    @Override
    public void render() { // Runs every frame
        clearScreen();
        renderShapes();
        renderText();
    }
}

```
\$\endgroup\$
1
  • \$\begingroup\$ You didn't include code for isLeftWallCollision, ball.leftWallBounce, and similar helpers. But certainly handleBallWallCollision is verbose and tedious. Consider storing an array of these four ESWN dx,dy values (Δx, Δy): (1, 0), (0, -1), (-1, 0), (0, -1). Now looping over those directions should let a single line of code detect most collisions. \$\endgroup\$
    – J_H
    Aug 10, 2023 at 19:00

1 Answer 1

1
\$\begingroup\$

Overall the code is well-organized and looks clean.

This game is very simple so you're getting away with it, but usually you should always use a Viewport (one for game world, one for UI) to manage the view and allow you to work with the objects in the game world without concern for the window size/shape. Making the game world fit the screen becomes encapsulated away from the logic of the gameplay and UI so your code doesn't become too complex or unmanageable.

Here

ball.draw(shape);
ball.update();

you should update before drawing or you'll always be visually lagging one frame.

I like to store Disposables that I instantiate in a List immediately at the site where I'm creating them, and then iterate this list to dispose them in dispose(). It helps prevent accidentally missing one of the items that needs to be disposed. This is an easy bug that can crop up as you add/remove classes.

Be careful about using int literals to represent floats. This can easily lead to bugs. I'm always explicit about using f after numbers that represent floats.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you for the advice! Once I get the free time, I intend on learning how to use viewports and screens. Currently I do not know how to separate game logic away from the rendering, but I imagine it is an important step. The disposables "cheat" is a pretty good idea. I will try doing it in the future. Do game libraries and other frameworks have a "viewport" equivalent? How do games handle non-pixel logic when they need to move their objects? Perhaps these questions are too loaded. \$\endgroup\$ Aug 24, 2023 at 19:24
  • 1
    \$\begingroup\$ The only other game engine I know is Unity. The Unity camera class has viewport functionality built into it instead of the way libGDX does it, where the Viewport class manages a Camera instance. Unity's camera class is kind of monolithic--it also manages the clear-color and frame buffer objects and various other things. IIRC it doesn't offer the pre-set choices like libGDX's ExtendViewport, FitViewport, etc. It's pretty simple to write a snippet that does that behavior for you and attach it to the camera. \$\endgroup\$
    – Tenfour04
    Aug 24, 2023 at 19:40
  • 1
    \$\begingroup\$ In either engine, you never should think about "pixel logic". Think of it in terms of world units. If you choose to make the scale of the world unit the same as a pixel on the screen, you're in for a world of hurt when you run it on differently sized screens because either it will become super zoomed out on big screens or your logic will be different on different screens. But for retro-style games, you can make a world unit equivalent to the size of a sprite's pixel if you like and this works fine because you don't have to consider screen size anywhere except in viewport/camera size code. \$\endgroup\$
    – Tenfour04
    Aug 24, 2023 at 19:42
  • 1
    \$\begingroup\$ So the key here is to keep the world units abstracted from whatever display we are using. The units can represent pixels, meters, lightyears - it doesn't matter. I guess that way a game screen can be scaled much easier. I will have to play around with this, thanks! \$\endgroup\$ Sep 1, 2023 at 7:55

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.