Breakout: Improving Paddle Physics
We'll add detailed collision logic to the paddle, giving the player control over the ball's trajectory.
Right now, our paddle behaves just like a moving wall. This is functional, but it doesn't give the player much control. In a classic Breakout game, where you hit the ball on the paddle determines its new direction. This lesson is all about implementing that nuanced physical interaction.
We will write code that calculates the precise point of impact on the paddle and uses that information to construct a new velocity for the ball. This will transform the gameplay from a simple reaction test into a game of skill, where players can aim their shots.
Additionally, we'll tackle a common problem in game physics: rapid, repeated collisions. We'll enhance our Component
class with an isEnabled
flag and use SDL_Timer
to temporarily disable the paddle's collider after a hit, ensuring clean, single bounces.
Enhancing CollisionComponent
Before we start updating our paddle, let's update our CollisionComponent
with some new capabilities that will be useful. We'll add a GetSize()
method, that returns the values stored in the private Width
and Height
members that already exist.
We'll also add a GetCenter()
method, which will return the position at the center of the collision rectangle. WorldBounds
is an SDL_FRect
where the {x, y}
position represents the top left of the rectangle. Therefore, to get the centre, we need to add half the width to the x
value, and subtract half the height from the y
value:
Engine/ECS/CollisionComponent.h
// ...
class CollisionComponent : public Component {
public:
// ...
Vec2 GetSize() const { return {Width, Height}; }
Vec2 GetCenter() const {
auto [x, y, w, h]{GetWorldBounds()};
return {
x + w / 2,
y + h / -2,
};
}
// ...
};
Let's also update DrawDebugHelpers()
to show this center using a small rectangle. We'll also add a Color
variable so the previous code and our new rectangle can both share it:
Engine/ECS/Source/CollisionComponent.cpp
// ...
void CollisionComponent::DrawDebugHelpers(
SDL_Surface* Surface
) {
Uint32 Color{SDL_MapRGB(
Surface->format, 255, 255, 0
)};
SDL_FRect ScreenBoundsF{
GetScene().ToScreenSpace(WorldBounds)};
SDL_Rect ScreenBounds{
Utilities::Round(ScreenBoundsF)};
Utilities::DrawRectOutline(
Surface,
ScreenBounds,
SDL_MapRGB(Surface->format, 255, 255, 0),
Color,
1
);
Vec2 CenterPoint{
GetScene().ToScreenSpace(GetCenter())
};
SDL_Rect CenterRect{Utilities::Round({
CenterPoint.x - 3,
CenterPoint.y - 3,
6, 6
})};
SDL_FillRect(Surface, &CenterRect, Color);
}
// ...

Influencing Ball Direction
The code that allows the paddle to change the ball's velocity will be quite long, so let's split it off into a new private method. Within our HandleCollision()
function, we'll call our new private helper if the collision was with a Ball
:
Breakout/Paddle.h
// ...
#include "Breakout/Ball.h"
class Paddle : public Entity {
public:
// ...
void HandleCollision(Entity& Other) override {
if (Ball* Ptr{dynamic_cast<Ball*>(&Other)}) {
HandleBallCollision(Ptr);
}
// ...
}
// ...
private:
void HandleBallCollision(Ball* BallPtr) {
// We'll implement this next
}
// ...
};
The Plan
Calculating the velocity we want to apply to the ball can be done in three steps.
First, we'll compare the horizontal positions of our ball and paddle colliders using our new GetCenter()
method. This will let us determine where on the paddle our ball hit. To make the future maths easier, we'll map this range to a value from -1.0
(far left) to 1.0
(far right)

Next, we'll construct a vector representing the desired new direction of our ball. We'll use the value we calculated in the previous step to set the horizontal component, and we'll set the vertical component to a positive value, such as 1.0
, representing an upward direction.

We'll use this direction to set a new velocity for our ball. However, our direction vectors can have different lengths - for example, {-1, 1}
is slightly longer than {0, 1}
. So, we'll follow the typical approach of normalizing the vector to get one pointing the same direction but with a length of 1.0
:

We can then multiply that normalized direction vector with the BallSpeed
value we set in our config:

