Creating a Collision Component
Enable entities to detect collisions using bounding boxes managed by a dedicated component.
Our entities can now move realistically thanks to the PhysicsComponent
, but they pass right through each other! This lesson introduces the CollisionComponent
, responsible for defining an entity's physical shape for interaction. You'll learn how to:
- Create a
CollisionComponent
with offset, width, and height. - Calculate the component's world-space bounding box each frame.
- Implement basic collision detection between entities.
- Integrate the
CollisionComponent
with theEntity
andTransformComponent
. - Visualize collision bounds for debugging.
By the end, your entities will be able to detect when they overlap, setting the stage for collision response in the next lesson.
Starting Point
This lesson builds on concepts we covered in the previous chapter - reviewing those lessons is recommended if you're not already familiar with bounding boxes and calculating intersections in SDL:
Bounding Boxes
Discover bounding boxes: what they are, why we use them, and how to create them
Intersections and Relevancy Tests
Optimize games by checking object intersections with functions like SDL_HasIntersection()
.
So far in our entity-component system, entities have TransformComponent
for position/scale, PhysicsComponent
for movement, ImageComponent
for rendering, and InputComponent
for control.
However, they lack any sense of physical presence beyond their single point position managed by their TransformComponent
. We'll address that by continue working from where we left off in the previous lesson and adding a CollisionComponent
.
Our CollisionComponent
will inherit from Component
, and work closely with the entity's TransformComponent
. For reference, our Component.h
and TransformComponent.h
are provided below:
Bounding Box Review
In an earlier lesson, we introduced bounding boxes as simple shapes (usually rectangles or cuboids) used to approximate the physical space an object occupies.
We used Axis-Aligned Bounding Boxes (AABBs) - rectangles whose sides are parallel to the coordinate axes. These are simpler to work with than rotated boxes.

