Some game progress

So far we have built a game that allows us to break bricks on the screen. But there's no recording of game progress, and no "winning" or "losing" (unless you count the ball going below the screen and having to shut off the game as losing).

Let's go over some of the stuff we learned in the last lesson. In particular I want to talk about using methods, because they can make code so much easier to write and read.

If you remember, I moved the code that draws the ball from the main loop into a method inside the Ball struct. Doing this means that whenever you call the method on a ball, it automatically can access all the things inside the ball (such as pos and radius). This means you no longer have to tell the compiler which Ball you are talking about, it's the one you called the method on.

This becomes really important when you have more than one of something. For instance with the Brick struct, we have an entire array of them. If you have multiple enemies roaming around, you probably want them all to have their own properties, and run them independently. Using methods helps make this easy because you can access the fields of the struct without having to name it.

So let's define formally what a method means. A method is a function written inside a struct that is given access to the data of the struct while running. You call a method by naming the struct that you wish to call with, and then append .methodName() where methodName is the name of your method. So in our Ball example, we have:

struct Ball {
    double radius = 10;
    Vector2 pos = Vector2(0, 0);
    Vector2 velocity = Vector2(0, 0);

    bool bounceY;
    bool bounceX;

    void draw() {
        DrawCircleV(pos, radius, Colors.RED);
    }
    ...
}

And we call it like this:

ball.draw();

Notice how we no longer have to say ball.pos, but just pos. This is because the compiler knows that we are inside the ball, and so when you use the fields of ball, you no longer need to tell it which object has that.

Just a small note about the function in there. D requires you to specify a return type for a function, because of the syntax. Remember in languages like lua and javascript we had to say "function" when it's a function. D doesn't have that, it is looking for something like ReturnType functionname(parameters), so you have to have something for that ReturnType, even if it doesn't return anything. In D, a function that doesn't return any value returns void.

During class, I also added some other functions in there for detecting collisions, and you can see from the code below, it's very nice to not have to say ball.whatever to access everything inside the ball.

There's one other concept that I introduced quickly but I want to go over it in more detail, and that's ref. You will notice in the code below, I have the statement:

foreach(ref brick; bricks) {
    if(ball.collide(brick))
        brick.hits -= 1;
}

The ref word at the front tells the compiler that I want to get the brick by reference. What does it mean by reference? It tells the computer that I want to actually affect the brick that's in the array. If I don't say ref, what happens is the compiler copies the brick, and while I can use it to run collisions, the line that tells brick to reduce its hit count by 1 will run on the copy, not the original. This means, once the foreach loop exits, the copy goes away, and nothing happened to the original brick. You can try removing the ref and see what happens!

You can use ref when declaring a function parameter as well, which tells the compiler to refer to the original instead of making a copy. In most cases, making a copy is fine, you only want to use ref when you need to modify the original.

Homework - Add some more game features

Before starting the homework, here is the code that I finished in the lesson. Even though I didn't do all the updates from the previous homework during the lesson, I added that stuff in here too.

import std.stdio;
import raylib;
import std.random;

struct Paddle
{
    int height = 50;
    int width = 150;
    int pos = 0;
}

struct Ball
{
    double radius = 10;
    Vector2 pos = Vector2(0, 0);
    Vector2 velocity = Vector2(0, 0);

    bool bounceY;
    bool bounceX;

    void draw()
    {
        DrawCircleV(pos, radius, Colors.RED);
    }

    void doBounce()
    {
        // actually perform the bounces recorded in the collide function.
        if(bounceX)
            velocity.x = -velocity.x;
        if(bounceY)
            velocity.y = -velocity.y;
        // reset for the next check
        bounceY = false;
        bounceX = false;
    }

    bool collide(Brick b)
    {
        // this code says -- if the ball is touching the brick at all,
        // record which direction it should bounce. Either an x bounce, a y
        // bounce, or both. If it bounces, return true.
        // if the brick has 0 hits left, don't collide.
        if(b.hits > 0 &&
           pos.x >= b.pos.x - radius && pos.x <= b.pos.x + b.width + radius &&
           pos.y >= b.pos.y - radius && pos.y <= b.pos.y + b.height + radius)
        {
            if(pos.x >= b.pos.x && pos.x <= b.pos.x + b.width)
            {
                bounceY = true;
            }
            else if(pos.y >= b.pos.y && pos.y <= b.pos.y + b.height)
            {
                bounceX = true;
            }
            else
            {
                if(pos.x < b.pos.x && velocity.x > 0)
                    bounceX = true;
                if(pos.x > b.pos.x + b.width && velocity.x < 0)
                    bounceX = true;
                if(pos.y < b.pos.y && velocity.y > 0)
                    bounceY = true;
                if(pos.y > b.pos.y + b.height && velocity.y < 0)
                    bounceY = true;                   
            }
            return true;   
        }
        return false;
    }
}

struct Brick
{
    Vector2 pos;
    int width;
    int height;
    int hits;
    Color color;

    void draw()
    {
        if(hits > 0)
            DrawRectangleV(pos, Vector2(width, height), color);
    }
}

void main()
{
    InitWindow(640, 720, "Bricker");

    Paddle paddle;

    Brick[] bricks;

    Colors[] colors = [Colors.RED, Colors.ORANGE, Colors.YELLOW, Colors.PURPLE, Colors.GREEN, Colors.BLUE, Colors.VIOLET];
    foreach(y; 0 .. 5)
        foreach(x; 0 .. 16)
            bricks ~= Brick(
                    Vector2(x * 40, y * 20), // position
                    40,                      // width
                    20,                      // height
                    1,                       // hits
                    choice(colors));         // color (pick a random one)

    // uncomment to use your texture for the paddle (if you have one)
    //Texture2D tex = LoadTexture("picklerick.png");
    //tex.width = paddle.width;
    //tex.height = paddle.height;
    Ball ball;

    ball.velocity = Vector2(10, -20);
    ball.pos = Vector2(0, 300);

    // angles to bounce off the paddle, according to which quadrant of the paddle the ball hits.
    Vector2[] angles = [
        Vector2(-20, -10),
        Vector2(-10, -20),
        Vector2(10, -20),
        Vector2(20, -10),
    ];

    SetTargetFPS(30);

    while(!WindowShouldClose())
    {
        // process paddle position
        paddle.pos = GetMouseX() - paddle.width / 2;
        auto paddleTop = GetScreenHeight() - paddle.height - 30;

        if(paddle.pos <= 0)
        {
            paddle.pos = 0;
        }

        if(paddle.pos >= GetScreenWidth() - paddle.width)
        {
            paddle.pos = GetScreenWidth() - paddle.width;
        }

        // move ball to a new position based on velocity
        ball.pos = Vector2Add(ball.pos, ball.velocity);

        if(ball.velocity.y < 0 && ball.pos.y <= ball.radius) { // top wall
            ball.velocity.y = -ball.velocity.y;
            float adjustment = ball.radius - ball.pos.y;
            ball.pos.y += adjustment + adjustment;
        }
        if(ball.velocity.x < 0 && ball.pos.x <= ball.radius) { // left wall
            ball.velocity.x = -ball.velocity.x;
            float adjustment = ball.radius - ball.pos.x;
            ball.pos.x += adjustment + adjustment;
        }
        //if(ball.velocity.y > 0 && ball.pos.y >= GetScreenHeight() - ball.radius) { // bottom wall
        //    ball.velocity.y = -ball.velocity.y;
        //    float adjustment = GetScreenHeight() - ball.radius - ball.pos.y;
        //    ball.pos.y += adjustment + adjustment;
        //}
        if(ball.velocity.x > 0 && ball.pos.x >= GetScreenWidth() - ball.radius) { // right wall
            ball.velocity.x = -ball.velocity.x;
            float adjustment = GetScreenWidth() - ball.radius - ball.pos.x;
            ball.pos.x += adjustment + adjustment;
        }

        // check for a paddle bounce
        if(ball.velocity.y > 0 && ball.pos.y >= paddleTop - ball.radius &&
                ball.pos.y <= paddleTop + ball.radius)
        {
            if(ball.pos.x >= paddle.pos && ball.pos.x <= paddle.pos + paddle.width)
            {
                int xdiff = cast(int)ball.pos.x - paddle.pos; // where on the paddleis the ball hitting
                int index = xdiff * 4 / paddle.width; // which angle should we use.
                ball.velocity = angles[index]; // set the angle
            }
        }

        // check for brick collisions
        foreach(ref brick; bricks) {
            if(ball.collide(brick))
                brick.hits -= 1;
        }

        // process the bounces
        ball.doBounce();

        BeginDrawing();
        ClearBackground(Colors.WHITE);
        DrawRectangle(paddle.pos, paddleTop,
                       paddle.width, paddle.height, Colors.BLACK);
        //DrawTexture(tex, paddle.pos, paddleTop, Colors.WHITE);
        foreach(ref brick; bricks)
            brick.draw();
        ball.draw();
        EndDrawing();
    }

    //UnloadTexture(tex);
    CloseWindow();
}

