Breakout: The Player Paddle
We'll build the player's paddle, hook it up to keyboard controls, and keep it from moving off-screen.
It's time to give the player some control! In this lesson, we will implement the paddle, the primary way the player interacts with the world of Breakout. We will build upon our entity-component system to create a paddle that moves left and right in response to keyboard input.
We will create a Paddle
entity and add components to it. A key focus will be on the InputComponent
and PhysicsComponent
. We'll explore how to customize the default input bindings, implement a "press to move, release to stop" control scheme, and add a new capability to our physics engine to constrain movement within a defined area.
In this lesson, we will:
- Create a
Paddle
entity class. - Bind the left and right arrow keys to paddle movement.
- Implement logic to reset velocity when no key is pressed.
- Add movement constraints to the
PhysicsComponent
to keep the paddle on-screen.
Adding a Paddle Class
As usual, let's begin by adding a class to manage our paddle. We'll start with a transform component and image component, and we'll save the transform component pointer to use later:
Breakout/Paddle.h
#pragma once
#include "Engine/ECS/Entity.h"
#include "Engine/ECS/TransformComponent.h"
#include "Engine/ECS/ImageComponent.h"
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
Transform = AddComponent<TransformComponent>();
AddComponent<ImageComponent>(
"Assets/Paddle_Frame_B.png"
);
}
private:
TransformComponent* Transform{nullptr};
};
Over in our BreakoutScene
, let's add an instance of our class to the scene within the Load()
method:
Breakout/BreakoutScene.cpp
// ...
#include "Breakout/BreakoutScene.h"
void BreakoutScene::Load(int Level) {
// ...
Entities.emplace_back(
std::make_unique<Paddle>(*this));
}
We won't see our paddle yet as it is positioned outside of the viewport. Let's update its position and set up its collider.
Configuring Position and Colliders
We'll set the initial position of our paddle to be just under the ball we added previously. For the image we're using in our examples, that corresponds to a position of approximately {4, 1}
.
We can also toggle on the DRAW_DEBUG_HELPERS
flag to help us position our collider. For our image, a collider size of {2.7, 0.9}
works well. We want to make sure the paddle isn't colliding with the bottom wall.
The image we're using has a transparent area on the left and right, so we also need to use SetOffset()
to move the collider to match up with the visual representation of the paddle. Moving it left by 0.95
units works for the image we're using:
Breakout/Paddle.h
// ...
#include "Engine/ECS/CollisionComponent.h"
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
Transform = AddComponent<TransformComponent>();
Transform->SetPosition({4, 1});
Collision = AddComponent<CollisionComponent>();
Collision->SetSize(2.7, 0.9);
Collision->SetOffset({0.95, 0});
AddComponent<ImageComponent>(
"Assets/Paddle_Frame_B.png");
}
private:
TransformComponent* Transform{nullptr};
CollisionComponent* Collision{nullptr};
};