We represented them using SDL_Rect
(for integer coordinates, like screen space) and SDL_FRect
(for floating-point coordinates, like our world space).
We also created a BoundingBox
class back then. Now, we'll create a CollisionComponent
that encapsulates similar ideas but integrates with our entity-component system.
Creating the CollisionComponent
Let's define CollisionComponent.h
. It will inherit from Component
and manage the shape of the entity for collision purposes.
Crucially, this component will not store the entity's world position directly. That's the job of the TransformComponent
. Instead, the CollisionComponent
defines its shape relative to the entity's origin (the TransformComponent
's position).
Let's start by giving it some member variables, alongside getters and setters. We'll give it:
Offset
: AVec2
representing the top-left corner's offset from the entity's origin.Width
,Height
: Floating-point values defining the size of the collision box in world units (e.g., meters).WorldBounds
: AnSDL_FRect
that will store the calculated world-space bounding box each frame. This is the rectangle we'll use for intersection tests.
#pragma once
#include "Component.h"
#include "Vec2.h"
#include <SDL.h>
class CollisionComponent : public Component {
public:
// Inherit constructor
using Component::Component;
// Setters for defining the collision shape
void SetOffset(const Vec2& NewOffset);
void SetSize(float NewWidth, float NewHeight);
// Getter for the calculated world-space bounds
const SDL_FRect& GetWorldBounds() const;
private:
// Shape definition relative to owner's origin
Vec2 Offset{0.0, 0.0};
float Width{1.0}; // Default 1m x 1m
float Height{1.0};
// Calculated bounds updated each tick
SDL_FRect WorldBounds{0.0, 0.0, 0.0, 0.0};
};
Now, let's implement the basic setters and the getter in CollisionComponent.cpp
:
// CollisionComponent.cpp
#include <iostream>
#include "CollisionComponent.h"
void CollisionComponent::SetOffset(
const Vec2& NewOffset
) {
Offset = NewOffset;
// WorldBounds will be recalculated in Tick()
}
void CollisionComponent::SetSize(
float NewWidth, float NewHeight
) {
if (NewWidth < 0 || NewHeight < 0) {
std::cerr << "Warning: CollisionComponent "
"width/height cannot be negative. "
"Using absolute values.\n";
Width = std::abs(NewWidth);
Height = std::abs(NewHeight);
} else {
Width = NewWidth;
Height = NewHeight;
}
// WorldBounds will be recalculated in Tick()
}
const SDL_FRect&
CollisionComponent::GetWorldBounds() const
{
return WorldBounds;
}
// Initialize(), Tick(), IsCollidingWith(), and
// DrawDebugHelpers() will be implemented next
Updating World Bounds - Tick()
The CollisionComponent
needs to recalculate its WorldBounds
rectangle every frame, as the owning entity might have moved. We'll do this in the Tick()
method.
// CollisionComponent.h
// ...
class CollisionComponent : public Component {
public:
// ...
// Called each frame to update WorldBounds
void Tick(float DeltaTime) override;
// ...
};
Remember our world is Y-up, but SDL_FRect
expects Y-down for its coordinate if we are using SDL intersection functions like SDL_HasIntersectionF()
.
However, to keep things simple, we'll worry about that when we come to calculate the intersections. GetWorldBounds()
just calculates the updated position, whilst our intersection logic will handle the coordinate system difference.
So, Tick()
needs to:
- Get the owner entity's current world position -
GetOwnerPosition()
- and scale -GetOwnerScale()
. - Calculate the top-left corner of the bounds:
OwnerPosition + Offset
. - Calculate the scaled width and height:
Width * OwnerScale
,Height * OwnerScale
. - Store these values in the
WorldBounds
member.
// CollisionComponent.cpp
// ...
void CollisionComponent::Tick(float DeltaTime) {
Vec2 OwnerPos{GetOwnerPosition()};
float OwnerScale{GetOwnerScale()};
// Calculate world-space position and dimensions
WorldBounds.x = OwnerPos.x + Offset.x;
WorldBounds.y = OwnerPos.y + Offset.y;
WorldBounds.w = Width * OwnerScale;
WorldBounds.h = Height * OwnerScale;
}
// ...
Dependency Check - Initialize()
The CollisionComponent
needs a TransformComponent
to know the entity's position and scale. Let's enforce this in Initialize()
:
// CollisionComponent.h
// ...
class CollisionComponent : public Component {
public:
// ...
// Dependency check
void Initialize() override;
// ...
};
// CollisionComponent.cpp
// ...
void CollisionComponent::Initialize() {
if (!GetOwner()->GetTransformComponent()) {
std::cerr << "Error: CollisionComponent "
"requires TransformComponent on its Owner.\n";
GetOwner()->RemoveComponent(this);
}
}
// ...
Integrating with Entity
We'll add the standard AddCollisionComponent()
and GetCollisionComponent()
methods to Entity.h
.
For simplicity in this lesson, we'll assume only one CollisionComponent
per entity. If multiple shapes are needed, a more advanced design might involve a GetCollisionComponents()
function that returns a std::vector
, similar to GetImageComponents()
.
Let's update our Entity
class:
// Entity.h
// ...
#include "CollisionComponent.h"
// ...
class Entity {
public:
// ...
CollisionComponent* AddCollisionComponent() {
// Allow multiple for now, but
// GetCollisionComponent below only gets the
// first one.
ComponentPtr& NewComponent{
Components.emplace_back(
std::make_unique<CollisionComponent>(this))
};
NewComponent->Initialize();
return static_cast<CollisionComponent*>(
NewComponent.get());
}
CollisionComponent* GetCollisionComponent() const {
// Returns the *first* collision component found
for (const ComponentPtr& C : Components) {
if (auto Ptr{dynamic_cast<
CollisionComponent*>(C.get())}) {
return Ptr;
}
}
return nullptr;
}
// ...
};
Collision Detection Logic
Now, let's implement IsCollidingWith()
in CollisionComponent.cpp
. This function will determine if this component's WorldBounds
overlaps with another CollisionComponent
's WorldBounds
.
We previously implemented world-space intersection logic in our earlier lesson
Intersections and Relevancy Tests
Optimize games by checking object intersections with functions like SDL_HasIntersection()
.
The core idea was to temporarily convert the Y-up world rectangles to SDL's expected Y-down format before using SDL's intersection functions. The process is as follows:
- Start with rectangles defined using the y-up convention.
- Create copies of these rectangles that have their
y
position reduced by their height (h
) - Use an SDL function like
SDL_IntersectFRect()
to calculate the intersection of these rectangles within SDL's coordinate system. - If we need the intersection rectangle, increase its
y
value by its height to convert it to the y-up representation.

