Breakout: The Ball

This lesson focuses on creating the Ball class, customizing our physics engine, and launching the ball.
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 2
Ryan McCombe
Ryan McCombe
Posted

Now that our project is running, we can start building the game itself. The first element we need is the ball. This lesson is dedicated to bringing our game's ball to life, from its visual representation to its movement.

We will begin by creating a new Ball class. This class will use our existing entity-component system to manage its properties. We'll add components for its position, image, and collision bounds. We'll also add a physics component and learn how to customize it for our game by disabling gravity.

Our key goals for this lesson are to:

  • Create a Ball entity class.
  • Add and configure the necessary components.
  • Modify the engine's PhysicsComponent to support zero-gravity movement.
  • Set an initial velocity to get the ball moving.

Adding a Ball Class

Let's start by adding a class to manage our ball. It'll inherit from our engine's Entity class and, like any Entity, it needs a reference to the scene it is part of.

We'll also give our Ball a transform component and image component so we can see it, and we'll position it somewhere near the middle of the scene for now.

We'll also save the pointer to the TransformComponent as we'll need it later:

Breakout/Ball.h

#pragma once
#include "Engine/ECS/Entity.h"
#include "Engine/ECS/ImageComponent.h"
#include "Engine/ECS/TransformComponent.h"
#include "Breakout/BreakoutScene.h"

class Ball : public Entity {
public:
  Ball(BreakoutScene& Scene) : Entity{Scene} {
    Transform = AddComponent<TransformComponent>();
    Transform->SetPosition({6, 7});
    AddComponent<ImageComponent>("Assets/Grey.png");
  }

private:
  TransformComponent* Transform;
};

Let's add a Ball to our scene. Loading levels will be quite a complex process eventually, especially once we start deserializing data from our editor files.

So, let's proactively break it out into a standalone function called Load(), which will take the level we want to load as an argument. From our constructor, we'll Load() level 1:

Breakout/BreakoutScene.h

#pragma once
#include <SDL.h>
#include "Engine/Scene.h"

class BreakoutScene : public Scene {
public:
  BreakoutScene(Window& ParentWindow)
    : Scene{ParentWindow} {
    Load(1);
  }

  void Load(int Level);
};

As a reminder, the entities of our scene is in a std::vector called Entities, which our BreakoutScene is inheriting from the base Scene class in our engine.

Every time we call Load(), we will clear all the entities in the scene, and load our new set of entities. For now, the only thing we have to load is our Ball:

Breakout/Source/BreakoutScene.cpp

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

void BreakoutScene::Load(int Level) {
  Entities.clear();
  Entities.emplace_back(
    std::make_unique<Ball>(*this));
}

With these changes, we should now see our ball rendered in the scene:

A screenshot of the ball rendered in the scene

Configuring Size and Colliders

For our ball to bounce around and collide with stuff, we need to give it a collider. Let's add a CollisionComponent, and save a pointer to this component as a member variable as we'll need it later.

We also need to set the size of the collider to be around the same size as the ball, using the SetSize() method. In our example, the correct size seems to be a width of around 1.1 and a height of 1.7, but yours may be different:

Breakout/Ball.h

#pragma once
#include "Engine/ECS/Entity.h"
#include "Engine/ECS/ImageComponent.h"
#include "Engine/ECS/TransformComponent.h"
#include "Engine/ECS/CollisionComponent.h"
#include "Breakout/BreakoutScene.h"

class Ball : public Entity {
public:
  Ball(BreakoutScene& Scene) : Entity{Scene} {
    Transform = AddComponent<TransformComponent>();
    Transform->SetPosition({6, 7});
    
    Collision = AddComponent<CollisionComponent>();
    Collision->SetSize(1.1, 1.7);
    
    AddComponent<ImageComponent>("Assets/Grey.png");
  }

private:
  TransformComponent* Transform;
  CollisionComponent* Collision;
};

Remember, if you enable the DRAW_DEBUG_HELPERS preprocessor definition, the collider will draw a yellow rectangle showing its position and size:

A screenshot of the ball rendered in the scene with debug helpers drawn

If you also need to change the position of the collider, you can use the SetOffset() method on the CollisionComponent.

Our ball is too large at the minute, so we'll also use the SetScale() method on the transform component to scale it down.

We'll also update the starting position to near the bottom of the scene, with enough space to put the paddle underneath it in the future:

Breakout/Ball.h

// ...

class Ball : public Entity {
public:
  Ball(BreakoutScene& Scene) : Entity{Scene} {
    Transform = AddComponent<TransformComponent>();
    Transform->SetPosition({6.1, 1.6});
    Transform->SetScale(0.3);
    
    Collision = AddComponent<CollisionComponent>();
    Collision->SetSize(1.1, 1.7);
    
    AddComponent<ImageComponent>("Assets/Grey.png");
  }

  // ...
};
A screenshot of the ball rendered at the correct size and position

