Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games
Our ball is moving, but it flies off the screen forever. To create a playable game, we need to contain it within the play area. We'll achieve this by adding four invisible walls around the edges of our scene.
This lesson will guide you through creating a Wall
entity. We'll instantiate four of them - for the top, bottom, left, and right boundaries - and give them collision components.
The core of this lesson, however, is implementing the collision response in our Ball
class.
We'll start with a simple bouncing algorithm that works well in most cases. Then, we'll explore a more advanced, physically accurate method using vector math concepts like the dot product and surface normals.
By the end, our ball will be correctly bouncing around inside the game window.
To simulate our ball bouncing off the edges of the screen, we'll add four walls to our scene. They will sit just outside of the visible area - one on the top, one on the bottom, one on the left, and one on the right.
Let's start by adding a Wall
class to manage this. We'll also add a WallPosition
enum to represent the four states.
Eventually, colliding with the bottom wall will mean the player lost, so we'll store which position each wall is in as a member variable so we can implement this losing logic later:
#pragma once
#include "Engine/ECS/Entity.h"
enum class WallPosition {
Top, Bottom, Left, Right
};
class Wall : public Entity {
public:
Wall(WallPosition Position, BreakoutScene& Scene)
: Entity{Scene}, Position{Position} {}
private:
WallPosition Position;
};
Back in our BreakoutScene
's Load()
method, let's add our four walls:
#include "Breakout/BreakoutScene.h"
#include "Breakout/Ball.h"
#include "Breakout/Wall.h"
void BreakoutScene::Load(int Level) {
Entities.clear();
Entities.emplace_back(
std::make_unique<Ball>(*this));
using enum WallPosition;
Entities.emplace_back(
std::make_unique<Wall>(Top, *this));
Entities.emplace_back(
std::make_unique<Wall>(Left, *this));
Entities.emplace_back(
std::make_unique<Wall>(Bottom, *this));
Entities.emplace_back(
std::make_unique<Wall>(Right, *this));
}
To collide with things, our walls will need both a TransformComponent
and a CollisionComponent
. Let's add them in the constructor:
#pragma once
#include "Engine/ECS/Entity.h"
#include "Engine/ECS/TransformComponent.h"
#include "Engine/ECS/CollisionComponent.h"
enum class WallPosition {
Top, Bottom, Left, Right
};
class Wall : public Entity {
public:
Wall(WallPosition Position, BreakoutScene& Scene)
: Entity{Scene}, Position{Position} {
TransformComponent* Transform{
AddComponent<TransformComponent>()};
CollisionComponent* Collision{
AddComponent<CollisionComponent>()};
}
private:
WallPosition Position;
};
The position of each wall will depend on the requested WallPosition
, as well as the width and height of our Scene
, which is available through the GetWidth()
and GetHeight()
member variables on the base Scene
class.
Our walls will need some thickness to collide correctly. We'll choose 1 unit for this, meaning the sizing and positioning of our walls would look something like this:
Let's implement this in the code:
// ...
class Wall : public Entity {
public:
Wall(WallPosition Position, BreakoutScene& Scene)
: Entity{Scene}, Position{Position} {
// ...
float Height{Scene.GetHeight()};
float Width{Scene.GetWidth()};
using enum WallPosition;
if (Position == Top) {
Transform->SetPosition({0, Height + 1});
Collision->SetSize(Width, 1);
} else if (Position == Bottom) {
Transform->SetPosition({0, 0});
Collision->SetSize(Width, 1);
} else if (Position == Left) {
Transform->SetPosition({-1, Height});
Collision->SetSize(1, Height);
} else if (Position == Right) {
Transform->SetPosition({Width, Height});
Collision->SetSize(1, Height);
}
}
// ...
};
Given our walls have no visual appearance, and are outside the viewport anyway, there will be no visual changes to our program yet. However, let's update our Ball
to let it bounce off these walls.
Given our Ball
and Wall
entities have collision components, our engine is already detecting these collisions over in the HandleCollisions
function of our base Scene
class.
That function is calling a virtual HandleCollision()
function on our entities. Let's override it in our Ball
class to implement the bouncing behavior:
// ...
class Ball : public Entity {
public:
// ...
void HandleCollision(Entity& Other) override;
// ...
};
We'll add a Ball.cpp
to implement our bouncing reaction. This code is the same logic we walked through in the bouncing ball example in the previous chapter:
#include "Breakout/Ball.h"
#include "Engine/Vec2.h"
void Ball::HandleCollision(Entity& Other) {
SDL_FRect Intersection;
Collision->GetCollisionRectangle(
*Other.GetComponent<CollisionComponent>(),
&Intersection
);
if (!(Physics && Transform)) return;
bool IsVertical{
Intersection.w > Intersection.h
};
Vec2 CurrentPos{Transform->GetPosition()};
if (IsVertical) {
if (Physics->GetVelocity().y < 0)
CurrentPos.y += Intersection.h;
else
CurrentPos.y -= Intersection.h;
} else {
if (Physics->GetVelocity().x > 0)
CurrentPos.x -= Intersection.w;
else
CurrentPos.x += Intersection.w;
}
Transform->SetPosition(CurrentPos);
Vec2 CurrentVel{Physics->GetVelocity()};
if (IsVertical) {
Physics->SetVelocity({
CurrentVel.x, -CurrentVel.y
});
} else {
Physics->SetVelocity({
-CurrentVel.x, CurrentVel.y
});
}
}
With these changes, our ball should now be bouncing off the edges of our window:
Our previous implementation of bouncing updates the ball's velocity by inverting a component based on the collision overlap. This simple implementation usually works, but it occasionally generates unnatural reactions when our objects intersect at their corners.
We can replace this approach by considering the relative position of our colliding objects, combining two techniques we briefly introduced in our physics chapter.
To calculate a realistic bounce, we need to know the orientation of the surface we collided with. This is represented by the surface normal, often just called the normal. The normal is a vector that points directly away from a surface, perpendicular to it.
For any given collision, if we know the surface normal and the incoming velocity of our ball, we can calculate the exact reflection vector. This is a much more robust approach than simply inverting the x
or y
velocity, as it handles collisions at any angle correctly, including corner hits.
The normal angle is generally easy to determine when our colliders are axis-aligned rectangles, which they all are in this case. The normal angle of such a shape is always directly up, down, left, or to the right:
Another mathematical tool we can use is the dot product. The dot product takes two vectors and returns a single number (a scalar) that tells us about the angle between them.
Specifically, the sign of the dot product tells us if the vectors are pointing in generally the same direction (positive), opposite directions (negative), or are perpendicular (zero).
In our case, we can take the dot product of the ball's velocity vector and the surface normal. If the result is negative, it confirms the ball is moving towards the thing our scene reported it collided with, and a bounce should occur. We can also use the magnitude of the dot product in the formula to calculate the reflection vector.
Our new implementation uses these concepts to calculate a proper reflection vector. First, we determine the surface normal based on the relative positions of the ball and the other object.
Then, we calculate the dot product of the velocity and the normal. The standard formula for vector reflection is then used to find the new velocity:
This gives us a bounce that is physically accurate and behaves correctly even in tricky corner-case collisions.
#include "Breakout/Ball.h"
#include "Engine/Vec2.h"
void Ball::HandleCollision(Entity& Other) {
TransformComponent* OtherTransform{
Other.GetComponent<TransformComponent>()
};
Vec2 RelativePosition{
Transform->GetPosition() -
OtherTransform->GetPosition()
};
Vec2 Normal{
IsVertical ? Vec2{
0.0f,
(RelativePosition.y > 0) ? 1.0f : -1.0f
} : Vec2{
(RelativePosition.x > 0) ? 1.0f : -1.0f,
0.0f
}
};
Vec2 Velocity{Physics->GetVelocity()};
float DotProduct{
Velocity.x * Normal.x +
Velocity.y * Normal.y
};
if (DotProduct < 0) {
Physics->SetVelocity(
Velocity - 2 * DotProduct * Normal
);
}
}
Complete versions of the files we updated in this section are available below:
We've now made our game feel like an enclosed arena by adding walls and implementing bouncing physics.
We created a Wall
class, placed four instances at the screen's edges, and then coded the collision logic within the Ball
's HandleCollision()
function.
Here's a summary of what we've accomplished:
Wall
entity to define the play area boundaries.Ball
to react to collisions with other entities.Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development
View CourseWe'll add invisible walls around the play area and write the collision response code to make the ball bounce.
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