Let's implement this in code
Getting Collision Point
In the previous section, we added GetCenter()
and GetSize()
functions to our collision components. We'll use them here to get the position of the ball relative to the paddle. We can do this simply by subtracting the ball's horizontal position from the paddle's horizontal position.
The maximum possible value this will return is half of the combined width of the paddle and ball, whilst its minimum will be the negative form of that same distance. For example, if our paddle's width was 100
and our ball's width was 20
, this value would range from -60
to 60
:

To convert this to a value ranging from -1
to 1
, we'd divide it by 60
. Or, more generally, we'd divide it by (PaddleWidth + BallWidth)/2
.
We'll call this value HitOffset
:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
// ...
private:
void HandleBallCollision(Ball* BallPtr) {
Vec2 PaddlePos{Collision->GetCenter()};
float PaddleWidth{Collision->GetSize().x};
CollisionComponent* BallCollision{
BallPtr->GetComponent<CollisionComponent>()
};
Vec2 BallPos{BallCollision->GetCenter()};
float BallWidth{BallCollision->GetSize().x};
// Where on the paddle the ball hit from
// -1.0 (left edge) to 1.0 (right edge)
float HitOffset{
(BallPos.x - PaddlePos.x) / (
(PaddleWidth + BallWidth) / 2
)
};
}
// ...
};
Creating Direction Vector
Next, we'll use this offset to create a direction vector. We want the horizontal direction to be influenced by where the ball hit the paddle, so we'll set the x
component to be our HitOffset
value.
We always want the ball to start moving upwards after hitting the paddle, so we'll set the y
component to any positive value, such as :
Breakout/Paddle.h
// ...
class Paddle : public Entity {
// ...
private:
void HandleBallCollision(Ball* BallPtr) {
Vec2 Direction{HitOffset, 1.0};
}
// ...
};
Setting New Velocity
Finally, we'll use this direction vector to update the ball's velocity, using the SetVelocity()
function of its PhysicsComponent
.
We want the ball's speed - that is, the magnitude of its velocity - to match what we set in the configuration file.
To do this, we'll first normalize the Direction
vector to generate a vector with the same direction, but a magnitude of . We'll then multiply this unit vector by the speed we want the ball to have, which is stored in our Config::Breakout
namespace:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
// ...
private:
void HandleBallCollision(Ball* BallPtr) {
PhysicsComponent* BallPhysics{
BallPtr->GetComponent<PhysicsComponent>()
};
BallPhysics->SetVelocity(
Direction.Normalize() *
Config::Breakout::BALL_SPEED
);
}
// ...
};
Disabling Bidirectional Interactions
Now that our paddle is setting the velocity of our ball in response to the collision, having our ball's bouncing logic also trigger as a result of that collision may cause some conflicting interactions.
We can prevent our Ball
from reacting to Paddle
collisions, but continue to react to other collisions, by returning early if it detects the Other
entity is the paddle:
Breakout/Source/Ball.cpp
// ...
#include "Paddle.h"
void Ball::HandleCollision(Entity& Other) {
if (dynamic_cast<Paddle*>(&Other)) {
return;
}
// ...
}
With these changes, we can now exhibit some control over the ball's movement by bouncing it off different parts of our paddle:

