Breakout: Final Touches

Learn to manage game states for winning, losing, and pausing, and prepare the final game for distribution.
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: Final Touches
Ryan McCombe
Ryan McCombe
Posted

Welcome to the home stretch! In this last lesson, we'll add the finishing touches that turn our Breakout project into a complete, replayable game. Our focus will be on the overall game flow - managing how the game begins, how it can be won or lost, and how it can be started again.

We'll implement a GameState enum to keep track of the current situation. We'll use this state to trigger a loss when the ball misses the paddle and to provide visual feedback to the player when they win or lose. We'll also add a pause system, so the action doesn't start until the player is ready, and a simple keypress to restart the entire game from level one.

Finally, we'll look at how to prepare our project for distribution by creating a "release" build configuration that strips out the level editor and other development tools, leaving us with a clean, standalone game.

Ending and Restarting Games

Finally, let's implement our game ending and restarting logic.

Tracking GameState

Let's begin by tracking what state the game is currently in - whether the player has won, lost, or the game is still in progress. This is likely to be useful to a wide variety of games, so we'll add it to the Scene base class in our engine. We'll default it to InProgress:

Engine/Scene.h

// ...

enum class GameState { InProgress, Won, Lost };

class Scene {
public:
  // ...

  GameState GetState() const { return State; }
  
  void SetState(GameState NewState) {
    State = NewState;
  }

private:
  // ...
  GameState State{GameState::InProgress};
};

Back in our BreakoutScene class, let's set this state in response to the GAME_WON and GAME_LOST events. We're already broadcasting the GAME_WON event at the appropriate time and will implement the GAME_LOST scenario in the next section.

In our level skip cheat, we'll also set the game state to be InProgress. This allows our cheat to complete the level even if we lost:

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
    ) {
      SetState(GameState::InProgress);
      CompleteLevel();
    }
#endif
    else if (E.type == LAUNCH_LEVEL) {
      Load(E.user.code);
    } else if (E.type == GAME_WON) {
      SetState(GameState::Won);
    } else if (E.type == GAME_LOST) {
      SetState(GameState::Lost);
    } 
  }
  // ...
};

Losing Games

When the player misses the ball with their paddle, we want the game to end. We can detect this if the ball collides with our bottom wall. When that happens, we'll push our GAME_LOST event:

Breakout/Wall.h

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

// ...

class Wall : public Entity {
 public:
  // ...
  void HandleCollision(Entity& Other) override {
    if (
      dynamic_cast<Ball*>(&Other) &&
      Position == WallPosition::Bottom
    ) {
      SDL_Event E{UserEvents::GAME_LOST};
      SDL_PushEvent(&E);
    }
  }
  // ...
};

Restarting

We'll let the player restart the game by pressing the "R" key. When we detect that event, we'll set our game state back to InProgress, and load the first 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) {/*...*/} else if ( E.type == SDL_KEYDOWN && E.key.keysym.sym == SDLK_r ) { SetState(GameState::InProgress); Load(1); } } // ... };

Reacting to Game Loss and Victory

Now that win and loss events are being broadcast through our program, we're free to make any part of our application react to those events in the usual ways. Alternatively, we can check the state of our game at any time using the scene's GetState() function.

For example, let's override our scene's Render() function to change the background color depending on the game state. We'll fill the surface with a solid color first, and then call the base Scene's Render() method to render our entities on top of that rectangle:

Breakout/BreakoutScene.h

// ...

class BreakoutScene : public Scene {
public:
  // ...
  void Render(SDL_Surface* Surface) {
    if (GetState() == GameState::Won) {
      SDL_FillRect(Surface, nullptr,
        SDL_MapRGB(Surface->format, 20, 50, 20)
      );
    } else if (GetState() == GameState::Lost) {
      SDL_FillRect(Surface, nullptr,
        SDL_MapRGB(Surface->format, 50, 0, 0)
      );
    }
    Scene::Render(Surface);
  }
  // ...
};

With those changes, our background turns red when we lose:

Screenshot showing the game in the loss state

And green when we win:

Screenshot showing the game in the victory state

Pausing the Game

For our final feature, let's implement pausing. To support a paused state, let's first allow our physics and input to be paused. We'll pause them simply by disabling them, so let's update those components to support that.

Disabling Physics

To have our physics component pause when it is disabled, we can simply stop all of the updates that happen in our Tick() function:

Engine/ECS/Source/PhysicsComponent.cpp

// ...

void PhysicsComponent::Tick(float DeltaTime) {
  if (!GetIsEnabled()) return;
} // ...

