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:
CollisionComponent
with offset, width, and height.CollisionComponent
with the Entity
and TransformComponent
.By the end, your entities will be able to detect when they overlap, setting the stage for collision response in the next lesson.
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:
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:
#pragma once
#include <SDL.h>
class Entity;
class Scene;
class AssetManager;
class Vec2;
class Component {
public:
Component(Entity* Owner) : Owner(Owner) {}
virtual void Initialize() {}
virtual void HandleEvent(const SDL_Event& E) {}
virtual void Tick(float DeltaTime) {}
virtual void Render(SDL_Surface* Surface) {}
virtual void DrawDebugHelpers(
SDL_Surface* Surface) {}
virtual ~Component() = default;
Entity* GetOwner() const { return Owner; }
Scene& GetScene() const;
AssetManager& GetAssetManager() const;
Vec2 ToScreenSpace(const Vec2& Pos) const;
Vec2 GetOwnerPosition() const;
void SetOwnerPosition(const Vec2& Pos) const;
Vec2 GetOwnerScreenSpacePosition() const;
float GetOwnerScale() const;
private:
Entity* Owner{nullptr};
};
#pragma once
#include <SDL.h>
#include "Utilities.h"
#include "Vec2.h"
#include "Component.h"
class TransformComponent : public Component {
public:
using Component::Component;
Vec2 GetPosition() const { return Position; }
void SetPosition(const Vec2& NewPosition) {
Position = NewPosition;
}
float GetScale() const { return Scale; }
void SetScale(float NewScale) {
Scale = NewScale;
}
void DrawDebugHelpers(SDL_Surface* S) override {
auto [x, y]{ToScreenSpace(Position)};
SDL_Rect Square{Utilities::Round({
x - 10, y - 10, 20, 20
})};
SDL_FillRect(S, &Square, SDL_MapRGB(
S->format, 255, 0, 0));
}
private:
Vec2 Position{0, 0};
float Scale{1.0f};
};
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.
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
: A Vec2
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
: An SDL_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
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:
GetOwnerPosition()
- and scale - GetOwnerScale()
.OwnerPosition + Offset
.Width * OwnerScale
, Height * OwnerScale
.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;
}
// ...
Notice that we update WorldBounds
in the CollisionComponent::Tick()
. This assumes that the TransformComponent
and PhysicsComponent
have already completed their Tick()
for the current frame, so GetOwnerPosition()
returns the final position for this frame.
Our current Entity::Tick()
iterates through all components. If the CollisionComponent
happens to be earlier in the Components
vector than the PhysicsComponent
, its Tick()
will run before the position is updated, leading to the collision bounds lagging one frame behind the entity's actual position.
A more robust system might involve different "phases" of ticking - e.g., PrePhysicsTick()
, PhysicsTick()
, PostPhysicsTick()
, and CollisionUpdateTick()
- to guarantee the correct order. For now, we'll rely on the typical component addition order or accept the potential one-frame lag, which is often negligible. A simple fix is to ensure PhysicsComponent
is added before CollisionComponent
.
Alternatively, the CollisionComponent::Tick()
could be empty, and a separate collision-checking phase in Scene::Tick()
could first update all collision bounds based on final transforms, and then check for overlaps. This decouples the update from the component iteration order. Let's stick with the component Tick()
for now for simplicity.
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);
}
}
// ...
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;
}
// ...
};
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
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:
y
position reduced by their height (h
)SDL_IntersectFRect()
to calculate the intersection of these rectangles within SDL’s coordinate system.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);
}
// ...
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:
SDL_FRect
in Y-up) for both components.SDL_IntersectFRect()
with the Y-down rectangles. It returns true
if they intersect and puts the intersection rectangle (also in Y-down) into our output parameter.SDL_IntersectFRect()
returns true, convert the resulting intersection rectangle back from Y-down to Y-up before returning.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;
}
// ...
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";
}
}
}
}
The nested loop structure checks every unique pair of entities exactly once. This is known as an algorithm, where represents the number of entities in the scene.
For example, if we have 10 entities in our scene, this means checks are required but, if we double the entity count to , the number of operations increases by a factor of four to .
In general, the number of checks we need to perform grows faster than the number of entities in the scene, which naturally becomes more problematic the more entities we have:
We cover algorithmic scaling in more detail here:
For a small number of entities, this algorithm is perfectly fine. However, in games with hundreds or thousands of objects, this becomes infeasible very quickly.
Real-world game engines use optimization techniques like spatial partitioning (e.g., Quadtrees in 2D, Octrees in 3D) to drastically reduce the number of pairs that need to be checked. These techniques group nearby objects together, so you only need to check for collisions between objects within the same group or neighboring groups.
We won't implement these complex optimizations in this course, but it's important to be aware of the performance limitations of the simple approach, and that alternatives are available if we ever need them.
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
);
}
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!
Here are the complete CollisionComponent.h/.cpp
files and the updated Scene.h/.cpp
and Entity.h
files incorporating the changes from this lesson.
#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;
// Check collision with another component
bool IsCollidingWith(
const CollisionComponent& Other
) const;
bool GetCollisionRectangle( // <h>
const CollisionComponent& Other, // <h>
SDL_FRect* OutIntersection // <h>
) const; // <h>
// Called each frame to update WorldBounds
void Tick(float DeltaTime) override;
// Dependency check
void Initialize() override;
// Draw the bounding box
void DrawDebugHelpers(SDL_Surface*) override;
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};
};
#include <iostream>
#include "CollisionComponent.h"
#include "Entity.h"
#include "Utilities.h"
#include "Scene.h"
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;
}
void CollisionComponent::Initialize() {
if (!GetOwner()->GetTransformComponent()) {
std::cerr << "Error: CollisionComponent "
"requires TransformComponent on its Owner.\n";
GetOwner()->RemoveComponent(this);
}
}
void CollisionComponent::SetOffset(
const Vec2& NewOffset
) {
Offset = NewOffset;
}
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;
}
}
const SDL_FRect&
CollisionComponent::GetWorldBounds() const {
return WorldBounds;
}
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);
}
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
);
}
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;
}
We also updated our Entity
class to allow CollisionComponent
s to be added and retrieved:
#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "CollisionComponent.h"
#include "Component.h"
#include "TransformComponent.h"
#include "InputComponent.h"
#include "Commands.h"
#include "ImageComponent.h"
#include "PhysicsComponent.h"
class Scene;
using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;
class Entity {
public:
Entity(Scene& Scene) : OwningScene{Scene} {}
Scene& GetScene() const { return OwningScene; }
virtual void HandleEvent(const SDL_Event& E) {
for (ComponentPtr& C : Components) {
C->HandleEvent(E);
}
}
virtual void Tick(float DeltaTime) {
for (ComponentPtr& C : Components) {
C->Tick(DeltaTime);
}
}
virtual void Render(SDL_Surface* Surface) {
for (ComponentPtr& C : Components) {
C->Render(Surface);
}
for (ComponentPtr& C : Components) {
C->DrawDebugHelpers(Surface);
}
}
virtual ~Entity() = default;
virtual void HandleCommand(
std::unique_ptr<Command> Cmd
) {
Cmd->Execute(this);
}
TransformComponent* AddTransformComponent() {
if (GetTransformComponent()) {
std::cout << "Error: Cannot have "
"multiple transform components";
return nullptr;
}
std::unique_ptr<Component>& NewComponent{
Components.emplace_back(
std::make_unique<
TransformComponent>(this))};
NewComponent->Initialize();
return static_cast<TransformComponent*>(
NewComponent.get());
}
TransformComponent* GetTransformComponent() const {
for (const ComponentPtr& C : Components) {
if (auto Ptr{
dynamic_cast<TransformComponent*>(C.get())
}) {
return Ptr;
}
}
return nullptr;
}
ImageComponent* AddImageComponent(
const std::string& FilePath
) {
std::unique_ptr<Component>& NewComponent{
Components.emplace_back(
std::make_unique<ImageComponent>(
this, FilePath))};
NewComponent->Initialize();
return static_cast<ImageComponent*>(
NewComponent.get());
}
using ImageComponents =
std::vector<ImageComponent*>;
ImageComponents GetImageComponents() const {
ImageComponents Result;
for (const ComponentPtr& C : Components) {
if (auto Ptr{dynamic_cast<
ImageComponent*>(C.get())}
) {
Result.push_back(Ptr);
}
}
return Result;
}
InputComponent* AddInputComponent() {
if (GetInputComponent()) {
std::cout << "Error: Cannot have "
"multiple input components";
return nullptr;
}
std::unique_ptr<Component>& NewComponent{
Components.emplace_back(
std::make_unique<
InputComponent>(this))};
NewComponent->Initialize();
return static_cast<InputComponent*>(
NewComponent.get());
}
InputComponent* GetInputComponent() const {
for (const ComponentPtr& C : Components) {
if (auto Ptr{
dynamic_cast<InputComponent*>(C.get())
}) {
return Ptr;
}
}
return nullptr;
}
PhysicsComponent* AddPhysicsComponent() {
if (GetPhysicsComponent()) {
std::cerr << "Error: Cannot add multiple "
"PhysicsComponents to an Entity.\n";
return nullptr;
}
ComponentPtr& NewComponent{
Components.emplace_back(
std::make_unique<PhysicsComponent>(this))
};
NewComponent->Initialize();
return static_cast<PhysicsComponent*>(
NewComponent.get());
}
PhysicsComponent* GetPhysicsComponent() const {
for (const ComponentPtr& C : Components) {
if (auto Ptr{
dynamic_cast<PhysicsComponent*>(C.get())
}) {
return Ptr;
}
}
return nullptr;
}
CollisionComponent* AddCollisionComponent() {
ComponentPtr& NewComponent{
Components.emplace_back(
std::make_unique<CollisionComponent>(this))
};
NewComponent->Initialize();
return static_cast<CollisionComponent*>(
NewComponent.get());
}
CollisionComponent* GetCollisionComponent() const {
for (const ComponentPtr& C : Components) {
if (auto Ptr{dynamic_cast<
CollisionComponent*>(C.get())}) {
return Ptr;
}
}
return nullptr;
}
void RemoveComponent(Component* PtrToRemove) {
for (size_t i{0}; i < Components.size(); ++i) {
if (Components[i].get() == PtrToRemove) {
Components.erase(Components.begin() + i);
return;
}
}
std::cout << "Warning: Attempted to remove "
"a component not found on this entity.\n";
}
protected:
ComponentPtrs Components;
Scene& OwningScene;
};
Finally, we added a CheckCollisions()
and ToScreenSpace(SDL_FRect)
functions to our Scene
class. We also updated our Tick()
function to call CheckCollisions()
:
#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:
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);
}
void HandleEvent(SDL_Event& E) {
for (EntityPtr& Entity : Entities) {
Entity->HandleEvent(E);
}
}
void Tick(float DeltaTime) {
for (EntityPtr& Entity : Entities) {
Entity->Tick(DeltaTime);
}
CheckCollisions();
}
void Render(SDL_Surface* Surface) {
SDL_GetClipRect(Surface, &Viewport);
for (EntityPtr& Entity : Entities) {
Entity->Render(Surface);
}
#ifdef DRAW_DEBUG_HELPERS
SDL_BlitSurface(
Trajectories, nullptr, Surface, nullptr);
#endif
}
Vec2 ToScreenSpace(const Vec2& Pos) const {
auto [vx, vy, vw, vh]{Viewport};
float HorizontalScaling{
vw / WorldSpaceWidth};
float VerticalScaling{
vh / WorldSpaceHeight};
return {
vx + Pos.x * HorizontalScaling,
vy + (WorldSpaceHeight - Pos.y)
* VerticalScaling
};
}
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
};
}
AssetManager& GetAssetManager() {
return Assets;
}
#ifdef DRAW_DEBUG_HELPERS
SDL_Surface* Trajectories{
SDL_CreateRGBSurfaceWithFormat(
0, 700, 300, 32, SDL_PIXELFORMAT_RGBA32
)
};
#endif
private:
void CheckCollisions();
EntityPtrs Entities;
AssetManager Assets;
SDL_Rect Viewport;
float WorldSpaceWidth{14}; // meters
float WorldSpaceHeight{6}; // meters
};
#include <vector>
#include "Scene.h"
#include "CollisionComponent.h"
void Scene::CheckCollisions() {
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";
}
}
}
}
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 the TransformComponent
origin.WorldBounds
(SDL_FRect
) is calculated each frame in Tick()
, incorporating owner position and scale.TransformComponent
, checked during Initialize()
.IsCollidingWith
) compares WorldBounds
of two components, requiring coordinate system adjustments for SDL functions.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.
Enable entities to detect collisions using bounding boxes managed by a dedicated component.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games