Collision Response

Make entities react realistically to collisions, stopping, bouncing, and interacting based on type.
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 physics collision responses
Ryan McCombe
Ryan McCombe
Posted

Our entities can now detect collisions thanks to the CollisionComponent, but they still pass through each other like ghosts. This lesson bridges the gap from detection to reaction. We'll implement two fundamental collision responses:

  1. Stopping, such as a character landing on a floor.
  2. Bouncing, such as a ball reflecting off surfaces.

We'll approach this by adding a virtual HandleCollision() function to our Entity() base class, allowing different entity types to define their unique reactions.

Starting Point

In the previous lesson, we created the CollisionComponent which calculates a world-space bounding box (WorldBounds) each frame:

#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};
};
// CollisionComponent.cpp
#include <iostream>
#include "CollisionComponent.h"

// For DrawRectOutline(), Round()
#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;
  // 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;
}

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 added a CheckCollisions() loop in the Scene to detect overlaps between entities using CollisionComponent::IsCollidingWith().

Our entities can now detect when they collide, but they don't yet do anything about it. Here's the relevant code we ended with:

// 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:
  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()
        ->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
  }

  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
};
// Scene.cpp
#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";
      }
    }
  }
}

Currently, our program has the following characteristics:

  • Our Scene constructor creates Player and Floor objects, which are both Entity instances.
  • The Player has a PhysicsComponent, causing it to fall downwards due to gravity. It falls through the Floor.
  • The CheckCollisions() loop in our Scene is detecting the Floor and Player, intersection, but our entities don’t currently react to it:
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!
// ...

Reacting to Collisions

Instead of putting all collision response logic inside the central Scene::CheckCollisions() loop, we'll use a more distributed approach. Each entity type will be responsible for defining how it reacts when it collides with other specific entity types.

We can achieve this by adding a virtual function to the base Entity class. Let's call it HandleCollision().

// Entity.h
// ...

class Entity {
public:
  // ...

  // Called when this entity collides with 'Other'
  virtual void HandleCollision(Entity& Other) {} 

  // ...
};

When the Scene detects a collision between EntityA and EntityB, it will notify both entities:

  1. EntityA->HandleCollision(EntityB);
  2. EntityB->HandleCollision(EntityA);

This allows EntityA to decide how to react to EntityB, and EntityB to decide how to react to EntityA. Let’s add it to our CheckCollisions() function, replacing our basic logging statement:

// Scene.cpp
// ...

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";
        Entities[i]->HandleCollision(*Entities[j]); 
        Entities[j]->HandleCollision(*Entities[i]); 
      }
    }
  }
}

With this structure, the base Entity does nothing on collision, but derived classes can override HandleCollision() to implement specific reactions. Let’s implement a couple of examples.

Example 1: Character Landing on Floor

First, let’s see how we can make our existing Player entity land on the floor, without falling through it.

Creating Entity Subtypes

We need specific entity types. Let's define Character and Floor classes that inherit from Entity.

We’ll also move the initial component configuration for our two objects (currently defined in the Scene constructor) to these classes. This will help remove some clutter from our Scene later:

// Character.h
#pragma once
#include "Entity.h"

class Scene;
class Character : public Entity {
 public:
  Character(Scene& Scene)
  : Entity{Scene}
  {
    AddTransformComponent()->SetPosition({6, 5});
    AddPhysicsComponent()->SetMass(50.0);
    AddImageComponent("player.png");
    AddCollisionComponent()->SetSize(1.9, 1.7);
  }
};
// Floor.h
#pragma once
#include "Entity.h"

class Scene;
class Floor : public Entity {
 public:
  Floor(Scene& Scene) : Entity{Scene} {
    AddTransformComponent()->SetPosition({4.5, 1});
    AddImageComponent("floor.png");
    AddCollisionComponent()->SetSize(5.0, 2.0);
  }
};

Over in our Scene, let’s update the constructor to use these new types. We can also delete all of their component setup, as that is now handled by the Character and Floor classes:

// Scene.h
// ...
#include "Entity.h"
#include "Character.h"
#include "Floor.h"

// ...