Again, we may want to take this further. For example, in our current implementation, a disabled physics component will still allow ApplyForce() and ApplyImpulse() to be used. The effect of those will be to change the acceleration and velocity of the component, which will be used as soon as it's unpaused.

This may be the preferred approach. Alternatively, it may be more intuitive to have the component effectively ignore those instructions when disabled, perhaps with an error message.

Disabling Physics on Slow Frames

The time between frames, DeltaTime, is usually very small. However, our program can sometimes stall for a much longer period.

This might be because the operating system is busy, another application is hogging resources, or we may even pause our program intentionally in a debugger.

When our program starts responding again, a huge DeltaTime will have accumulated and, when multiplied by our Velocity, the ball may move a very large distance in a single step.

This can break our game, as it can make our ball move further than the thickness of our walls in a single step. This will cause the collision to be missed and the ball to no longer be within the window.

We can rarely detect that our program is going to stop updating in advance. Instead, on each iteration of our application loop, we can check if it has been a long time since the previous iteration. Below, we'll prevent our physics component from ticking on any iteration that takes longer than 0.1 seconds:

Engine/ECS/Source/PhysicsComponent.cpp

// ...

void PhysicsComponent::Tick(float DeltaTime) {
  if (DeltaTime > 0.1) return;
  // ...
}

// ...

An easy way to test this behavior on Windows is to move the window whilst our game is running. When this happens, Windows will pause the execution of our application loop. With our new DeltaTime check, as long as that pause is longer than 100 milliseconds, our physics simulation will pause too.

Move elegant options to deal specifically with this Windows behavior are discussed in this thread on SDL's GitHub repository.

Disabling Input

Our current input commands are setting the velocity of the physics component so, when the physics component is disabled, the input component is effectively disabled too.

However, that won't be true for any other inputs we might potentially use in the future, so let's explicitly support disabling our InputComponent.

To prevent inputs from having any effect on a disabled input component, it also shouldn't tick, which will therefore stop it from polling the keyboard state. We should also disable the HandleEvent() logic so that the input component stops responding to SDL_KEYDOWN events:

Engine/ECS/Source/InputComponent.cpp

// ...

void InputComponent::Tick(float DeltaTime) {
  if (!GetIsEnabled()) return;
} void InputComponent::HandleEvent( const SDL_Event& E) { if (!GetIsEnabled()) return;
}

Triggering the Game Pause

Currently, our ball starts moving as soon as the program starts. That can be a little annoying, so let's open the game in a paused state, and only start the level when the player is ready.

We'll do this by disabling our paddle's PhysicsComponent and InputComponent, and also our ball's PhysicsComponent. Let's add a new private helper function, and call it in the constructor:

Breakout/Paddle.h

// ...

class Paddle : public Entity {
public:
  Paddle(BreakoutScene& Scene) : Entity{Scene} {
    // ...

    SetIsPaused(true);
  }
  
  // ...
  
private:
  void SetIsPaused(bool isPaused) {
    Input->SetIsEnabled(!isPaused);
    Physics->SetIsEnabled(!isPaused);
  }

  // ...
};

Similarly, for our ball:

Breakout/Ball.h

// ...

class Ball : public Entity {
public:
  Ball(BreakoutScene& Scene) : Entity{Scene} {
    // ...

    SetIsPaused(true);
  }

  // ...
private:
  void SetIsPaused(bool isPaused) {
    Physics->SetIsEnabled(!isPaused);
  }
};

If we run our game now, it starts in a paused state. Every time we load a new level, we load a new ball and paddle, so we also effectively pause between levels.

Unpausing the Game

We'll let the user press their space bar to start each level. When they do that, and the game is in the InProgress state (that is, they haven't already won or lost), then we'll unpause our paddle:

Breakout/Paddle.h

// ...

class Paddle : public Entity {
public:
  // ...

  void HandleEvent(const SDL_Event& E) override {
    if (
      E.type == SDL_KEYDOWN &&
      E.key.keysym.sym == SDLK_SPACE &&
      GetScene().GetState() == GameState::InProgress
    ) {
      SetIsPaused(false);
    } else if (
      E.type == UserEvents::GAME_WON ||
      E.type == UserEvents::GAME_LOST
    ) {
      SetIsPaused(true);
    }
  }
  
  // ...

};

And similarly, for our ball:

Breakout/Ball.h

// ...