Disabling Components
In our current implementation, we can possibly create a situation where the ball collides with our paddle multiple times in quick succession. This is more likely to happen if the ball hits the side of the paddle, whilst the paddle is also moving towards the ball.
This is a relatively common problem in physics systems, and we can address it by disabling a collider for a brief moment after it hits something.
The ability to "disable" a component is quite useful in general, so let's add this capability to the base Component
class in our Engine/ECS
directory.
Adding isEnabled
to Component
To support this feature, we'll add a private isEnabled
value, defaulted to true
, alongside getters and setters:
Engine/ECS/Component.h
// ...
class Component {
public:
// ...
void SetIsEnabled(bool Enabled) {
isEnabled = Enabled;
}
bool GetIsEnabled() const {
return isEnabled;
}
private:
// ...
bool isEnabled{true};
};
Disabling Collision Components
Classes that derive from Component
can now decide how they should act when they have been disabled. We'll implement this for the CollisionComponent
in this section and for some more components later in the project.
The most important effect of a collision component being disabled is that it should no longer result in HandleCollision()
calls being made.
This logic is currently implemented in our Scene.cpp
file, so let's update it:
Engine/Source/Scene.cpp
// ...
void Scene::CheckCollisions() {
for (size_t i{0}; i < Entities.size(); ++i) {
CollisionComponent* ColA{
Entities[i]
->GetComponent<CollisionComponent>()};
if (!ColA) {
if (!(ColA && ColA->GetIsEnabled())) {
continue;
}
for (size_t j{i+1}; j < Entities.size(); ++j) {
CollisionComponent* ColB{
Entities[j]
->GetComponent<CollisionComponent>()};
if (!ColB) {
if (!(ColB && ColB->GetIsEnabled())) {
continue;
}
if (ColA->IsCollidingWith(*ColB)) {
Entities[i]->HandleCollision(
*Entities[j]);
Entities[j]->HandleCollision(
*Entities[i]);
}
}
}
}
// ...
Updating some behaviors in the CollisionComponent
class may also be warranted, but it's less obvious what should happen there. For example, if the component stops ticking, that means its position stops updating, and functions like GetCenter()
will report the wrong value.
We could update a function like IsCollidingWith()
to always return false
if the component is disabled, but that may also be unintuitive.
However, it does seem safe to at least update DrawDebugHelpers()
to represent the component being disabled. Previously, it was always rendered in yellow. Let's update it to render in orange when it is disabled:
Engine/ECS/Source/CollisionComponent.cpp
// ...
void CollisionComponent::DrawDebugHelpers(
SDL_Surface* Surface
) {
SDL_Color Yellow{255, 255, 0, 255};
SDL_Color Orange{255, 165, 0, 255};
auto [r, g, b, a]{
GetIsEnabled() ? Yellow : Orange
};
Uint32 Color{SDL_MapRGB(Surface->format, r, g, b)};
}
// ...
Preventing Double Collisions
Back in our Paddle
class, we can now simply call this function when we want to disable or enable collisions. Let's disable them when a collision happens, and we'll work on enabling them again a few moments later in the next section:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
// ...
void HandleCollision(Entity& Other) override {
// ...
Collision->SetIsEnabled(false);
}
// ...
};
We should now see our paddle's collider rendering in orange after a collision:

Reenabling Collisions on a Timer
To enable collisions after a brief pause, let's use the SDL_Timer
techniques we covered earlier in the course.
SDL2 Timers and Callbacks
Learn how to use callbacks with SDL_AddTimer()
to provide functions that are executed on time-based intervals
We'll start by ensuring SDL's timer functionality is included in our SDL_Init()
call:
main.cpp
// ...
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
// ...
}
To set a timer, we pass three arguments to SDL_AddTimer()
:
- The interval, in milliseconds
- A function pointer that we want to invoke at the end of the timer
- A pointer to some data we want to pass to the callback function. This argument is a void pointer (
void*
) so we can pass a pointer to anything, or anullptr
if we don't need any additional data.
Let's set a timer to enable our collisions after 500 milliseconds. We'll implement this behavior in a function called Paddle::EnableCollision
, and we'll pass a pointer to ourselves (this
) as the final argument.
We'll need the timer ID returned by this function later, so let's save it as a member variable:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
// ...
void HandleCollision(Entity& Other) override {
// ...
Collision->SetIsEnabled(false);
TimerID = SDL_AddTimer(
500, &Paddle::EnableCollision, this
);
}
private:
// ...
SDL_TimerID TimerID{0};
static Uint32 EnableCollision(
Uint32 Interval, void* Entity
) {
Paddle* Target{static_cast<Paddle*>(Entity)};
// Memory issue here - see next section
if (Target && Target->Collision) {
Target->Collision->SetIsEnabled(true);
}
return 0;
}
};
The callback we pass to SDL_AddTimer()
must be a plain function pointer that returns a Uint32
and accepts Uint32
and void*
arguments.
- The return value allows us to repeat the timer by passing the next interval, which will cause the function to be invoked again after that many milliseconds. We can pass
0
if we don't need the timer to repeat, which is what we want in this case. - The
Uint32
parameter is provided with the current interval, which is500
in our example. We don't need this value so we'll just ignore it. - The
void*
parameter is the pointer we passed toSDL_AddTimer()
, so it is the pointer to ourPaddle
instance.
The function passed to SDL_AddTimer()
must be a plain function pointer, not a member function pointer. However, we can use a static member function:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
// ...
private:
// ...
static Uint32 EnableCollision(
Uint32 Interval, void* Entity
) {
return 0;
}
};
To enable our collider from this function, we need to cast the void pointer to a Paddle*
, and then call SetIsEnabled()
on its collision component.
Note that the following code isn't memory-safe. We'll explain why and fix it in the next section:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
// ...
private:
// ...
static Uint32 EnableCollision(
Uint32 Interval, void* Entity
) {
Paddle* Target{static_cast<Paddle*>(Entity)};
// Memory issue here - see next section
if (Target && Target->Collision) {
Target->Collision->SetIsEnabled(true);
}
return 0;
}
};
With these changes, our paddle's collider should now renable 500 milliseconds after the collision:

Fixing Timer Issues
When working with asynchronous code such as timers, we should be more mindful of things that can go wrong. Whilst synchronous code typically only needs to worry about the state of the program right now, asynchronous code needs to consider what might happen between us setting up the timer, and that callback being invoked in the future.
For example, what if HandleCollision()
is called a second time, whilst our timer is still running? Our callback is still going to be called on its original schedule, meaning collisions will be reenabled again sooner than we intended:

This second collision is not currently possible in our program - HandleCollision()
is only called by Scene
, and Scene
will not call it if the collision component is disabled.
However, we can still be safe and ensure Paddle()
is internally consistent. HandleCollision()
is a public function, so we should be prepared for external code to call it at any time. To handle this situation more gracefully, when HandleCollision()
is called, we can cancel the existing timer, if there is one

In code, it might look like this:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
// ...
void HandleCollision(Entity& Other) override {
// ...
Collision->SetIsEnabled(false);
if (TimerID != 0) {
SDL_RemoveTimer(TimerID);
}
TimerID = SDL_AddTimer(
500, &Paddle::EnableCollision, this
);
}
~Paddle() {
if (TimerID) {
SDL_RemoveTimer(TimerID);
}
}
// ...
};
Memory-Safe Timers
The second thing we should be mindful of is the state of our program at the time the callback is invoked. What state could our paddle be in? Does the paddle even still exist?
Currently, our paddle remains alive for the duration of our program, but we'll be changing that soon. If the paddle is deleted between the timer starting and the callback being invoked, the void*
the callback receives will be a dangling pointer.
To address this, we should also remove our timer when the paddle is destroyed:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
// ...
~Paddle() {
if (TimerID) {
SDL_RemoveTimer(TimerID);
}
}
// ...
};
Rule of Three
Now that we've intervened in the object destruction process, the rule of three should prompt us to consider whether we need to intervene in the copying process, too. Will our paddle objects be compatible with the default copy behavior?
Unfortunately, they won't, as copies will have the same TimerID
as the original object. This means that paddle objects risk calling SDL_RemoveTimer()
for timers that they did not create, and aren't responsible for managing.
In our case, we don't need to fix this, because we don't need paddles to be copyable. So instead, let's make that explicit by deleting the copy constructor and copy assignment operators:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
// ...
Paddle& operator=(const Paddle& Other) = delete;
Paddle(const Paddle& Other) = delete;
// ...
};
Complete Code
Complete versions of the files we created in this lesson are below:
Files
Summary
In this lesson, we significantly improved our game's physics and interactivity. We implemented logic that allows the paddle to control the ball's direction, turning simple bounces into skillful shots.
We also built a robust system for handling rapid collisions by temporarily disabling the paddle's collider using an SDL timer, and we discussed the memory safety considerations that come with asynchronous code.
Key takeaways from this lesson include:
- Gameplay depth can be added by implementing nuanced collision responses.
- Calculating the relative position of two colliders is key to determining how they should interact.
- A generic
isEnabled
flag on components is a flexible way to manage their state. - SDL timers are useful for creating delayed actions, like re-enabling a component.
- When using timers or other asynchronous callbacks, it's crucial to manage resource lifetimes and prevent dangling pointers, often by following the Rule of Three/Five.
Breakout: Loading Levels
Add breakable bricks to the game by loading and parsing level files saved from our level editor.