Disabling Gravity

To get our ball moving, we can add a PhysicsComponent, but we need to make a small enhancement to it first. Our PhysicsComponent assumes we always want to simulate gravity, but that's not the case in our game.

Let's update the component with a new Vec2 to store the desired gravity effect, as well as a getter and setter to access it:

Engine/ECS/PhysicsComponent.h

// ...

class PhysicsComponent : public Component {
 public:
  // ...

  Vec2 GetGravity() const {
    return Gravity;
  }
  void SetGravity(Vec2 NewGravity) {
    Gravity = NewGravity;
  }

 private:
  // ...
  Vec2 Gravity{0.0, -9.8}; // m/s^2
};

In the physics component's Tick() function, we can now use this value, rather than the fixed local variable we were using previously:

Engine/ECS/Source/PhysicsComponent.cpp

// ...

void PhysicsComponent::Tick(float DeltaTime) {
  const Vec2 GRAVITY{0.0f, -9.8f};
  ApplyForce(GRAVITY * Mass);
  ApplyForce(GetGravity() * Mass);

  Velocity += Acceleration * DeltaTime;

  SetOwnerPosition(
    GetOwnerPosition() + Velocity * DeltaTime
  );

  Acceleration = {0.0, 0.0};
}

Adding Movement

Now that we can control gravity, let's add a PhysicsComponent to our Ball, and disable the effects of gravity by setting it to {0, 0}.

We'll also need to use our physics component in other Ball methods in the future, so we'll store its pointer as a member variable:

Breakout/Ball.h

// ...
#include "Engine/ECS/PhysicsComponent.h"

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

    Physics = AddComponent<PhysicsComponent>();
    Physics->SetGravity({0, 0});
    
    AddComponent<ImageComponent>("Assets/Grey.png");
  }

private:
  // ...
  PhysicsComponent* Physics;
};

Tick Order

The order in which we add components to our entity influences the order in which those components will tick. This sometimes affects the behavior of our program in subtle ways. For example, the physics component updates the object's position, and the image component uses the object's position to decide where to render.

As such, if we add the image component after the physics component, that means the image component's tick function will be using the latest positional data, which is what we want.

If the image component ticked before the physics component, it would be using the position data from the previous frame. This is rarely noticeable and sometimes not worth worrying about, but reordering the insertion order of our components is pretty easy, so we may as well be mindful of it.  Specifically:

  • InputComponent::Tick() updates the physics component's Velocity, and PhysicsComponent::Tick() uses that Velocity. Therefore, input components should be added before the physics component.
  • PhysicsComponent::Tick() updates the transform component's position, and ImageComponent::Tick() and CollisionComponent::Tick() use that position. Therefore, image and collision components should be added after the physics component.

To get our ball moving, let's first add a configuration variable that lets us easily change the speed in the future:

Config.h

// ...

namespace Config::Breakout {
inline const float BALL_SPEED{10};
}

// ...

Back in our Ball class, we can set the velocity in three steps:

  1. We'll create a vector to set the initial direction of our ball. You can use any value you prefer here - we'll use {1, 2}, meaning the ball will move up and to the right.
  2. We'll normalize this vector, thereby ensuring it has a length of 1.
  3. We'll multiply this normalized vector by the BALL_SPEED configuration value.
Example of normalizing a vector

These steps ensure our velocity matches both the direction ({1, 2}) and speed (10) that we configured.

Breakout/Ball.h

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

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

    Physics = AddComponent<PhysicsComponent>();
    Physics->SetGravity({0, 0});
    Physics->SetVelocity(Vec2{1, 2}.Normalize()
      * Config::Breakout::BALL_SPEED);
  }

private:
  TransformComponent* Transform;
  PhysicsComponent* Physics;
  CollisionComponent* Collision;
};

With those changes, our ball should now start moving, eventually leaving our screen entirely. We've enabled DRAW_DEBUG_HELPERS again for this screenshot, which causes our movement trajectories to be drawn as blue lines:

A screenshot of the ball moving

In the next part, we'll add some walls around the edges of the window that our ball will bounce off.

Complete Code

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

Files

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

Summary

In this lesson, we successfully added the first interactive element to our Breakout game: the ball. We created a dedicated Ball class, equipped it with transform, image, collision, and physics components, and set it in motion. We also enhanced our engine's PhysicsComponent to allow for zero-gravity environments.

Here are the key takeaways:

  • Game-specific objects are created as Entity subclasses that compose generic components.
  • The order in which components are added can affect behavior, particularly between physics, collision, and rendering.
  • It's often necessary to adapt generic engine features (like gravity in PhysicsComponent) to suit the specific needs of a game.
  • Vector normalization is a useful technique for setting an object's velocity, allowing us to define a direction and speed independently.
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
Lesson Contents

Breakout: The Ball

This lesson focuses on creating the Ball class, customizing our physics engine, and launching the ball.

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