class Ball : public Entity {
public:
  // ...
  void HandleEvent(const SDL_Event& E) override {
    if (
      E.type == SDL_KEYDOWN &&
      E.key.keysym.sym == SDLK_SPACE &&
      GetScene().GetState() == GameState::InProgress
    ) {
      SetIsPaused(false);
    } else if (
      E.type == UserEvents::GAME_WON ||
      E.type == UserEvents::GAME_LOST
    ) {
      SetIsPaused(true);
    }
  }
  // ...
};

Refactoring Duplicate Code

We just added some duplicate code across our Paddle and Ball classes, with the HandleEvent() function in particular being identical. A small amount of duplication - particularly at the end of the project - isn't particularly problematic.

However, if we found ourselves needing to repeated this a third time, or if we were going to expand these functions with more identical logic in the future, we should consider refining our approach.

Our component system often gives us the natural way of solving problems like this - creating a new component that our entities can share. For example, a BreakoutPauseManager component might look something like this:

Breakout/BreakoutPauseManager.h

#pragma once
#include "Engine/ECS/Component.h"
#include "Engine/ECS/PhysicsComponent.h"
#include "Engine/ECS/InputComponent.h"
#include "Config.h"

class BreakoutPauseManager : public Component {
public:
  BreakoutPauseManager(Entity* Parent)
  : Component{Parent} {
    SetIsPaused(true);
  }

  void HandleEvent(const SDL_Event& E) override {
    if (
      E.type == SDL_KEYDOWN &&
      E.key.keysym.sym == SDLK_SPACE &&
      GetScene().GetState() == GameState::InProgress
    ) {
      SetIsPaused(false);
    } else if (
      E.type == UserEvents::GAME_WON ||
      E.type == UserEvents::GAME_LOST
    ) {
      SetIsPaused(true);
    }
  }

private:
  void SetIsPaused(bool isPaused) {
    if (InputComponent* Input{
      GetOwner()->GetComponent<InputComponent>()
    }) {
      Input->SetIsEnabled(!isPaused);
   }

   if (PhysicsComponent* Physics{
     GetOwner()->GetComponent<PhysicsComponent>()
   }) {
     Physics->SetIsEnabled(!isPaused);
   }
  }
};

We could then delete the HandleEvent() and SetIsPaused() functions from both our Paddle and Ball classes, and instead attach a BreakoutPauseManager component to them in their constructors:

Breakout/Paddle.h

// ...
#include "Breakout/BreakoutPauseManager.h"

class Paddle : public Entity {
public:
  Paddle(BreakoutScene& Scene) : Entity{Scene} {
    // ...

    SetIsPaused(true); 
    AddComponent<BreakoutPauseManager>();
  }
  
  void HandleEvent(const SDL_Event& E) override {
    if (
      E.type == SDL_KEYDOWN &&
      E.key.keysym.sym == SDLK_SPACE &&
      GetScene().GetState() == GameState::InProgress
    ) {
      SetIsPaused(false);
    } else if (
      E.type == UserEvents::GAME_WON ||
      E.type == UserEvents::GAME_LOST
    ) {
      SetIsPaused(true);
    }
  }
  
  // ...
  
private:
  void SetIsPaused(bool isPaused) {
    Input->SetIsEnabled(!isPaused);
    Physics->SetIsEnabled(!isPaused);
  }
  
  // ...
};

Breakout/Ball.h

// ...
#include "Breakout/BreakoutPauseManager.h"

class Ball : public Entity {
public:
  Ball(BreakoutScene& Scene) : Entity{Scene} {
    // ...

    SetIsPaused(true);
    AddComponent<BreakoutPauseManager>();
  }
  
  // ...

  void HandleEvent(const SDL_Event& E) override {
    if (
      E.type == SDL_KEYDOWN &&
      E.key.keysym.sym == SDLK_SPACE &&
      GetScene().GetState() == GameState::InProgress
    ) {
      SetIsPaused(false);
    } else if (
      E.type == UserEvents::GAME_WON ||
      E.type == UserEvents::GAME_LOST
    ) {
      SetIsPaused(true);
    }
  }

private:
  void SetIsPaused(bool isPaused) {
    Physics->SetIsEnabled(!isPaused);
  }

  // ...
};

Excluding the Editor

With all our features in place, we're now ready to ship our game! The last step is to disable any undesirable preprocessor definitions (such as DRAW_DEBUG_HELPERS and ENABLE_CHEATS.

We also need detach our editor from the project. Our files already include preprocessor directives that will exclude editor-specific logic when the WITH_EDITOR macro is not defined.