class Scene {
 public:
  Scene() {
    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()
      ->SetSize(1.9, 1.7);
    Entities.emplace_back(
      std::make_unique<Character>(*this));

    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);
    Entities.emplace_back(
      std::make_unique<Floor>(*this));
  }

  void HandleEvent(SDL_Event& E) {
    for (EntityPtr& Entity : Entities) {
      Entity->HandleEvent(E);
    }
  }

  // ...
};

Implementing Character's Reaction

Now, let's implement Character::HandleCollision() for our Character.

The character only cares about collisions with the Floor. We use dynamic_cast to check if the Other entity it collided with is actually a Floor. We can use dynamic_cast for this and, if it’s not a Floor, we’re not interested in reacting so we’ll just return:

// Character.h
// ...
#include "Floor.h"

// ...

class Character : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    // Check if we collided with a Floor
    Floor* FloorPtr{dynamic_cast<Floor*>(&Other)};
    
    // It's not a floor, so we don't care
    if (!FloorPtr) return;
    
    // It is a floor, so we need to react
  }
};

If it is a Floor, we next need to get the collision intersection rectangle using the GetCollisionRectangle() we added to CollisionComponent in the previous lesson. This tells us how much they overlap.

// Character.h
// ...
#include "Floor.h"

// ...

class Character : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    Floor* FloorPtr{dynamic_cast<Floor*>(&Other)};
    if (!FloorPtr) return;
    
    CollisionComponent* MyCollider{
      GetCollisionComponent()};
    CollisionComponent* FloorCollider{
      FloorPtr->GetCollisionComponent()};

    SDL_FRect Intersection;
    MyCollider->GetCollisionRectangle(
      *FloorCollider, &Intersection);
   
   // ...
  }
};

Next, to resolve the penetration, we’ll push the Character upwards out of the Floor. The distance to push is the height of the intersection rectangle. We modify the TransformComponent's position:

// Character.h
// ...
#include "Floor.h"

// ...

class Character : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    Floor* FloorPtr{dynamic_cast<Floor*>(&Other)};
    if (!FloorPtr) return;
    
    CollisionComponent* MyCollider{
      GetCollisionComponent()};
    CollisionComponent* FloorCollider{
      FloorPtr->GetCollisionComponent()};

    SDL_FRect Intersection;
    MyCollider->GetCollisionRectangle(
      *FloorCollider, &Intersection);
   
   Vec2 CurrentPos{GetTransformComponent()
     ->GetPosition()};
   GetTransformComponent()->SetPosition({
     CurrentPos.x,
     CurrentPos.y + Intersection.h
   });
   
   // ...
  }
};

Finally, we’ll stop the downward motion of the Character by setting the vertical component of its PhysicsComponent's velocity to zero.

This prevents gravity from constantly accelerating the entity, to the point where it is travelling fast enough that it can fully penetrate the Floor within a single tick of our physics simulation:

// Character.h
// ...
#include "Floor.h"

// ...

class Character : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    Floor* FloorPtr{dynamic_cast<Floor*>(&Other)};
    if (!FloorPtr) return;
    
    CollisionComponent* MyCollider{
      GetCollisionComponent()};
    CollisionComponent* FloorCollider{
      FloorPtr->GetCollisionComponent()};

    SDL_FRect Intersection;
    MyCollider->GetCollisionRectangle(
      *FloorCollider, &Intersection);
   
    Vec2 CurrentPos{GetTransformComponent()->GetPosition()};
    GetTransformComponent()->SetPosition({
      CurrentPos.x,
      CurrentPos.y + Intersection.h
    });
   
    PhysicsComponent* Physics{GetPhysicsComponent()};
    if (Physics) {
      Vec2 CurrentVel{Physics->GetVelocity()};
      // Stop vertical movement upon landing
      if (CurrentVel.y < 0) {  // Only if falling
        Physics->SetVelocity({CurrentVel.x, 0.0});
      }
    }
  }
};

If we run our game now. The Character should fall due to its PhysicsComponent, collide with the Floor, and stop correctly on top of it, thanks to the logic in Character::HandleCollision().

Screenshot showing character stopped on the floor

Example 2: Bouncing Ball