Let's replicate the logic we walked through in that lesson in our IsCollidingWith()
function:
// CollisionComponent.cpp
// ...
class CollisionComponent : public Component {
public:
// ...
// Check collision with another component
bool IsCollidingWith(
const CollisionComponent& Other
) const;
// ...
};
// CollisionComponent.cpp
// ...
bool CollisionComponent::IsCollidingWith(
const CollisionComponent& Other
) const {
// Get the world bounds rectangles
const SDL_FRect& A_world{GetWorldBounds()};
const SDL_FRect& B_world{
Other.GetWorldBounds()};
// Convert to SDL's coordinate system (Y-down)
// by subtracting height from Y
SDL_FRect A_sdl{A_world};
A_sdl.y -= A_world.h; // Convert A to Y-down
SDL_FRect B_sdl{B_world};
B_sdl.y -= B_world.h; // Convert B to Y-down
// Use SDL's built-in intersection check
return SDL_HasIntersectionF(&A_sdl, &B_sdl);
}
// ...
Getting Collision Rectangle
Sometimes, just knowing if a collision occurred isn't enough. For collision response, we often need to know the exact region where the two objects overlap. We can create a variation of IsCollidingWith()
that calculates this intersection rectangle.
This function will take a pointer to an SDL_FRect
as an output parameter (OutIntersection
).
// CollisionComponent.h
// ...
class CollisionComponent : public Component {
public:
// ...
// Check collision and get intersection rectangle
bool GetCollisionRectangle( // <h>
const CollisionComponent& Other, // <h>
SDL_FRect* OutIntersection // <h>
) const; // <h>
// ...
};
If a collision occurs, the function will populate this rectangle with the overlapping area (in world-space, Y-up coordinates) and return true
. If there's no collision, it will return false, and the contents of OutIntersection
will be undefined.
We'll use SDL_IntersectFRect()
for this. Similar to SDL_HasIntersectionF()
, it operates in SDL's Y-down coordinate system. So, we need to follow the exact same process as before to convert Y-down rectangles to Y-up.
The process looks like this:
- Get the world bounds (
SDL_FRect
in Y-up) for both components. - Convert both rectangles to SDL's Y-down system.
- Call
SDL_IntersectFRect()
with the Y-down rectangles. It returnstrue
if they intersect and puts the intersection rectangle (also in Y-down) into our output parameter. - If
SDL_IntersectFRect()
returns true, convert the resulting intersection rectangle back from Y-down to Y-up before returning. - Return the result of
SDL_IntersectFRect()
.
// CollisionComponent.cpp
// ...
bool CollisionComponent::GetCollisionRectangle(
const CollisionComponent& Other,
SDL_FRect* OutIntersection
) const {
// Ensure the output pointer is valid
if (!OutIntersection) {
std::cerr << "Error: OutIntersection pointer "
"is null in GetCollisionRectangle.\n";
return false;
}
// Get world bounds (Y-up)
const SDL_FRect& A_world{GetWorldBounds()};
const SDL_FRect& B_world{
Other.GetWorldBounds()};
// Convert to SDL's Y-down system
SDL_FRect A_sdl{A_world};
A_sdl.y -= A_world.h; // Convert A to Y-down
SDL_FRect B_sdl{B_world};
B_sdl.y -= B_world.h; // Convert B to Y-down
// Calculate intersection in Y-down system
SDL_FRect Intersection_sdl;
if (SDL_IntersectFRect(
&A_sdl, &B_sdl, &Intersection_sdl))
{
// Collision occurred! Convert intersection
// back to Y-up world space
*OutIntersection = Intersection_sdl;
OutIntersection->y += Intersection_sdl.h;
return true;
}
// No collision
return false;
}
// ...
Scene-Level Collision Checking
The IsCollidingWith()
method checks if two specific components collide. To check all potential collisions in the scene, we need to iterate through pairs of entities in Scene::Tick()
.
This is often done after all entities have finished their individual Tick()
updates (including physics and collision bound updates).
// Scene.h
// ...
class Scene {
public:
// ...
void Tick(float DeltaTime) {
// 1. Tick all entities (updates physics,
// collision bounds, etc.)
for (EntityPtr& Entity : Entities) {
Entity->Tick(DeltaTime);
}
// 2. Check for collisions between entities
CheckCollisions();
}
// ...
private:
void CheckCollisions();
// ...
};
// Scene.cpp
#include <vector>
#include "Scene.h"
#include "CollisionComponent.h"
void Scene::CheckCollisions() {
// Basic N^2 check is inefficient for
// large scenes - see note below
for (size_t i{0}; i < Entities.size(); ++i) {
CollisionComponent* ColA{
Entities[i]->GetCollisionComponent()};
// Skip if no collision component
if (!ColA) continue;
for (size_t j{i + 1}; j < Entities.size(); ++j) {
CollisionComponent* ColB{
Entities[j]->GetCollisionComponent()};
// Skip if no collision component
if (!ColB) continue;
if (ColA->IsCollidingWith(*ColB)) {
std::cout << "Collision detected between "
"Entity " << i << " and Entity " << j
<< "!\n";
}
}
}
}
Debug Drawing
Let's add debug drawing to visualize the WorldBounds
.
To help with this, we first need a way to convert our WorldBounds
rectangle from world space to screen space. Let's overload the ToScreenSpace()
function to our Scene
class to accept an SDL_FRect
. Note this is the exact same function we walked through creating in our earlier lesson on bounding boxes:
// Scene.h
#pragma once
#include <SDL.h>
#include <vector>
#include "AssetManager.h"
#include "Entity.h"
#define DRAW_DEBUG_HELPERS
using EntityPtr = std::unique_ptr<Entity>;
using EntityPtrs = std::vector<EntityPtr>;
class Scene {
public:
Vec2 ToScreenSpace(const Vec2& Pos) {/*...*/}
SDL_FRect ToScreenSpace(
const SDL_FRect& Rect
) const {
Vec2 ScreenPos{ToScreenSpace(
Vec2{Rect.x, Rect.y})};
float HorizontalScaling{
Viewport.w / WorldSpaceWidth};
float VerticalScaling{
Viewport.h / WorldSpaceHeight};
return {
ScreenPos.x,
ScreenPos.y,
Rect.w * HorizontalScaling,
Rect.h * VerticalScaling
};
}
// ...
};
We'll use the DrawRectOutline()
helper from Utilities.h
which we created earlier in the chapter:
// CollisionComponent.h
// ...
class CollisionComponent : public Component {
public:
// ...
void DrawDebugHelpers(
SDL_Surface* Surface) override;
// ...
};
// CollisionComponent.cpp
// ...
// For DrawRectOutline(), Round()
#include "Utilities.h"
#include "Scene.h" // For ToScreenSpace()
// ...
void CollisionComponent::DrawDebugHelpers(
SDL_Surface* Surface
) {
// Convert world bounds to screen space for
// drawing onto the surface
SDL_FRect ScreenBoundsF{
GetScene().ToScreenSpace(WorldBounds)};
SDL_Rect ScreenBounds{
Utilities::Round(ScreenBoundsF)};
// Draw outline using the helper
Utilities::DrawRectOutline(
Surface,
ScreenBounds,
// Yellow
SDL_MapRGB(Surface->format, 255, 255, 0),
1 // Thin line
);
}
Example Usage
Let's set up two entities in Scene.h
with collision components and make one fall onto the other:
// Scene.h
// ...
class Scene {
public:
Scene() {
// --- Falling Entity ---
EntityPtr& Player{Entities.emplace_back(
std::make_unique<Entity>(*this))};
Player->AddTransformComponent()
->SetPosition({6, 5});
Player->AddPhysicsComponent()
->SetMass(50.0);
Player->AddImageComponent("player.png");
Player
->AddCollisionComponent()
// Match rough image size
->SetSize(1.9, 1.7);
// --- Static Entity ---
EntityPtr& Floor{Entities.emplace_back(
std::make_unique<Entity>(*this))};
Floor->AddTransformComponent()
->SetPosition({4.5, 1});
Floor->AddImageComponent("floor.png");
Floor
->AddCollisionComponent()
->SetSize(5.0, 2.0);
// Note the floor has no physics component
// so will not be affected by gravity
}
// ... (Rest of Scene class)
};
Now, when you run the game, the "Player" entity will fall due to gravity (from its PhysicsComponent
). As it falls, its CollisionComponent
's WorldBounds
will update. The Scene::CheckCollisions()
loop will compare the player's bounds with the floor's bounds.
Once they overlap, you'll see "Collision detected..." messages printed to the console, and the yellow debug rectangles will visually confirm the overlap.

