Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games
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:
Paddle
entity class.PhysicsComponent
to keep the paddle on-screen.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:
#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:
// ...
#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.
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:
// ...
#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};
};
Next, let's update our paddle to respond to inputs. We'll add an InputComponent
and PhysicsComponent
, saving their pointers for later:
// ...
#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.
Again, note that the order in which we add components to our entity is meaningful, as it affects their tick order. The commands generated by the Tick()
function of our InputComponent
update the velocity of our PhysicsComponent
which, in turn, gets used by the physics component's Tick()
function.
If we add our InputComponent
before the PhysicsComponent
, that means the InputComponent
will tick before the PhysicsComponent
, which is what we want. If we changed the order, our physics component wouldn't react to our inputs until the next iteration of our application loop, which makes our program feel less responsive.
Additionally, our "Resetting Velocity" implementation in the next section requires that our input component tick before our physics component, so it's particularly important in this case.
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.
// ...
class Paddle : public Entity {
public:
// ...
void Tick(float DeltaTime) override {
Entity::Tick(DeltaTime);
Physics->SetVelocity({0, 0});
}
// ...
};
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:
// ...
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:
// ...
#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:
// ...
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});
}
// ...
};
A more succinct way to provide these bindings is to use a lambda expression. Lambdas allow us to provide inline functions within the body of other functions, such as our Paddle
constructor.
If a function is short and only going to be used in a single place, providing it as a lambda often makes our code easier to follow than defining the logic elsewhere in the file.
The lambda approach would look like this:
// ...
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
// ...
Input = AddComponent<InputComponent>();
Input->BindKeyHeld(SDLK_LEFT, []{
return std::make_unique<MovementCommand>(
Vec2{-Config::Breakout::PADDLE_SPEED, 0.0}
);
});
Input->BindKeyHeld(SDLK_RIGHT, []{
return std::make_unique<MovementCommand>(
Vec2{Config::Breakout::PADDLE_SPEED, 0.0}
);
});
// ...
}
// ...
};
We cover lambdas in much more detail in the advanced course.
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:
// ...
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:
// ...
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
// ...
Input = AddComponent<InputComponent>();
Input->UnbindKey(SDLK_SPACE);
// ...
}
// ...
};
Components do not always need to be generic. It can be helpful to add game-specific components to remove complexity from our entity types, or to define behaviors that are useful across multiple entities.
These components can also inherit from base components if doing so would be useful. Below, we derive from InputComponent
to create a component that can be used to encapsulate our paddle input logic.
We override the base Initialize()
method to provide the keybindings we need for our paddle:
#pragma once
#include "Engine/ECS/InputComponent.h"
#include "Config.h"
class PaddleInputComponent : public InputComponent {
public:
// Inherit constructors
using InputComponent::InputComponent;
void Initialize() override {
BindKeyHeld(SDLK_LEFT, [] {
return std::make_unique<MovementCommand>(
Vec2{-Config::Breakout::PADDLE_SPEED, 0.0});
}
);
BindKeyHeld(SDLK_RIGHT, [] {
return std::make_unique<MovementCommand>(
Vec2{Config::Breakout::PADDLE_SPEED, 0.0});
}
);
}
};
This allows us to remove a lot of code from our Paddle
class, helping keep things organized:
// ...
#include "Engine/ECS/InputComponent.h"
#include "Breakout/PaddleInputComponent.h"
class Paddle : public Entity {
public:
Paddle(BreakoutScene& Scene) : Entity{Scene} {
// ...
Input = AddComponent<PaddleInputComponent>();
Input->UnbindKey(SDLK_SPACE);
Input->BindKeyHeld(SDLK_LEFT, []{
return std::make_unique<MovementCommand>(
Vec2{-Config::Breakout::PADDLE_SPEED, 0.0}
);
});
Input->BindKeyHeld(SDLK_RIGHT, []{
return std::make_unique<MovementCommand>(
Vec2{Config::Breakout::PADDLE_SPEED, 0.0}
);
});
// ...
}
private:
// ...
InputComponent* Input{nullptr};
PaddleInputComponent* Input{nullptr};
};
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:
// ...
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:
// ...
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:
// ...
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.
Our updated code is provided below:
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:
Paddle
entity and added it to our scene.Paddle::Tick()
function for responsive controls.PhysicsComponent
.Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development
View CourseWe'll build the player's paddle, hook it up to keyboard controls, and keep it from moving off-screen.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games
Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development
View Course