Now for a different reaction: a ball that bounces off surfaces.

Creating the BouncingBall Entity

Similar to Character, let's define a BouncingBall entity type, with some initial components and state:

// BouncingBall.h
#pragma once
#include "Entity.h"

class Scene;

class BouncingBall: public Entity {
public:
  BouncingBall(Scene& Scene) : Entity{Scene} {
    AddTransformComponent()->SetPosition({3, 4});
    AddPhysicsComponent()->SetVelocity({5, 3});
    AddImageComponent("basketball.png");
    AddCollisionComponent();
  }
};

We’ll update our Scene to include a BouncingBall, and two simple entities that it can bounce off:

// Scene.h
// ...
#include "BouncingBall.h"
// ...

class Scene {
 public:
  Scene() {
    Entities.emplace_back(
      std::make_unique<BouncingBall>(*this));

    EntityPtr& Floor{Entities.emplace_back(
      std::make_unique<Entity>(*this))};
    Floor->AddTransformComponent()
      ->SetPosition({3, 1});
    Floor->AddCollisionComponent()
      ->SetSize(7, 2);

    EntityPtr& Wall{Entities.emplace_back(
      std::make_unique<Entity>(*this))};
    Wall->AddTransformComponent()
      ->SetPosition({10, 5});
    Wall->AddCollisionComponent()
      ->SetSize(2, 5);
  }

  // ...
};

For now, our ball just falls through the floor:

Screenshot showing ball falling through the floor

Resolving Penetration

Let’s implement our bouncing behaviour by overriding the HandleCollision() function, in the same way we did for our Character. Let’s get started by grabbing the physics and transform components as we did before, as well as the collision intersection. In this case, we’ll let our ball bounce of anything, so we’ll skip checking what the Other entity is:

// BouncingBall.h
// ...

class BouncingBall : public Entity {
public:
  // ...

  void HandleCollision(Entity& Other) override {
    SDL_FRect Intersection;
    
    GetCollisionComponent()->GetCollisionRectangle(
      *Other.GetCollisionComponent(), &Intersection
    );

    PhysicsComponent* Physics{
      GetPhysicsComponent()};
    TransformComponent* Transform{
      GetTransformComponent()};
    
    // Safety check - we need these to bounce
    if (!(Physics && Transform)) return;
    
    // ...
  }
};

Let’s start by pushing the ball out of the object it collided with, similar to how we pushed our Character out from the Floor. The additional challenge here is that we don’t just always assume we need to push our ball up. We need to determine the direction based on the nature of the collision.

Collision Normal

In physics and geometry, a normal (or normal vector) is a vector that is perpendicular to a surface at a given point. Think of it as an arrow pointing directly "out" from the surface.

When two objects collide, the collision normal represents the primary direction of the impact force. It's the direction along which the objects push against each other. For simple shapes like our Axis-Aligned Bounding Boxes (AABBs), the normal is usually aligned with one of the major axes (X or Y).

  • Flat Horizontal Surface (like our Floor): If an object hits it from above, the collision normal points straight up along the positive Y-axis, e.g., {0, 1}. This is the direction the floor "pushes back".
  • Vertical Surface (like a Wall): If an object hits it from the right, the normal points straight left along the negative X-axis, e.g., {-1, 0}. If hit from the left, it points right, {1, 0}.
  • Angled Surface: For more complex shapes or angled surfaces, the normal wouldn't align perfectly with the X or Y axis, but would still point perpendicularly away from the surface at the point of impact.
Diagram showing collision normals

Knowing the collision normal is useful for realistic physics responses. It tells us the direction to push objects apart to resolve penetration and the axis along which velocity should be reflected for bouncing.

For AABB collisions, we can guess what the collision normal is based on the nature of the collision intersection rectangle:

  • If the intersection rectangle is wider than it is tall, we guess the collision is primarily vertical. If the ball was moving up, we push it back down. If the ball was moving down, we push it back up.
  • If the intersection rectangle is taller than it is wide, we guess the collision is primarily horizontal. If the ball was moving left, we push it back to the right. If the ball was moving right, we push it back to the left.
Diagram showing intersection rectangles

Let’s implement this logic:

// BouncingBall.h
// ...

class BouncingBall : public Entity {
public:
  // ...

  void HandleCollision(Entity& Other) override {
    SDL_FRect Intersection;
    
    GetCollisionComponent()->GetCollisionRectangle(
      *Other.GetCollisionComponent(), &Intersection
    );

    PhysicsComponent* Physics{
      GetPhysicsComponent()};
    TransformComponent* Transform{
      GetTransformComponent()};

    if (!(Physics && Transform)) return;

    Vec2 CurrentPos{Transform->GetPosition()};
    if (Intersection.w < Intersection.h) {
      if (Physics->GetVelocity().x > 0)
        CurrentPos.x -= Intersection.w;
      else
        CurrentPos.x += Intersection.w;
    } else {
      if (Physics->GetVelocity().y <0)
        CurrentPos.y += Intersection.h;
      else
        CurrentPos.y -= Intersection.h;
    }
    Transform->SetPosition(CurrentPos);
    
    // ...
  }
};

We should now see our ball hit the floor and then slide along it until it gets stuck in the corner:

Screenshot showing our ball reacting to collisions

Implementing Bouncing

Finally, let’s implement our bouncing behaviour. This involves reversing one of the components of our ball’s velocity. If the collision is primarily vertical, we’ll reverse the ball’s y velocity whilst, if it’s horizontal, we’ll reverse it’s x velocity:

// BouncingBall.h
// ...

class BouncingBall : public Entity {
public:
  // ...

  void HandleCollision(Entity& Other) override {
    SDL_FRect Intersection;
    
    GetCollisionComponent()->GetCollisionRectangle(
      *Other.GetCollisionComponent(), &Intersection
    );

    PhysicsComponent* Physics{
      GetPhysicsComponent()};
    TransformComponent* Transform{
      GetTransformComponent()};

    if (!(Physics && Transform)) return;

    Vec2 CurrentPos{Transform->GetPosition()};
    if (Intersection.w < Intersection.h) {
      if (Physics->GetVelocity().x > 0)
        CurrentPos.x -= Intersection.w;
      else
        CurrentPos.x += Intersection.w;
    } else {
      if (Physics->GetVelocity().y <0)
        CurrentPos.y += Intersection.h;
      else
        CurrentPos.y -= Intersection.h;
    }
    Transform->SetPosition(CurrentPos);

    Vec2 CurrentVel{Physics->GetVelocity()};
    // Wider intersection = vertical collision
    if (Intersection.w > Intersection.h) {
      Physics->SetVelocity({
        CurrentVel.x,
        -CurrentVel.y
      });
    } else {
      Physics->SetVelocity({
        -CurrentVel.x,
        CurrentVel.y
      });
    }
  }
};

With these changes, our ball should now bounce off our other entities:

Screenshot showing our ball bouncing

We can make this more realistic by applying a dampening effect, reducing the ball’s speed on each impact:

// BouncingBall.h
// ...

class BouncingBall : public Entity {
public:
  // ...

  void HandleCollision(Entity& Other) override {
float DAMPENING{0.8}; Vec2 CurrentVel{Physics->GetVelocity()}; if (Intersection.w > Intersection.h) { Physics->SetVelocity({ CurrentVel.x, -CurrentVel.y * DAMPENING }); } else { Physics->SetVelocity({ -CurrentVel.x * DAMPENING, CurrentVel.y }); } } };
Screenshot showing our ball bouncing with dampening

More Robust Penetration Resolution

The penetration resolution logic above is quite simple. It pushes the ball out based only on the smallest overlap dimension. This can sometimes cause slightly incorrect behavior if the ball hits a corner, or if the "push out" moves the object into a position where it’s now colliding with something else.

A more robust (but complex) methods involve techniques like calculating the exact time of impact and "rewinding" the physics state to that exact moment. Examining the world at the specific moment the collision happened lets us better understand how it happened, so we can apply the bounce reaction more accurately.

Having implemented the reaction (eg, the change in the ball’s velocity), we then re-simulate the remaining portion of the timestep to determine if that change caused any further collisions.

This technique is called Continuous Collision Detection (CCD) and is beyond the scope of this course, but is often used in more advanced physics engines. Our simpler "push-out" method is still commonly used, as it is less expensive and sufficiently accurate for less important objects.

Handling Different Reactions

The use of virtual functions allows for polymorphic collision handling. The Scene doesn't need to know the specific types involved; it simply calls HandleCollision(), and the runtime system ensures the correct overridden version for each Entity subtype is executed.

This makes our system easy to extend without requiring us to add additional complexity to important classes such as Scene. For example, let’s reintroduce our Character and Floor to the Scene:

// Scene.h
// ...

#include "BouncingBall.h"
#include "Character.h"
#include "Floor.h"

// ...

class Scene {
 public:
  Scene() {
    Entities.emplace_back(
      std::make_unique<BouncingBall>(*this));
    Entities.emplace_back(
      std::make_unique<Character>(*this));
    Entities.emplace_back(
      std::make_unique<Floor>(*this));
  }
};

Without any further changes, our bouncing ball now reacts with our Character and Floor, because it reacts with any Entity in our scene that has a CollisionComponent:

Screenshot showing our ball bouncing off the character

Updating our Character to react to being hit by a BouncingBall just takes a few lines of code in our Character class. In this example, we’ve also moved our floor collision logic into a new private function, to keep things organized:

// Character.h
// ...
#include "BouncingBall.h"

class Character : public Entity {
 public:
  // ...