I want you to do 3 things:

  1. Allow the person to click the mouse to start the game when it's not running. This means, we don't have the ball on the screen, but maybe we have the paddle moving with the mouse. And we need to show a message to the user telling them to start.
  2. On clicking, reset the game state (including all bricks and the ball starting position/velocity).
  3. If the ball is lost down the bottom, tell the user his game is over, and he can click to start over.
  4. BONUS: if the player destroys all the bricks, tell him he won!

First thing we need is a variable to store whether the game is being played or not. This can be a simple boolean, which you would declare BEFORE the event loop:

bool gameRunning = false;

Now, based on this boolean, you conditionally skip ALL the game code (moving the ball, checking for collisions, etc), and skip drawing the ball. Be sure to leave in the paddle movement, or it will look like the game is hung.


while(!WindowShouldClose())
    ...

    if(paddle.pos >= GetScreenWidth() - paddle.width)
    {
        paddle.pos = GetScreenWidth() - paddle.width;
    }

    if(gameRunning) {
        // all the code that moves the ball, checks for collisions, etc.
        ...
    }
    // be sure to skip drawing the ball later if game is not running!

Second thing we need is to draw text to the screen when the game is not being played to let the user know that the game is over, and they need to click to start again. For this, we will draw text right in the center. But you can't tell it the center point to draw around, the point where it starts is the upper-left part of the text! So how do we center it?

There is a raylib function called MeasureText which tells you how wide the text given would be if drawn. We'll use that to draw text to the center of the screen:

// font size is 30
int textWidth = MeasureText("Game Over! Click to start!", 30);
DrawText("Game Over! Click to start!", (GetScreenWidth() - textWidth) / 2, GetScreenHeight() / 2, 30, Colors.BLUE);

The second parameter is the font size. Now we have a way to draw text to the screen! But we want to be able to do this any time we want. So let's write it as a function (it can go anywhere, including inside the main function (Note, you can use whatever color you want):

void showMessage(const char *msg) {
    int textWidth = MeasureText(msg, 30);
    DrawText(msg, (GetScreenWidth() - textWidth) / 2, GetScreenHeight() / 2, 30, Colors.BLUE);
}

And now, we can call it from our drawing code, and we can display whatever message we want:

showMessage("Game Over! Click to start!");

A nice advantage to this is we don't have to repeat the message, it's a parameter. You should call this function when the game is not running. You can do this either by using if(!gameRunning) or by using an else statement after the if(gameRunning) that you added in step 1.

Next, we need to handle the click. When the click happens, we want to initialize all the brick data, and the ball, and turn the game on. To initialize the bricks, we will loop over the array by reference, setting the brick hit values to 1 (so they show up and are involved in collisions). Then we will move the ball to the starting position (wherever you want to) and the starting velocity:

if(!gameRunning && IsMouseButtonPressed(MouseButton.MOUSE_LEFT_BUTTON)) {
    foreach(ref brick; bricks)
        brick.hit = 1;
    // reset the ball position and velocity
    ... // you fill this in
    gameRunning = true;
}

You fill in the code that sets the ball up. It needs the velocity and the position set. Make sure this code does NOT go inside the if statement that skips all the other code, because then it will never run!

And finally, we need to check to see if the game should be over. To see if you lost, just check to see if the ball went off the screen. I want you to write this one. When the game is over, you should set the gameRunning boolean to false, which will display the game over message that we added already (and disable the game from running).

Bonus

Another check you can add is to see if any bricks are still alive (have more than 0 hits). If no bricks are alive, then you can stop the game, and show a winning message (you can use the same showMessage function we used before). This is quite a bit of code using loops and if statements, and I'm expecting that not many will figure this out, but see if you can do it! If nobody gets it by Thursday, I'll post the answer for it. (Hint, you can add code to the foreach loop through the bricks that checks for collisions)

©2021 Steven Schveighoffer