Handling Object Collisions
Implement bounding box collision detection and response between game objects
In this lesson, we'll revisit our physics system and integrate our new bounding boxes and rectangle intersection tools to allow objects to interact with each other.
We'll start by adding a floor object, and we'll use bounding box intersections to detect when our object hits the floor (or anything else).
Finally, we'll code the logic to react to these collisions appropriately, with behaviors such as preventing objects from overlapping or reducing our player's health if they get hit by a projectile.
Currently, we're simulating the effects of gravity, which is constantly accelerating objects towards the floor of our world. However, we don't really have a "floor" - we're just faking it by limiting our object's y
position:
// GameObject.cpp
// ...
void GameObject::Tick(float DeltaTime) {
ApplyForce(FrictionForce(DeltaTime));
ApplyForce(DragForce());
Velocity += Acceleration * DeltaTime;
Position += Velocity * DeltaTime;
Acceleration = {0, -9.8};
// Don't fall through the floor
if (Position.y < 2) {
Position.y = 2;
Velocity.y = 0;
}
Bounds.SetPosition(GetPosition());
Clamp(Velocity);
}
// ...
Let's improve this by introducing an object that represents our floor, and prevents other objects from falling through it. We'll do this using our new bounding boxes and intersection tests, rather than the hard-coded assumption that the ground's y-position is always 2
.
Adding Stationary Objects
Let's add an object to our scene to represent our floor. We typically don't want our floor and similar objects to be affected by gravity or movable in general. To handle this, we'll add an isMovable
boolean to our GameObject
class:
// GameObject.h
// ...
class GameObject {
public:
GameObject(
const std::string& ImagePath,
const Vec2& InitialPosition,
float Width,
float Height,
const Scene& Scene,
bool isMovable = false
) : Image{ImagePath},
Position{InitialPosition},
Scene{Scene},
isMovable{isMovable},
Bounds{
SDL_FRect{
InitialPosition.x, InitialPosition.y,
Width, Height
}} {}
private:
bool isMovable;
};
Within our Tick()
function, we'll skip all of our physics simulations for stationary objects:
// GameObject.cpp
// ...
void GameObject::Tick(float DeltaTime) {
if (!isMovable) return;
}
// ...
Let's update our Scene
to make our dwarf movable, and construct an immovable object representing our floor:
// Scene.h
// ...
class Scene {
public:
Scene() {
Objects.emplace_back("dwarf.png",
Vec2{6, 2.8}, 1.9, 1.7, *this, true);
Objects.emplace_back("floor.png",
Vec2{4.5, 1}, 5, 2, *this, false);
}
// ...
};
We should now see both of our objects rendered, and our character will fall until it reaches our hardcoded Position.y < 2
check.

Collision Checks
To determine what an object is colliding with, the object will need access to the things within our scene. We'll add a getter to our Scene
to provide this access:
// Scene.h
// ...
class Scene {
public:
// ...
const std::vector<GameObject>& GetObjects() const {
return Objects;
}
// ...
};
Within our GameObject
class, we'll add a HandleCollisions()
function.
// GameObject.h
// ...
class GameObject {
// ...
private:
// ...
void HandleCollisions();
};
// GameObject.cpp
// ...
void GameObject::HandleCollisions() {
// ...
}
We'll call it within our Tick()
function. We'll also remove our rudimentary floor check, as our HandleCollision()
function will take care of it eventually:
// GameObject.cpp
// ...
void GameObject::Tick(float DeltaTime) {
// Don't fall through the floor
if (Position.y < 2) {
Position.y = 2;
Velocity.y = 0;
}
Bounds.SetPosition(GetPosition());
HandleCollisions();
Clamp(Velocity);
}
// ...
Running our game, we should now see that our player immediately falls through the floor and off the bottom of our screen:

To solve this problem, we first need to detect when our character hits the floor. Earlier in the chapter, we added bounding boxes to our GameObject
instances so, to understand which objects are colliding, we need to check which bounding boxes are intersecting.
Let's do this within our HandleCollisions()
function:
// GameObject.cpp
// ...
void GameObject::HandleCollisions() {
for (const GameObject& O : Scene.GetObjects()) {
// Prevent self-collision
if (&O == this) continue;
SDL_FRect Intersection;
if (Bounds.GetIntersection(
O.Bounds, &Intersection
)) {
std::cout << "Collision Detected\n";
// Handle Collision...
} else {
std::cout << "No Collision\n";
}
}
}
We should now see collisions being detected. Eventually, our player will completely fall through the floor to the space below it, at which point their bounding boxes will no longer be overlapping:
Collision Detected
Collision Detected
Collision Detected
...
No Collision
No Collision
...
Reacting to Collisions
Once we've detected a collision, we next need to understand how to react to it. The nature of our reaction depends entirely on our game and the mechanics we're trying to create.
Throughout the rest of this lesson, we'll cover many techniques to understand the nature of collisions so we can create more dynamic reactions. For now, though, the only possible collision our system could have detected was the character falling into the floor, so let's react to that.
The reaction to floor collisions is usually pretty standard across games - we resolve the overlap by moving our object out of the collision. When falling onto a surface, this typically means pushing our object upwards by increasing it's Position.y
.
To understand how much we need to increase y
by, we need to determine how far our character has fallen into the floor. The most generally useful approach is to retrieve the intersection rectangle using a function like the GetIntersection()
method we added to our BoundingBox
class in the previous lesson.
Then, using the intersection rectangle calculated by GetIntersection()
, we can determine the overlap depth. For a simple vertical collision (like landing on a floor), we push our object up by the height of that intersection (Intersection.h
), thereby resolving the overlap and placing our object correctly on top of the surface it was colliding with:
// GameObject.cpp
// ...
void GameObject::HandleCollisions() {
for (const GameObject& O : Scene.GetObjects()) {
// Prevent self-collision
if (&O == this) continue;
SDL_FRect Intersection;
if (Bounds.GetIntersection(
O.Bounds, &Intersection
)) {
Position.y += Intersection.h;
// See note below
Velocity.y = 0;
}
}
}
Remember, the physics and collision reactions are all happening within the same frame - the player never sees our objects overlapping. It happens behind the scenes, and our HandleCollision()
function resolves it before the player sees any overlap.
Note: Updating Velocity
In most cases, we should ensure we change the velocity of objects as a result of collisions, not just their position. Not setting velocity is a common source of bugs.
If we don't do it, our program can still appear to be working correctly. But, behind the scenes, the gravity acceleration is constantly increasing the velocity, and, eventually, it can get so high that our object can fully move through the floor within a single frame without the collision being detected.
Tracking when the Character is on the Ground
In addition to moving our character, our floor collisions should also update a member variable to let other code determine whether the character is currently on the ground.
By default, we'll assume an object is not on the ground on any given frame, unless our collision system detects that it is:
// GameObject.h
// ...
class GameObject {
// ...
private:
// ...
bool isOnGround;
};
// GameObject.cpp
// ...
void GameObject::HandleCollisions() {
isOnGround = false;
for (const GameObject& O : Scene.GetObjects()) {
// Prevent self-collision
if (&O == this) continue;
SDL_FRect Intersection;
if (Bounds.GetIntersection(
O.Bounds, &Intersection
)) {
isOnGround = true;
Position.y += Intersection.h;
Velocity.y = 0;
}
}
}
In our HandleEvent()
function, we're currently controlling whether our character can jump based on a hard-coded comparison of its y
position. Let's update that to use our new variable:
// GameObject.cpp
// ...
void GameObject::HandleEvent(
const SDL_Event& E) {
if (E.type == SDL_MOUSEBUTTONDOWN) {
// Create explosion at click position
if (E.button.button == SDL_BUTTON_LEFT) {
ApplyPositionalImpulse(
Scene.ToWorldSpace({
static_cast<float>(E.button.x),
static_cast<float>(E.button.y)
}), 1000);
}
} else if (E.type == SDL_KEYDOWN) {
// Jump
if (E.key.keysym.sym == SDLK_SPACE) {
if (Position.y > 2) return;
if (!isOnGround) return;
ApplyImpulse({0.0f, 300.0f});
}
}
}
We're using the same logic to enable friction in our GetCoefficientFunction()
function, so let's update that too:
// GameObject.h
// ...
class GameObject {
// ...
private:
// ...
float GetFrictionCoefficient() const {
return Position.y > 2 ? 0 : 0.5;
return isOnGround ? 0.5 : 0;
}
};
How did I Collide?
A common requirement we will have when implementing our game logic is a need to understand not just when a collision happened, but the nature of that collision.
For example, our HandleCollision()
function may encounter a situation like the following:

It's not entirely clear how this should be resolved. If the player got into this situation by falling down, the expected resolution would be to push the character back up. But, if they got into this situation by jumping into the wall from the left, the natural response would be to push the character back to the left:

To understand how to resolve this situation, our HandleCollision()
function often needs to get a deeper understanding of how this situation arose. This might involve tactics like:
- Checking the velocity of the player
- Comparing the positions of the colliding objects
- Comparing the width and height of the intersection rectangle
Ultimately, how we react to any collision will be a judgment call based on the needs of our game. The following code shows some examples of implementing these checks:
// GameObject.cpp
// ...
void GameObject::HandleCollisions() {
isOnGround = false;
for (const GameObject& O
: Scene.GetObjects()
) {
// Prevent self-collision
if (&O == this) continue;
SDL_FRect Intersection;
if (Bounds.GetIntersection(
O.Bounds, &Intersection
)) {
if (Velocity.x > 0) {
std::cout << "I was moving right\n";
}
if (Velocity.y >= 0) {
std::cout << "I wasn't falling\n";
}
if (Position.x < O.Position.x) {
std::cout << "I'm to the left of the "
"object I collided with\n";
}
if (Intersection.h > Intersection.w) {
std::cout << "The intersection is "
"mostly vertical\n";
}
std::cout << "I think I should be pushed "
"to the left";
Position.x -= Intersection.w;
}
}
}
I was moving right
I wasn't falling
I'm to the left of the object I collided with
The intersection is mostly vertical
I think I should be pushed to the left
Multiple Colliders
For more complex simulations, our object may need to be comprised of multiple bounding boxes. For example, we might need to determine if objects hit our player's weapon, shield, or body:

We'll introduce an efficient way to let our objects have multiple bounding boxes in the next chapter. However, this is less necessary than we might expect. We can create many mechanics with a single bounding box and some clever logic.
For example, classic platformer games typically have mechanics where jumping on the head of an enemy defeats them, but hitting any other part causes the player to lose a life.
An intuitive approach to solve this problem might be to add separate bounding boxes for the head and the body, but this is rarely necessary. Instead, the illusion can be created with a single bounding box, and comparing the state of the objects at the time they intersect:
// GameObject.cpp
// ...
void GameObject::HandleCollisions() {
isOnGround = false;
for (const GameObject& O
: Scene.GetObjects()
) {
// Prevent self-collision
if (&O == this) continue;
SDL_FRect Intersection;
if (Bounds.GetIntersection(
O.Bounds, &Intersection
)) {
if (Position.y > O.Position.y) {
std::cout << "I landed on his head";
}
}
}
}
What did I Collide With?
In a more complex game, our objects can collide with many different things, such as enemies, walls, and projectiles.
If the player collides with a wall, we might want to change their position but, if they collide with a projectile, perhaps we want to reduce their health instead.

In a real project, it is typically the case that our objects will use some form of inheritance based on a polymorphic base type. Within that context, we can add a virtual method for retrieving information as to the type of the object.
Derived types can override those functions as needed:
// GameObject.h
// ...
enum class GameObjectType {
GameObject, Player, Projectile, Floor
};
class GameObject {
public:
virtual GameObjectType GetType() {
return GameObjectType::GameObject;
}
virtual std::string GetTypeName() {
return "GameObject";
}
bool HasType(GameObjectType TargetType) {
return GetType() == TargetType;
}
virtual ~GameObject() = default;
// ...
};
class Player : public GameObject {
public:
using GameObject::GameObject;
GameObjectType GetType() override {
return GameObjectType::Player;
}
std::stringGetTypeName() override {
return "Player";
}
};
class Floor: public GameObject {
public:
using GameObject::GameObject;
GameObjectType GetType() override {
return GameObjectType::Floor;
}
std::stringGetTypeName() override {
return "Floor";
}
};
class Projectile : public GameObject {
public:
using GameObject::GameObject;
GameObjectType GetType() override {
return GameObjectType::Projectile;
}
std::stringGetTypeName() override {
return "Projectile";
}
};
To implement run-time polymorphism, systems like our Scene
and physics code will be working with pointers or references to that polymorphic base type. For example, rather than managing a collection of GameObject
instances, our Scene
might manage a collection of GameObject
pointers, or smart pointers:
// Scene.h
// ...
#include <memory> // for std::unique_ptr
class Scene {
public:
Scene() {
Objects.emplace_back(std::make_unique<Player>(
"dwarf.png", Vec2{6, 2.8}, 1.9, 1.7, *this, true));
Objects.emplace_back(std::make_unique<Floor>(
"floor.png", Vec2{4.5, 1}, 5, 2, *this, false));
}
// ...
private:
std::vector<std::unique_ptr<GameObject>> Objects;
// ...
};
Note that this type change requires more code updates than what is shown above. In the Complete Code section at the end of this lesson, we include all the changes.
Now that we've split our game objects across multiple types, our collision system has more information with which to determine its reaction:
// GameObject.cpp
// ...
void GameObject::HandleCollisions() {
isOnGround = false;
for (const std::unique_ptr<GameObject>& O
: Scene.GetObjects()
) {
// Prevent self-collision
if (O.get() == this) continue;
SDL_FRect Intersection;
if (Bounds.GetIntersection(
O->Bounds, &Intersection
)) {
std::cout << "I hit a " << O->GetTypeName();
if (O->HasType(GameObjectType::Floor)) {
std::cout << "\nReacting to floor..."
// ...
}
}
}
}
I hit a floor
Reacting to floor...
A common design is also to let the object itself determine what effect it should have when it hits some other object. This can include simple values that the colliding object can examine or functions that it can call for more complex behaviors.
Below, we add a CanPassThrough()
method that determines whether our type is a solid object like the floor or something that objects can move through such as light foliage:
// GameObject.h
// ...
class GameObject {
public:
virtual bool CanPassThrough() { return true; };
// ...
};
class Floor : public GameObject {
public:
bool CanPassThrough() override { return false; }
// ...
};
We can also add an OnHit()
method to our base class, which subtypes can override to create more complex hit interactions. Below, our FloorTrap
type uses this to inflict damage if it hits a Player
:
// GameObject.h
// ...
class GameObject {
public:
virtual void OnHit(GameObject& Target) {}
// ...
};
// ...
class Player : public GameObject {
public:
// ...
void TakeDamage(int Damage) {
Health -= Damage;
}
int Health{100};
};
class FloorTrap : public Floor {
public:
using Floor::Floor;
void OnHit(GameObject& Target) override {
if (Player* P{dynamic_cast<Player*>(&Target)}) {
P->TakeDamage(10);
}
}
};
Using these new functions in our collision system would look something like this:
// GameObject.h
// ...
void GameObject::HandleCollisions() {
isOnGround = false;
for (const GameObject& O : Scene.GetObjects()) {
// Prevent self-collision
if (&O == this) continue;
SDL_FRect Intersection;
if (Bounds.GetIntersection(
O.Bounds, &Intersection
)) {
O->OnHit(this);
if (O->CanPassThrough()) {
// I can pass through this object - no need
// to change my position or velocity
continue;
}
// I can't pass through - I need to be
// pushed back
// ...
}
}
Complete Code
We've included the complete versions of our GameObject
and Scene
classes below, containing all the techniques and concepts we covered in this lesson:
Summary
In this lesson, we made our game objects interact by implementing a collision system. We replaced arbitrary position limits with detection logic based on bounding box intersections. We focused on resolving collisions by correcting object positions and velocities, particularly for floor interactions, and applied polymorphism to allow varied responses depending on what objects are colliding.
Key Takeaways:
- Check for collisions by iterating through scene objects and testing bounding box intersections, skipping self-collision checks.
- The intersection rectangle (
SDL_FRect
) provides data (width, height) crucial for resolving the collision. - Basic floor collision response involves moving the object up by the intersection height and zeroing vertical velocity.
- Use flags like
isOnGround
to communicate collision state to other parts of the game logic (e.g., jumping). - More advanced resolution considers collision angle/sides by comparing intersection width and height or object velocities.
- Object-oriented design with
virtual
methods enables defining specific behaviors (like damage on hit or pass-through ability) for different object types. - Using smart pointers like
std::unique_ptr
manages memory for polymorphic objects stored in collections likestd::vector
.
Building with Components
Learn how composition helps build complex objects by combining smaller components