Creating a Collision Component

Enable entities to detect collisions using bounding boxes managed by a dedicated component.

Ryan McCombe
Updated

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:

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

// ...

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:

  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.

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

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 CollisionComponents 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 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.

Next Lesson
Lesson 121 of 129

Collision Response

Make entities react realistically to collisions, stopping, bouncing, and interacting based on type.

Have a question about this lesson?
Purchase the course to ask your own questions