Breakout: Game Progression

Implement the core gameplay loop of destroying blocks and advancing through levels using SDL events.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

View Full CourseGet Started for Free
Abstract art representing computer programming
Breakout: Part 7
Ryan McCombe
Ryan McCombe
Posted

Our game now has all its core interactive elements: a ball, a paddle, walls, and bricks. In this lesson, we'll tie them all together to create a complete gameplay loop, from destroying the first brick to advancing to the next level.

The main focus will be on handling the consequences of collisions. When the ball hits a brick, we need to "destroy" it. We'll implement this by disabling the brick's image and collision components, effectively removing it from play. We'll also use SDL's event system to broadcast a BLOCK_DESTROYED event.

Our BreakoutScene will listen for these events, keeping a count of the remaining bricks.

Once that count reaches zero, we'll trigger the level advancement logic, either loading the next level or declaring the game won.

Reacting to Block Collisions

Let's begin by having our blocks react to being collided with. We'll do this by disabling their components and, later, we'll also have them push an SDL_Event to report they were destroyed.

Earlier in the project, we added the ability to disable components and updated the collision component to react appropriately to being disabled. Let's do the same thing for our ImageComponent.

Disabling Image Components

Our Component base class has a GetIsEnabled() function, so supporting it within our ImageComponent is quite straightforward. We can stop our component from being rendered when it is disabled with a single line of code:

Engine/ECS/Source/ImageComponent.cpp

// ...

void ImageComponent::Render(
  SDL_Surface* Surface
) {
  if (!GetIsEnabled()) return;
} // ...

We may also want to change what DrawDebugHelpers() does when the component is disabled. Perhaps it renders nothing, or we may want it to render a small indicator to signify the component is still there, even though it is disabled.

In our case, we'll just have it render nothing:

Engine/ECS/Source/ImageComponent.cpp

// ...

void ImageComponent::DrawDebugHelpers(
  SDL_Surface* Surface
) {
  using Utilities::DrawRectOutline;
  if (!GetIsEnabled()) return;
}

"Destroying" Blocks

Now, to get rid of our blocks when the ball hits them, we just need to override the HandleCollision() function and disable our image and collision components:

Breakout/Block.h

// ...

class Block : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    Image->SetIsEnabled(false);
    Collision->SetIsEnabled(false);
  }
  // ...
};

With these changes, the core gameplay is now in place - we can bounce the ball off our paddle to "destroy" the blocks in our level:

Screenshot showing blocks getting destroyed

Debugging: Blocks Disappearing Immediately

With these changes, it's possible that some or all of your blocks will disappear as soon as the game starts. This is almost certainly due to blocks colliding with other blocks, or colliding with our walls.

This can be fixed by updating your block positioning logic to ensure each block's collider isn't overlapping with adjacent colliders.

Alternatively, you can update your HandleCollision() override to ignore collisions with other blocks:

Breakout/Block.h

// ...

class Block : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    if (dynamic_cast<Block*>(&Other)) {
      return;
    }
    Image->SetIsEnabled(false);
    Collision->SetIsEnabled(false);
  }
  // ...
};

Or alternatively, to ignore collisions with anything that is not a ball:

Breakout/Block.h

// ...
#include "Breakout/Ball.h"

class Block : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    if (!dynamic_cast<Ball*>(&Other)) {
      return;
    }
    Image->SetIsEnabled(false);
    Collision->SetIsEnabled(false);
  }
  // ...
};

Why Not Destroy Entities and Components for Real?

You may be wondering why we're essentially faking the destruction of our blocks by disabling their components. Why not just destroy them for real, like in the following example:

void HandleCollision(Entity& Other) override {
  // Note that RemoveEntity() does not exist in our
  // project - this is a hypothetical example:
  GetScene().RemoveEntity(this);
}

Alternatively, why not remove the components entirely instead of disabling them?

void HandleCollision(Entity& Other) override {
  RemoveComponent(Image);
  RemoveComponent(Collision);
}

This may work, but we should always be cautious about deleting objects, as it's quite risky.

We need to be fully aware of the contexts in which our function might be invoked, and the full implications of deleting an object at those points in time. For example, if HandleCollision() is being called from a loop that's iterating over an array of entities, deleting an entity from that array in the middle of the loop is problematic.

In our program, HandleCollisions() is indeed being called from such a loop. It's being called in the Scene::CheckCollisions() function.

When we need to support entity deletion, a common pattern is not to immediately delete the object, but rather to save those deletion requests into a collection to be handled later. For example:

class Scene {
public:
  // ...
  void RemoveEntity(Entity* EntityToDelete) {
    EntitiesToDelete.push_back(EntityToDelete);
  }
  
private:
  // ...
  std::vector<Entity*> EntitiesToDelete;
}

Later in the application loop, our Scene can delete those objects at a convenient time and in a safe way.

Advancing the Game

Let's register some events that we can use to report progress through our game. We'll register events for a block being destroyed, as well as the overall game being won or lost:

Config.h

// ...

namespace UserEvents{
// ...
inline Uint32 BLOCK_DESTROYED{
  SDL_RegisterEvents(1)};
inline Uint32 GAME_WON{SDL_RegisterEvents(1)};
inline Uint32 GAME_LOST{SDL_RegisterEvents(1)};
}

// ...

Keeping Track of Remaining Blocks

In our Block class, in addition to disabling components when collided with, let's also broadcast one of our new BLOCK_DESTROYED events:

Breakout/Block.h

// ...

class Block : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
SDL_Event E{UserEvents::BLOCK_DESTROYED}; SDL_PushEvent(&E); } // ... };

Over in our BreakoutScene, let's keep track of how many blocks are remaining in the scene. We'll add a member variable called BlocksRemaining and, when we receive a BLOCK_DESTROYED event, we'll decrement that count:

Breakout/BreakoutScene.h

// ...
#include "Config.h"

class BreakoutScene : public Scene {
public:
  // ...

  void HandleEvent(const SDL_Event& E) {
    Scene::HandleEvent(E);
    using namespace UserEvents;
    if (E.type == BLOCK_DESTROYED) {
      --BlocksRemaining;
    }
  }

private:
  int BlocksRemaining{0};
};

When we load a level, we'll update the BlocksRemaining count with the number of blocks we loaded:

Breakout/Source/BreakoutScene.cpp

// ...

void BreakoutScene::Load(int Level) {
BlocksRemaining = BlockCount; }

Loading the Next Level

When all the blocks in the level are destroyed, we want to advance the player to the next level. Let's have our BreakoutScene keep track of what level it has currently loaded:

Breakout/Source/BreakoutScene.cpp

// ...

class BreakoutScene : public Scene {
  // ...

private:
  int LoadedLevel{1};
  // ...
};

We'll update our Load() function to set this variable to the correct value after a level is successfully loaded:

Breakout/Source/BreakoutScene.cpp

// ...

void BreakoutScene::Load(int Level) {
LoadedLevel = Level; }

When all the blocks are destroyed in our level, we'll call a new CompleteLevel() function. If the player just completed the last level in our game (which is level 3, in our case) we'll push a GAME_WON event. Alternatively, if they completed some other level, we'll load the next level:

Breakout/BreakoutScene.h

// ...

class BreakoutScene : public Scene {
public:
  // ...

  void HandleEvent(const SDL_Event& E) {
    Scene::HandleEvent(E);
    using namespace UserEvents;
    if (E.type == BLOCK_DESTROYED) {
      --BlocksRemaining;
      if (BlocksRemaining == 0) {
        CompleteLevel();
      }
    }
  }
  // ...

private:
  void CompleteLevel() {
    if (LoadedLevel == 3) {
      SDL_Event WonEvent{UserEvents::GAME_WON};
      SDL_PushEvent(&WonEvent);
    } else {
      Load(LoadedLevel + 1);
    }
  }
  // ...
};

Enabling Cheats

Needing to complete a level to see if our code is working can get quite annoying. Typically, we'll add some cheats to our game to help with development and testing.

Let's add the cheats behind an ENABLE_CHEATS preprocessor macro. If that flag is set, and the user presses their "c" key, we'll immediately complete the current level:

Breakout/BreakoutScene.h

// ...

class BreakoutScene : public Scene {
public:
  // ...

  void HandleEvent(const SDL_Event& E) {
    Scene::HandleEvent(E);
    using namespace UserEvents;
    if (E.type == BLOCK_DESTROYED) {
      --BlocksRemaining;
      if (BlocksRemaining == 0) {
        CompleteLevel();
      }
    }
#ifdef ENABLE_CHEATS
    else if (
      E.type == SDL_KEYDOWN &&
      E.key.keysym.sym == SDLK_c
    ) {
      CompleteLevel();
    }
#endif
  }

  // ...
};

Loading Levels from the Editor

In our Editor project, we added a button that lets us play a level directly from our editor. We had that button broadcast a LAUNCH_LEVEL event, with the code member set to the level that should be launched.

Let's have our BreakoutScene listen for those events, and load the requested level:

Breakout/BreakoutScene.h

// ...

class BreakoutScene : public Scene {
public:
  // ...
  void HandleEvent(const SDL_Event& E) {
else if (E.type == LAUNCH_LEVEL) { Load(E.user.code); } } // ... };

With these changes, we can now advance through the levels of our game either by completing them, or pressing the C key when we have enabled cheats. We can also load a specific level from our editor window, and then play it in our game window.

Complete Code

Complete versions of the files we changed in this lesson are provided below:

Files

Breakout
Engine
Config.h
Select a file to view its content

Summary

We've now added a clear objective and a sense of progression to our game. This lesson focused on handling the aftermath of a block collision: making the block disappear and tracking it as progress towards winning the level.

We used component disabling for destruction, SDL events for communication, and simple state tracking in our scene to manage level advancement.

Here's an overview of our changes:

  • Blocks now get "destroyed" when the ball hits them.
  • We created a BLOCK_DESTROYED event to signal progress.
  • Our BreakoutScene now counts remaining blocks to detect when a level is won.
  • We implemented the logic to load the next level or trigger a GAME_WON event.
  • We added a helpful cheat code to speed up testing.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

View Course
Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

View Full CourseGet Started for Free

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 128 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

View Course
Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved