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:
We'll approach this by adding a virtual HandleCollision()
function to our Entity()
base class, allowing different entity types to define their unique reactions.
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:
Scene
constructor creates Player
and Floor
objects, which are both Entity
instances.Player
has a PhysicsComponent
, causing it to fall downwards due to gravity. It falls through the Floor
.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!
// ...
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:
EntityA->HandleCollision(EntityB);
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.
First, let’s see how we can make our existing Player
entity land on the floor, without falling through it.
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);
}
}
// ...
};
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()
.
Now for a different reaction: a ball that bounces off surfaces.
BouncingBall
EntitySimilar 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:
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.
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).
{0, 1}
. This is the direction the floor "pushes back".{-1, 0}
. If hit from the left, it points right, {1, 0}
.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:
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:
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:
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
});
}
}
};
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.
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!
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;
};
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:
HandleCollision()
- allows entity types to define their own reactions to specific collision partners.dynamic_cast
is used within HandleCollision()
to determine the type of the Other
entity involved.TransformComponent::Position
to push them apart.PhysicsComponent::Velocity
is used for realistic reactions like stopping or bouncing.Scene::CheckCollisions()
loop now triggers the response by calling HandleCollision()
on both colliding entities.Make entities react realistically to collisions, stopping, bouncing, and interacting based on type.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games