The Breakout Tutorial With C++ And SDL 2.0
In my previous article, The Pong Tutorial, we created a simple game with the mechanics of the classic Pong game. This tutorial shows the next step, a more complex game. We will be creating a game with some of the mechanics of Breakout and Arkanoid. It was also inspired by Megaball
In this tutorial, I have decided to use C++ and SDL 2.0 to implement the game. Developing games with C++ and SDL is an excellent choice, because this allows the game to be compiled on multiple platforms such as Windows, Mac OS X, Linux, iOS, and Android. The logic of the game can easily be ported to any other programming language. The resulting game with full source is available for download at the bottom of this article.
Breakout
In The Pong Tutorial, we introduced paddle movement, ball movement and simple collisions. In Breakout, there is no Artificial Intelligence. The player has to clear a board of bricks by shooting a ball against them. He controls a horizontal paddle and must prevent the ball from leaving the bottom part of the screen. When all of the bricks are removed from the board, a new level should be presented. In addition to the mechanics we implemented for Pong, a game like Breakout needs level management and more complex brick-ball collisions.
The screenshot below shows the original Super Breakout game for the Atari 2600. You can see that all of the elements of the game that are described above are present: a board of bricks, the ball and a paddle.
Interactions
Most of the interactions that we require, are already explained and implemented in my previous article: The Pong Tutorial. We have a moving ball, a controllable paddle, ball-playing field collisions and ball-paddle collisions. I would recommend reading The Pong Tutorial first, before reading the rest of this tutorial, because this tutorial builds upon the knowledge from the first tutorial.
What differentiates Breakout from Pong is the addition of a board of breakable bricks on the screen. The ball can interact with these bricks and break them when a collision occurs. What we need is to generate the board of bricks and define what happens when the ball collides with the bricks.
Generating Levels
Levels are defined by a two-dimensional rectangle that contains bricks at fixed-grid positions. Bricks have properties, such as type which determines the color of the brick. Another property is the state of the brick, which determines if the brick is alive or destroyed. So, lets define a Brick and its properties.
1 2 3 4 5 | class Brick { public: int type; bool state; }; |
The level, also called board, which contains the bricks can be defined by using a two-dimensional array of Bricks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Define the dimensions of the board and bricks #define BOARD_WIDTH 12 #define BOARD_HEIGHT 12 #define BOARD_BRWIDTH 64 #define BOARD_BRHEIGHT 24 // (...) class Board: public Entity { public: Board(SDL_Renderer* renderer); ~Board(); void Update(float delta); void Render(float delta); void CreateLevel(); float brickoffsetx, brickoffsety; // Define the two-dimensional array of bricks Brick bricks[BOARD_WIDTH][BOARD_HEIGHT]; private: SDL_Texture* bricktexture; SDL_Texture* sidetexture; }; |
For this tutorial, I generated a very simple level, where every brick is present, but has a random color. The level is generated in the CreateLevel function.
1 2 3 4 5 6 7 8 9 10 11 12 | void Board::CreateLevel() { for (int i=0; i<BOARD_WIDTH; i++) { for (int j=0; j<BOARD_HEIGHT; j++) { Brick brick; brick.type = rand() % 4; // Random color //brick.type = (i ^ j) % 4; // Example of a fixed pattern using xor brick.state = true; // Brick is present bricks[i][j] = brick; } } } |
Ball-Brick Collisions
The most complex feature of a Breakout game is determining and resolving collisions between a ball and the bricks. It is not enough to determine that there is a collision, we also need to know which side of the brick collided with the ball to give a proper collision response. A brick has four sides, and hitting a different side results in a different collision response. This means that the ball bounces back in a different direction when hitting a different wall.
We can divide this feature in three steps:
- Determine that there is a collision
- Calculate which side of the brick is hit
- Give a collision response
Determining that there is a collision, is pretty easy. We represent the ball as a rectangle and determine if it overlaps any of the bricks that are still present on the screen. It is a bit harder to determine which side of the brick was hit in the collision. To determine the side, we look at the overlapping parts of the bounding rectangle of the ball and the rectangle of the brick. We determine the amount of overlap in the x-direction and the y-direction, which will be called xsize and ysize in the code below.
Based on these two values we can determine which side of the brick was most likely hit by the ball:
- If there is more overlap in the x-direction, there is a collision with the top or bottom of the brick
- If there is more overlap in the y-direction, there is a collision with the left or right of the brick
The side that was hit by the ball can now be calculated by looking at the centers of the collision objects.
The image below explains how we calculate the overlap between the ball and the brick:
Below you can see the CheckBrickCollisions function as explained before.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | void Game::CheckBrickCollisions() { for (int i=0; i<BOARD_WIDTH; i++) { for (int j=0; j<BOARD_HEIGHT; j++) { Brick brick = board->bricks[i][j]; // Check if brick is present if (brick.state) { // Brick x and y coordinates float brickx = board->brickoffsetx + board->x + i*BOARD_BRWIDTH; float bricky = board->brickoffsety + board->y + j*BOARD_BRHEIGHT; // Center of the ball x and y coordinates float ballcenterx = ball->x + 0.5f*ball->width; float ballcentery = ball->y + 0.5f*ball->height; // Center of the brick x and y coordinates float brickcenterx = brickx + 0.5f*BOARD_BRWIDTH; float brickcentery = bricky + 0.5f*BOARD_BRHEIGHT; if (ball->x <= brickx + BOARD_BRWIDTH && ball->x+ball->width >= brickx && ball->y <= bricky + BOARD_BRHEIGHT && ball->y + ball->height >= bricky) { // Collision detected, remove the brick board->bricks[i][j].state = false; // Asume the ball goes slow enough to not skip through the bricks // Calculate ysize float ymin = 0; if (bricky > ball->y) { ymin = bricky; } else { ymin = ball->y; } float ymax = 0; if (bricky+BOARD_BRHEIGHT < ball->y+ball->height) { ymax = bricky+BOARD_BRHEIGHT; } else { ymax = ball->y+ball->height; } float ysize = ymax - ymin; // Calculate xsize float xmin = 0; if (brickx > ball->x) { xmin = brickx; } else { xmin = ball->x; } float xmax = 0; if (brickx+BOARD_BRWIDTH < ball->x+ball->width) { xmax = brickx+BOARD_BRWIDTH; } else { xmax = ball->x+ball->width; } float xsize = xmax - xmin; // The origin is at the top-left corner of the screen! // Set collision response if (xsize > ysize) { if (ballcentery > brickcentery) { // Bottom ball->y += ysize + 0.01f; // Move out of collision BallBrickResponse(3); } else { // Top ball->y -= ysize + 0.01f; // Move out of collision BallBrickResponse(1); } } else { if (ballcenterx < brickcenterx) { // Left ball->x -= xsize + 0.01f; // Move out of collision BallBrickResponse(0); } else { // Right ball->x += xsize + 0.01f; // Move out of collision BallBrickResponse(2); } } return; } } } } } |
The final step is to determine the collision response by creating the BallBrickResponse function. This function has one parameter dirindex. This parameter indicates which side of the brick was hit in a collision. Based on this parameter and the current direction of the ball, the new direction of the ball is determined.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | void Game::BallBrickResponse(int dirindex) { // dirindex 0: Left, 1: Top, 2: Right, 3: Bottom // Direction factors int mulx = 1; int muly = 1; if (ball->dirx > 0) { // Ball is moving in the positive x direction if (ball->diry > 0) { // Ball is moving in the positive y direction // +1 +1 if (dirindex == 0 || dirindex == 3) { mulx = -1; } else { muly = -1; } } else if (ball->diry < 0) { // Ball is moving in the negative y direction // +1 -1 if (dirindex == 0 || dirindex == 1) { mulx = -1; } else { muly = -1; } } } else if (ball->dirx < 0) { // Ball is moving in the negative x direction if (ball->diry > 0) { // Ball is moving in the positive y direction // -1 +1 if (dirindex == 2 || dirindex == 3) { mulx = -1; } else { muly = -1; } } else if (ball->diry < 0) { // Ball is moving in the negative y direction // -1 -1 if (dirindex == 1 || dirindex == 2) { mulx = -1; } else { muly = -1; } } } // Set the new direction of the ball, by multiplying the old direction // with the determined direction factors ball->SetDirection(mulx*ball->dirx, muly*ball->diry); } |
Download
The full source code and a Windows executable of this tutorial can be downloaded below. Included is a project file for the Code Blocks IDE. To compile the project, you need to install a compiler like MinGW and define the sdl2 global variable in the Code Blocks global variable editor. Alternatively, you can use Visual Studio Express and import the source files into a new project, followed by linking to the SDL2 and SDL2_image libraries. The SDL library can be found here: SDL 2.0. Details on how to compile the source code is out of the scope of this tutorial. The source code of the project is available here.