Creating a Collision Component

Enable entities to detect collisions using bounding boxes managed by a dedicated component.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing collision boxes
Ryan McCombe
Ryan McCombe
Posted

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 the Entity and TransformComponent.
  • 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:

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};
};

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.

Diagram showing examples of two bounding boxes - one axis-aligned, and one not

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: 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

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 yy 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:

  1. Get the owner entity's current world position - GetOwnerPosition() - and scale - GetOwnerScale().
  2. Calculate the top-left corner of the bounds: OwnerPosition + Offset.
  3. Calculate the scaled width and height: Width * OwnerScale, Height * OwnerScale.
  4. 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;
}

// ...

Tick Order Matters

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.

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

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:

  1. Start with rectangles defined using the y-up convention.
  2. Create copies of these rectangles that have their y position reduced by their height (h)
  3. Use an SDL function like SDL_IntersectFRect() to calculate the intersection of these rectangles within SDL’s coordinate system.
  4. If we need the intersection rectangle, increase its y value by its height to convert it to the y-up representation.
Diagram showing the process of converting between y-up and y-down representations

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:

  1. Get the world bounds (SDL_FRect in Y-up) for both components.
  2. Convert both rectangles to SDL's Y-down system.
  3. Call 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.
  4. If SDL_IntersectFRect() returns true, convert the resulting intersection rectangle back from Y-down to Y-up before returning.
  5. 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";
      }
    }
  }
}

Optimizing Collision Checks

The nested loop structure checks every unique pair of entities exactly once. This is known as an O(N2)O(N^2) algorithm, where NN represents the number of entities in the scene.

For example, if we have 10 entities in our scene, this means 102=10010^2 = 100 checks are required but, if we double the entity count to 2020, the number of operations increases by a factor of four to 202=40020^2 = 400.

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:

Showing the exponential relationship between entity count and collision checks

We cover algorithmic scaling in more detail here:

For a small number of entities, this O(N2)O(N^2) 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 N2N^2 approach, and that alternatives are available if we ever need them.

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.

Screenshot showing our collision debug helpers
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.

#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 CollisionComponents 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";
      }
    }
  }
}

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 the TransformComponent origin.
  • WorldBounds (SDL_FRect) is calculated each frame in Tick(), incorporating owner position and scale.
  • The component relies on TransformComponent, checked during Initialize().
  • Collision detection (IsCollidingWith) compares WorldBounds of two components, requiring coordinate system adjustments for SDL functions.
  • Scene-level collision checks iterate through entity pairs, currently requiring O(N2)O(N^2) 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.

Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Creating Components
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 108 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved