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

Files

src
Select a file to view its content

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) and SDL_FRect (for floating-point coordinates).

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 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.
  • Bounds: An SDL_FRect that will store the calculated bounding box each frame. This is the rectangle we'll use for intersection tests.

src/CollisionComponent.h

#pragma once
#include "Component.h"
#include "Vec2.h"
#include <SDL3/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 bounds
  const SDL_FRect& GetBounds() const;

 private:
  // Shape definition relative to owner's origin
  Vec2 Offset{0.0, 0.0};
  float Width{1.0};  // Default 1x1
  float Height{1.0};

  // Calculated bounds updated each tick
  SDL_FRect Bounds{0.0, 0.0, 0.0, 0.0};
};

Now, let's implement the basic setters and the getter in CollisionComponent.cpp:

src/CollisionComponent.cpp

#include <iostream>
#include <cmath>
#include "CollisionComponent.h"

void CollisionComponent::SetOffset(
  const Vec2& NewOffset
) {
  Offset = NewOffset;
  // Bounds 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;
  }
  // Bounds will be recalculated in Tick()
}

const SDL_FRect& CollisionComponent::GetBounds() const {
  return Bounds;
}

Updating Bounds - Tick()

The CollisionComponent needs to recalculate its Bounds rectangle every frame, as the owning entity might have moved. We'll do this in the Tick() method.

src/CollisionComponent.h

// ...

class CollisionComponent : public Component {
 public:
  // ...

  // Called each frame to update Bounds
  void Tick(float DeltaTime) override; // <h>
  // ...
};

Tick() needs to:

  1. Get the owner entity's current 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 Bounds member.

src/CollisionComponent.cpp

// ...

void CollisionComponent::Tick(float DeltaTime) {
  Vec2 OwnerPos{GetOwnerPosition()};
  float OwnerScale{GetOwnerScale()};

  // Calculate position and dimensions
  Bounds.x = OwnerPos.x + Offset.x;
  Bounds.y = OwnerPos.y + Offset.y;
  Bounds.w = Width * OwnerScale;
  Bounds.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():

Files

src
Select a file to view its content

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:

src/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 Bounds overlaps with another CollisionComponent's Bounds.

We rely on SDL3's SDL_HasRectIntersectionFloat() function to perform the overlap check.

Files

src
Select a file to view its content

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

src/CollisionComponent.h

// ...

class CollisionComponent : public Component {
 public:
  // ...

  // Check collision and get intersection rectangle
  bool GetCollisionRectangle( 
    const CollisionComponent& Other, 
    SDL_FRect* OutIntersection 
  ) const; 

  // ...
};

If a collision occurs, the function will populate this rectangle with the overlapping area and return true. If there's no collision, it will return false. We use SDL_GetRectIntersectionFloat() for this.

src/CollisionComponent.cpp

// ...

bool CollisionComponent::GetCollisionRectangle(
  const CollisionComponent& Other,
  SDL_FRect* OutIntersection
) const {
  if (!OutIntersection) {
    std::cerr << "Error: OutIntersection pointer "
      "is null in GetCollisionRectangle.\n";
    return false;
  }

  return SDL_GetRectIntersectionFloat(
    &Bounds, &Other.Bounds, OutIntersection
  );
}

// ...

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

Files

src
Select a file to view its content

Debug Drawing

Let's add debug drawing to visualize the Bounds.

We'll use the DrawRectOutline() helper from Utilities.h which we created earlier in the chapter.

Files

src
Select a file to view its content

Example Usage

Let's set up two entities in Scene.h with collision components and make one fall onto the other:

src/Scene.h

// ...

class Scene {
 public:
  Scene() {
    std::string BasePath{SDL_GetBasePath()};

    // --- Falling Entity ---
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>(*this)
    )};

    Player->AddTransformComponent()
      ->SetPosition({
        2.f * PIXELS_PER_METER,
        1.f * PIXELS_PER_METER,
      });
    Player->AddPhysicsComponent()
      ->SetMass(50.0);
    Player->AddImageComponent(BasePath + "player.png");
    Player
      ->AddCollisionComponent()
      // Match rough image size
      ->SetSize(
        1.9f * PIXELS_PER_METER,
        1.7f * PIXELS_PER_METER
      );

    // --- Static Entity ---
    EntityPtr& Floor{Entities.emplace_back(
      std::make_unique<Entity>(*this)
    )};

    Floor->AddTransformComponent()
      ->SetPosition({
        1.f * PIXELS_PER_METER,
        4.f * PIXELS_PER_METER,
      });
    // Add an image - optional - we can see where the
    // the object is based on the collision component
    // drawn by DrawDebugHelpers()
    Floor->AddImageComponent(BasePath + "floor.png");
    Floor
      ->AddCollisionComponent()
      ->SetSize(
        5.0f * PIXELS_PER_METER,
        2.0f * PIXELS_PER_METER
      );

    // Note the floor has no physics component
    // so will not be affected by gravity
  }

  // ...
};

Now, when you run the game, the "Player" entity will fall due to gravity (from its PhysicsComponent). As it falls, its CollisionComponent's Bounds will update. The Scene::CheckCollisions() loop will compare the player's bounds with the floor's bounds on every tick.

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.

Files

src
Select a file to view its content

Summary

In this lesson, we created a CollisionComponent that defines the physical shape of our entities for interaction. This component calculates its bounding box (Bounds) 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 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.
  • Bounds (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 Bounds of two components.
  • Scene-level collision checks iterate through entity pairs, currently requiring O(n2)O(n^2) checks.
  • Debug drawing of Bounds 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 117 of 117

Collision Response

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

Have a question about this lesson?
Answers are generated by AI models and may not be accurate