Collision Response

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

Ryan McCombe
Updated

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:

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:

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

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:

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.

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.

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:

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:

Dampening

We can make our ball bounces more realistic by applying a dampening effect, reducing the 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 }); } } };

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:

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:

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

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.
Next Lesson
Lesson 122 of 129

Breakout: Project Setup

Begin building a Breakout clone by integrating our level editor and engine into a new game project.

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