Basic Breakout Game with Raylib
I have always wanted to write games. Back in the day that was the whole idea why I wanted to be a programmer. And I have worked myself into a new job at Fliva exactly because I wanted to become a game programmer.
Only problem was that I found a way to make a much more lucrative business out of our video company by applying what little knowledge I had aquired about game programming to video programming… And suddenly I was doing low level C++ programming as a day job; and game programming went a little to the background in my mind.
And that have not really changed; however I like to do little experiments with game programming now and again.
Why write a game now?
This time it was because I was looking into teaching my son to make games. He wants to be a game programmer when he grows up; but he is still in the unrealistic phase of “I want to make the next GTA meets Fortnite on my own, and become a bazzilionaire”thinking, where this game programming thing is just get an idea, tell the computer what the idea is and make a ton of money!
I want to both teach him that this is hard, but also that it is doable.
Well; first of all I need to know whether to go the “learn to code first”-route with him or the “learn a game engine first” route. This is my attempt to find a simple starting point for the learn to code first route, where it is not overly complex, but also not overly limiting in what he will be able to do.
Raylib
RayLib is in my opinion a great learning tool; so I tried to make breakout in Raylib - and c++. I am in no way done; and I have some things that are definitely not working correctly. But the game can be started, played and has two levels… Whoah!
Entrypoint
Currently the game has the simplest main function:
int main(void)
{
auto game = setup();
run(game);
shutdown();
return 0;
}
Setup returns a Game struct, which is sent to run (which is the main game loop). When the game loop is over, the shutdown method will be called (shutdown just closes the window, which is opened in the setup method)
Immutable state
The run method is slightly more interesting.
void run(Game game) {
while (!WindowShouldClose())
{
game = simulate(game);
draw(game);
}
}
I opted - mostly as an experiment - to have the Game state be immutable, which means that simulate will take the current state of the world, and return a completely new Game object for each frame. This works quite well (for this tiny game) actually.
The draw game then cannot mutate the Game state at all; and those two methods just run forever, until something makes WindowShouldClose be true.
Project Structure
All that code (and some boilerplate) was placed in a main.cpp file, and the rest of the game has been split into three files.
- game_objects.hpp containing all the structs of the game. Game, Scene, Ball, Player, Brick.
- draw.hpp containg everything to do with drawing the game
- simulation.hpp containing everything that is mutating the game (e.g. player input)
Game Setup
Game Objects
The main state of a game is called Game
. This contains data that should “persist” duiring the game; it has a currentScene
which is just the current level being played (I have been programming video stuff so long, that Scene was a more natural word for me than Level). The game object also owns the score, number of lives and other data that persists across levels.
The level Scene
has a collection of bricks Brick
and a collection of balls Ball
(this game allows more than one ball being present at a time, even though one is the default). And finally the level has a Player
Player consists of a Rect (position and size) and a speed.
Ball has a radius, position, direction, speed and a texture.
Bricks have a position, texture and score (how much you are awarded in score for destroying the brick)
Loading Levels
Levels are loaded from Tiled data; we parse the tilesets (tsx files) and levels (tmx files); extracting the parts we need.
We create a texture per brick based on the data from the tileset; and set its position and score from the level file.
A future blog post will explore the details of this a bit further ; but it really just is using rapidxml to read the data we need from the xml files.
Simulation
User Input
The only user input we support is KEY_LEFT
, KEY_RIGHT
for moving the player, SPACE
for shooting another ball (if extra balls are present) and finally ESC
to exit the game.
Raylib has a single function for testing user input called IsKeyPressed
which we use to check for each key in turn.
Then we mutate our (copy of) the game state.
if(IsKeyDown(KEY_LEFT)) {
game.currentScene.player.rect.x -= game.currentScene.player.speed * elapsedTime;
}
if(IsKeyDown(KEY_RIGHT)) {
game.currentScene.player.rect.x += game.currentScene.player.speed * elapsedTime;
}
if(IsKeyPressed(KEY_SPACE)) {
if(game.extraBalls > 0) {
auto ball = defaultBall();
ball.position.x = game.currentScene.player.rect.x + 20;
ball.position.y = game.currentScene.player.rect.y - 20;
game.currentScene.balls.push_back(ball);
game.extraBalls--;
}
}
Collisions
There are three types of collisions in the game.
Check ball against outer walls
There are no rendered outer walls in this game; so we just check to see if the ball is heading outside the window.
Special handling is required for the bottom part of the screen, since this will cause a life to be lost (and potentially end the game)
if(ball.position.x + 0.5 * ball.radius > game.width || ball.position.x - 0.5 * ball.radius < 0) ball.direction.x *= -1;
if(ball.position.y - 0.5 * ball.radius < 0) ball.direction.y *= -1;
if(ball.position.y + 0.5 * ball.radius > game.height) {
if(ballCount == 1) {
if(game.lives == 1) game.state = GAMEOVER;
game.lives -= 1;
ball.direction.y *= -1;
ball.position.y = game.currentScene.player.rect.y - 50;
ball.position.x = game.currentScene.player.rect.x + 50;
} else {
game.currentScene.balls.erase(game.currentScene.balls.begin() + ballId);
ballId--;
}
}
Check ball against bricks
Raylib has a collision function we can use called CheckCollisionCircleRec
which does the heavy lifting. We run through all the bricks to check if there is a collision with any of them; and then we run through the collision list handling all collisions by changing the balls direction, adding the score and removing the brick.
std::vector<int> collisionList;
{
int index = 0;
for(auto brick : game.currentScene.bricks) {
Rectangle rect;
rect.x = brick.position.x;
rect.y = brick.position.y;
rect.width = brick.texture.width;
rect.height = brick.texture.height;
if(CheckCollisionCircleRec(ball.position, ball.radius, rect)) {
collisionList.push_back(index);
break;
}
index++;
}
}
for(auto index : collisionList) {
auto brick = game.currentScene.bricks[index];
if(ball.position.y < brick.position.y) {
ball.direction.y *= -1;
} else if(ball.position.y > brick.position.y + brick.texture.height) {
ball.direction.y *= -1;
} else {
ball.direction.x *= -1;
}
if(game.currentScene.bricks.size() < 20) {
ball.speed += 10;
}
game.score += brick.score;
game.currentScene.bricks.erase(game.currentScene.bricks.begin() + index);
}
Check ball against player
The first naive version of this game only has the ball moving in 45 degree angles; which makes this test very simple.
if(CheckCollisionCircleRec(ball.position, ball.radius, game.currentScene.player.rect)) {
ball.direction.y *= -1;
}
Drawing
The draw function is relatively simple. BeginDrawing
and EndDrawing
are functions Raylib requires you to call before and after drawing. ClearBackground is called before any drawing commands, and the switch statement is there to only draw the parts of the app, that matches the state the game is in.
This also means that we draw every single element to screen for every frame of the game.
void draw(Game game) {
BeginDrawing();
ClearBackground(RAYWHITE);
drawGameUI(game);
switch(game.state)
{
case START : drawStartScreen(game); break;
case PLAYING: drawScene(game); break;
case NEXTSCENE: drawScene(game); break;
case GAMEOVER : drawGameOver(game); break;
}
EndDrawing();
}
Drawing UI
UI Elements like score, lives and extra balls are really just text. Drawing them is dead easy with Raylibs DrawText
function.
std::string score = "Score: " + std::to_string(game.score);
DrawText(score.c_str(), 20, 20, 20, BLACK);
std::string lives = "Lives: " + std::to_string(game.lives);
DrawText(lives.c_str(), 520, 20, 20, BLACK);
std::string balls = "Extra Balls: " + std::to_string(game.extraBalls);
DrawText(balls.c_str(), 720, 20, 20, BLACK);
Drawing Player Bat
The player bat is really just a rectangle with a position, size and a color.
DrawRectangle(scene.player.rect.x, scene.player.rect.y, scene.player.rect.width, scene.player.rect.height, BLACK);
Drawing Ball
The balls are all just circles with a color.
for(auto ball : scene.balls) {
DrawCircle(ball.position.x, ball.position.y, ball.radius, RED);
}
Drawing Bricks
The bricks are drawn using textures (meanig they have an image, instead of just a color).
for(auto brick : scene.bricks) {
DrawTexture(brick.texture, brick.position.x, brick.position.y, WHITE);
}