Configuring Inputs
Next, let's update our paddle to respond to inputs. We'll add an InputComponent
and PhysicsComponent
, saving their pointers for later:
Breakout/Paddle.h
// ...
#include "Engine/ECS/InputComponent.h"
#include "Engine/ECS/PhysicsComponent.h"
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
// ...
Input = AddComponent<InputComponent>();
Physics = AddComponent<PhysicsComponent>();
Physics->SetGravity({0, 0});
// ...
}
private:
InputComponent* Input{nullptr};
PhysicsComponent* Physics{nullptr};
};
Our base input component automatically binds the left and right arrow to horizontal movement, and the space bar to jumping. We'll refine this movement in the next section.
Resetting Velocity
When we press the left or right arrow key, our paddle continues to move in that direction until we press the opposite arrow key. That may be what we want, but let's walk through changing this behavior.
In our physics chapter, we saw an example of simulating forces like friction, which will cause our paddle to slow down over time once we stop providing any movement input.
In this case, we'll simply stop our paddle immediately. To do this, we can override the Tick()
function and set the physics component's velocity back to {0, 0}
.
We should do this after calling the base Entity::Tick()
. If we did it before, our paddle wouldn't move, because we'd remove the velocity before the physics component's Tick()
function gets to use it.
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
// ...
void Tick(float DeltaTime) override {
Entity::Tick(DeltaTime);
Physics->SetVelocity({0, 0});
}
// ...
};
Updating Commands
Let's replace the left and right key binding with paddle-specific logic. We'll add a config variable controlling how fast our paddle can move:
Config.h
// ...
namespace Config::Breakout {
inline const float BALL_SPEED{10};
inline const float PADDLE_SPEED{5};
}
// ...
Then, we'll bind our left and right keys to functions that create left and right movement commands. Note that the BindKeyHeld()
calls in our constructor happens after the InputComponent
's Initialize()
method, so these calls are replacing those existing bindings.
We may just want to delete the Initialize()
override on our InputComponent
to keep things clearer if preferred. But for now, let's just overwrite them from our Paddle
constructor:
Breakout/Paddle.h
// ...
#include "Config.h"
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
// ...
Input = AddComponent<InputComponent>();
Input->BindKeyHeld(
SDLK_LEFT,
CreateMoveLeftCommand
);
Input->BindKeyHeld(
SDLK_RIGHT,
CreateMoveRightCommand
);
// ...
}
// ...
};
When we created our input component in the previous chapter, we defined these CreateMoveLeftCommand()
and CreateMoveRightCommand()
functions in an anonymous namespace within the input component source file.
In this case, we'll replicate them as static Paddle
methods, and we'll use our configuration variable as the horizontal component of the velocity:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
// ...
private:
static CommandPtr CreateMoveLeftCommand() {
return std::make_unique<MovementCommand>(
Vec2{-Config::Breakout::PADDLE_SPEED, 0.0});
}
static CommandPtr CreateMoveRightCommand() {
return std::make_unique<MovementCommand>(
Vec2{Config::Breakout::PADDLE_SPEED, 0.0});
}
// ...
};
Unbinding Keys
We can now control the left and right movement of our paddle, but the default InputComponent
is also causing our paddle to jump when the space key is pressed. We don't need this.
It'd be reasonable to simply delete that binding from the InputComponent::Initialize()
function but, for the sake of learning, let's give our InputComponent
the ability to unbind keys dynamically.
We'll add a public UnbindKey()
method that accepts the key code we want to unbind. The function will then remove that binding from both of the binding maps, using the erase()
method on their std::unordered_map
class:
Engine/ECS/InputComponent.h
// ...
class InputComponent : public Component {
public:
// ...
void UnbindKey(SDL_Keycode Key) {
KeyDownBindings.erase(Key);
KeyHeldBindings.erase(Key);
}
// ...
};
In our Paddle
constructor, we can now unbind the spacebar, preventing any jumping:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
// ...
Input = AddComponent<InputComponent>();
Input->UnbindKey(SDLK_SPACE);
// ...
}
// ...
};
Constraining Movement
The final problem we have with our paddle's input is that it can move completely off the screen. Let's constrain its horizontal motion.
Constraining motion is a generally useful capability, so we'll implement some of it in the physics component so it can be reused in the future.
We'll add a ConstrainHorizontalMovement()
function that accepts the left and right edge, as well as three member variables to support this behavior:
Engine/ECS/PhysicsComponent.h
// ...
class PhysicsComponent : public Component {
public:
// ...
void ConstrainHorizontalMovement(
float Left, float Right
) {
ShouldConstrainHorizontalMovement = true;
ConstrainLeft = Left;
ConstrainRight = Right;
}
private:
// ...
bool ShouldConstrainHorizontalMovement{false};
float ConstrainLeft;
float ConstrainRight;
};
In our Tick()
function, if the constraint is enabled and our velocity causes the entity to mvoe beyond those bounds, we'll snap it back:
Engine/ECS/Source/PhysicsComponent.cpp
// ...
void PhysicsComponent::Tick(float DeltaTime) {
ApplyForce(GetGravity() * Mass);
Velocity += Acceleration * DeltaTime;
SetOwnerPosition(
GetOwnerPosition() + Velocity * DeltaTime
);
Acceleration = {0.0, 0.0};
if (ShouldConstrainHorizontalMovement) {
auto [x, y]{GetOwnerPosition()};
if (x < ConstrainLeft) {
SetOwnerPosition({ConstrainLeft, y});
} else if (x > ConstrainRight) {
SetOwnerPosition({ConstrainRight, y});
}
}
}
// ...
Back in our Entity
constructor, we'll set the constraints such that our collider stays within the bounds of our scene.
To help with this, we'll do a minor refactor to add CollisionWidth
and CollisionOffsetX
values, so they can be used as arguments for both our existing function calls and our new ConstrainHorizontalMovement()
call:
Breakout/Paddle.h
// ...
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
// ...
float CollisionWidth{2.7};
float CollisionOffsetX{0.95};
Physics = AddComponent<PhysicsComponent>();
Physics->SetGravity({0, 0});
Physics->ConstrainHorizontalMovement(
-CollisionOffsetX,
Scene.GetWidth() - (
CollisionOffsetX + CollisionWidth
)
);
Collision = AddComponent<CollisionComponent>();
Collision->SetSize(2.7, 0.9);
Collision->SetSize(CollisionWidth, 0.9);
Collision->SetOffset({0.95, 0});
Collision->SetOffset({CollisionOffsetX, 0});
// ...
}
// ...
};
With those changes in place, we should now be able to move our paddle within our window, and the ball will bounce off it:

In the next part, we'll update our paddle to let it modify the ball's velocity depending on where the ball hit.
Complete Code
Our updated code is provided below:
Files
Summary
We've now added player control to our Breakout game by creating the paddle. This involved creating the Paddle entity, configuring its components, and writing the logic for its input and movement.
We customized the default keybindings, created a "release-to-stop" movement feel, and ensured the paddle can't leave the play area.
Here's an overview:
- We created a
Paddle
entity and added it to our scene. - We learned to override default input bindings and unbind keys we don't need.
- We implemented a velocity reset in the
Paddle::Tick()
function for responsive controls. - We added a reusable movement constraint feature to our
PhysicsComponent
. - The player can now move the paddle to hit the ball.
Breakout: Improving Paddle Physics
We'll add detailed collision logic to the paddle, giving the player control over the ball's trajectory.