However, our editor source files are still being sent to the compiler, and that may cause compilation issues or other problems. The simplest way to solve this is to include the #ifdef checks in those files, too. This means that, when WITH_EDITOR isn't defined, those files will be empty by the time the compiler sees them.

For example:

Editor/Source/Actor.cpp

#ifdef WITH_EDITOR
#endif

A more elegant solution would involve conditionally not sending those files to be compiled at all. The build management tools included in most IDEs should let us automate this.

In Visual Studio, for example, you can select the files in the Solution Explorer, right-click on them, click on Properties, and set Excluded From Build to "Yes" for any build configuration that should not include the editor:

Screenshot showing excluding files in Visual Studio

If you're using CMake, the simplest option is to set() a variable. We can then append the files to the build and set the preprocessor directive only if that variable is ON:

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 20)

# Change me to ON to enable the editor
SET(WITH_EDITOR OFF)

project(Breakout VERSION 1.0.0)

set(SOURCE_FILES
  "main.cpp"

  "Engine/Source/Scene.cpp"
  "Engine/ECS/Source/Component.cpp"
  "Engine/ECS/Source/CollisionComponent.cpp"
  "Engine/ECS/Source/ImageComponent.cpp"
  "Engine/ECS/Source/PhysicsComponent.cpp"
  "Engine/ECS/Source/InputComponent.cpp"
  "Engine/ECS/Source/Commands.cpp"

  "Breakout/Source/BreakoutScene.cpp"
  "Breakout/Source/Ball.cpp"
)

# Include the editor files only if WITH_EDITOR is ON
if(WITH_EDITOR) 
  list(APPEND SOURCE_FILES 
    "Editor/Source/Button.cpp" 
    "Editor/Source/Blocks.cpp" 
    "Editor/Source/Actor.cpp" 
    "Editor/Source/ActorTooltip.cpp" 
    "Editor/Source/Level.cpp" 
  ) 
endif() 

add_executable(Breakout
  ${SOURCE_FILES}
)

target_compile_definitions(Breakout PUBLIC
  CHECK_ERRORS
  # ENABLE_CHEATS
  # DRAW_DEBUG_HELPERS
)

# Conditionally set the WITH_EDITOR preprocessor definition
if(WITH_EDITOR) 
  target_compile_definitions(Breakout PUBLIC 
    WITH_EDITOR 
  ) 
endif() 

target_include_directories(
  Breakout PUBLIC
  ${PROJECT_SOURCE_DIR}
)

add_subdirectory(external/SDL)
add_subdirectory(external/SDL_image)
add_subdirectory(external/SDL_ttf)

target_link_libraries(Breakout PRIVATE
  SDL2
  SDL2_image
  SDL2_ttf
)

if (WIN32)
  target_link_libraries(
    Breakout PRIVATE SDL2main
  )
endif()

set(AssetDirectory "${PROJECT_SOURCE_DIR}/Assets")
add_custom_command(
  TARGET Breakout POST_BUILD
  COMMAND
    ${CMAKE_COMMAND} -E copy_if_different
      "$<TARGET_FILE:SDL2>"
      "$<TARGET_FILE:SDL2_image>"
      "$<TARGET_FILE:SDL2_ttf>"
      "$<TARGET_FILE_DIR:Breakout>"
  COMMAND
    ${CMAKE_COMMAND} -E copy_directory_if_different
      "${AssetDirectory}"
      "$<TARGET_FILE_DIR:Breakout>/Assets"
  VERBATIM
)

With those changes, we can now easily prepare a version of our game that doesn't include our developer tools. Remember, our game still needs the output of those tools, such as the "Level1.bin" files.

Ensure that those are included alongside all the other assets (such as image files, fonts, and SDL's library files) that our program needs at run time.

Complete Code

Complete versions of all the files we updated in this lesson are available below:

Files

Breakout
Editor
Engine
CMakeLists.txt
Select a file to view its content

A zip file containing all of the code is available here:

The fonts and images used in our screenshots are available here:

Summary

Our Breakout clone is now finished! This lesson was about adding the final logic that defines a complete game experience.

We introduced a GameState enum, implemented win and loss conditions, gave the player a way to restart, and created a pause system that activates at the start of levels and on game-end conditions.

Finally, we prepared our build system to easily exclude the editor for a release version.

Here's what we've accomplished in this project:

  • We built a fully featured Breakout clone with multiple levels.
  • We created a data-driven workflow with a separate level editor.
  • We implemented a component-based engine for managing game entities.
  • We handled player input, physics, and complex collision responses.
  • We managed the overall game state, including winning, losing, pausing, and restarting.
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