  void HandleCollision(Entity& Other) override {
    auto* FloorPtr{dynamic_cast<Floor*>(&Other)};
    if (FloorPtr) HandleCollision(FloorPtr);

    auto* BallPtr{dynamic_cast<BouncingBall*>(&Other)};
    if (BallPtr) HandleCollision(BallPtr);
  }

private:
void HandleCollision(Floor* FloorPtr){/*...*} void HandleCollision(BouncingBall* BallPtr) { std::cout << "A ball hit me!\n"; } };
A ball hit me!

Complete Code

Below, we’ve provided all the files we created and updated in this lesson. Our new Floor, BouncingBall, and Character classes are here:

#pragma once
#include "Entity.h"

class Scene;
class Floor : public Entity {
 public:
  Floor(Scene& Scene) : Entity{Scene} {
    AddTransformComponent()->SetPosition({4.5, 1});
    AddImageComponent("floor.png");
    AddCollisionComponent()->SetSize(5.0, 2.0);
  }
};
#pragma once
#include "Entity.h"

class Scene;

class BouncingBall : public Entity {
public:
  BouncingBall(Scene& Scene) : Entity{Scene} {
    AddTransformComponent()->SetPosition({3, 4});
    AddPhysicsComponent()->SetVelocity({5, 3});
    AddImageComponent("basketball.png");
    AddCollisionComponent();
  }

  void HandleCollision(Entity& Other) override {
    SDL_FRect Intersection;

    GetCollisionComponent()->GetCollisionRectangle(
        *Other.GetCollisionComponent(), &Intersection);

    PhysicsComponent* Physics{GetPhysicsComponent()};
    TransformComponent* Transform{GetTransformComponent()};

    if (!(Physics && Transform)) return;

    Vec2 CurrentPos{Transform->GetPosition()};
    if (Intersection.w < Intersection.h) {
      if (Physics->GetVelocity().x > 0)
        CurrentPos.x -= Intersection.w;
      else
        CurrentPos.x += Intersection.w;
    } else {
      if (Physics->GetVelocity().y < 0)
        CurrentPos.y += Intersection.h;
      else
        CurrentPos.y -= Intersection.h;
    }
    Transform->SetPosition(CurrentPos);

    Vec2 CurrentVel{Physics->GetVelocity()};
    if (Intersection.w > Intersection.h) {
      Physics->SetVelocity({
          CurrentVel.x,
          -CurrentVel.y * 0.8f
      });
    } else {
      Physics->SetVelocity({
          -CurrentVel.x * 0.8f,
          CurrentVel.y
      });
    }
  }
};
#pragma once
#include "BouncingBall.h"
#include "Entity.h"
#include "Floor.h"

class Scene;
class Floor;

class Character : public Entity {
 public:
  Character(Scene& Scene)
  : Entity{Scene}
  {
    AddTransformComponent()->SetPosition({6, 5});
    AddPhysicsComponent()->SetMass(50.0);
    AddImageComponent("player.png");
    AddCollisionComponent()->SetSize(1.9, 1.7);
  }

  void HandleCollision(Entity& Other) override {
    auto* FloorPtr{dynamic_cast<Floor*>(&Other)};
    if (FloorPtr) HandleCollision(FloorPtr);

    auto* BallPtr{dynamic_cast<BouncingBall*>(&Other)};
    if (BallPtr) HandleCollision(BallPtr);
  }

private:
  void HandleCollision(Floor* FloorPtr) {
    CollisionComponent* MyCollider{
      GetCollisionComponent()};
    CollisionComponent* FloorCollider{
      FloorPtr->GetCollisionComponent()};

    SDL_FRect Intersection;
    MyCollider->GetCollisionRectangle(
      *FloorCollider, &Intersection);
   
    Vec2 CurrentPos{GetTransformComponent()->GetPosition()};
    GetTransformComponent()->SetPosition({
      CurrentPos.x,
      CurrentPos.y + Intersection.h
    });
   
    PhysicsComponent* Physics{GetPhysicsComponent()};
    if (Physics) {
      Vec2 CurrentVel{Physics->GetVelocity()};
      if (CurrentVel.y < 0) {
        Physics->SetVelocity({CurrentVel.x, 0.0});
      }
    }
  }

  void HandleCollision(BouncingBall* BallPtr) {
    std::cout << "Ouch!\n";
  }
};

Our updated Scene and Entity files are provided below, with the changes we made in this lesson highlighted:

// Scene.h
#pragma once
#include <SDL.h>
#include <vector>
#include "AssetManager.h"
#include "Character.h"
#include "BouncingBall.h"
#include "Floor.h"

#define DRAW_DEBUG_HELPERS

using EntityPtr = std::unique_ptr<Entity>;
using EntityPtrs = std::vector<EntityPtr>;

class Scene {
 public:
  Scene() {
    Entities.emplace_back(
      std::make_unique<BouncingBall>(*this));
    Entities.emplace_back(
      std::make_unique<Character>(*this));
    Entities.emplace_back(
      std::make_unique<Floor>(*this));
  }

  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
};
// Scene.cpp
#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()};
    if (!ColA) continue;

    for (size_t j{i + 1}; j < Entities.size(); ++j) {
      CollisionComponent* ColB{
        Entities[j]->GetCollisionComponent()};
      if (!ColB) continue;

      if (ColA->IsCollidingWith(*ColB)) {
        Entities[i]->HandleCollision(*Entities[j]);
        Entities[j]->HandleCollision(*Entities[i]);
      }
    }
  }
}
#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 void HandleCollision(Entity& Other) {}

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

Summary

In this lesson, we transitioned from simply detecting collisions to implementing meaningful reactions. We adopted a distributed response model using a virtual HandleCollision(Entity& Other) method in the base Entity class.

We created specific entity subtypes (Character, Floor, BouncingBall) that override HandleCollision() to define their unique behaviors when interacting with other types, identified using dynamic_cast.

Key takeaways:

  • Collision Response: The actions taken after a collision is detected.
  • Distributed Response: Using virtual functions - HandleCollision() - allows entity types to define their own reactions to specific collision partners.
  • Identifying Participants: dynamic_cast is used within HandleCollision() to determine the type of the Other entity involved.
  • Penetration Resolution: Pushing entities apart based on the intersection rectangle is used to prevent objects from occupying the same space in an unrealistic way. We adjusted TransformComponent::Position to push them apart.
  • Motion Correction: Modifying PhysicsComponent::Velocity is used for realistic reactions like stopping or bouncing.
  • Stopping: Typically involves setting the velocity component along the collision normal to zero (e.g., vertical velocity when landing).
  • Bouncing: Involves reversing the velocity component along the collision normal (approximated for AABBs by comparing intersection width/height).
  • The Scene::CheckCollisions() loop now triggers the response by calling HandleCollision() on both colliding entities.
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
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:

  • 110 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