Collision detected between Entity 0 and Entity 1!
Collision detected between Entity 0 and Entity 1!
Collision detected between Entity 0 and Entity 1!
// ...
The player still passes through the floor because we haven't implemented any collision response yet. That's the topic for the next lesson!
Complete Code
Here are the complete CollisionComponent.h/.cpp
files and the updated Scene.h/.cpp
and Entity.h
files incorporating the changes from this lesson.
We also updated our Entity
class to allow CollisionComponent
s to be added and retrieved:
Finally, we added a CheckCollisions()
and ToScreenSpace(SDL_FRect)
functions to our Scene
class. We also updated our Tick()
function to call CheckCollisions()
:
Summary
We've successfully created a CollisionComponent
that defines the physical shape of our entities for interaction. This component calculates its world-space bounding box (WorldBounds
) each frame based on its owner's TransformComponent
and its own offset and size properties.
We implemented a basic IsCollidingWith
method using SDL's intersection functions (handling the Y-up world vs. Y-down SDL coordinate difference) and integrated a scene-level check to detect overlaps between entities. Debug drawing helps visualize these collision bounds.
Key takeaways:
CollisionComponent
defines shape (Offset
,Width
,Height
) relative to theTransformComponent
origin.WorldBounds
(SDL_FRect
) is calculated each frame inTick()
, incorporating owner position and scale.- The component relies on
TransformComponent
, checked duringInitialize()
. - Collision detection (
IsCollidingWith
) comparesWorldBounds
of two components, requiring coordinate system adjustments for SDL functions. - Scene-level collision checks iterate through entity pairs, currently requiring checks.
- Debug drawing of
WorldBounds
is crucial for verifying collision shapes and detection.
Our entities can now detect collisions, but they don't yet react to them. Implementing collision response (stopping, bouncing, taking damage, etc.) will be the focus of the next lesson.
Collision Response
Make entities react realistically to collisions, stopping, bouncing, and interacting based on type.