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
.
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.
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
...
When an object is moving very quickly, or our physics system is ticking slowly, it may miss some collisions. For example, a projectile may be moving so quickly that it travels through its target within a single frame:
From the perspective of our physics system, the projectile was never overlapping its target, so this would have been inaccurately reported as a miss.
A common way to solve this is using substepping. For example, within every frame, we may decide to have our physics system tick 4 times, where each step simulates 25% of the total frame delta time:
We won’t need physics sub-stepping in our simple games, but it’s a useful concept to be aware of
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.
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.
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;
}
};
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:
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
We can also keep track of additional data to assist our collision system in determining how it should resolve collisions. A frequently useful addition is to keep track of both the previous position and the current position of the object.
// GameObject.h
// ...
class GameObject {
// ...
private:
Vec2 Position{0, 0};
// Where was I on the previous frame?
Vec2 PreviousPosition{Position};
// ...
};
// GameObject.cpp
// ...
void GameObject::Tick(float DeltaTime) {
PreviousPosition = Position;
}
This lets our collision resolver retrieve where the objects were in the previous frame, which can further help it understand what is going on.
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";
}
}
}
}
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
// ...
}
}
In this section, we had a situation where we have a base class that, in a complex game, is likely to have many child classes. Additionally, all (or most) of these child classes are expected to override some simple inherited functions in a predictable, repeated pattern:
// GameObject.h
// ...
class Projectile : public GameObject {
public:
using GameObject::GameObject;
GameObjectType GetType() override {
return GameObjectType::Projectile;
}
std::string GetTypeName() override {
return "Projectile";
}
};
class Enemy: public GameObject {
public:
using GameObject::GameObject;
GameObjectType GetType() override {
return GameObjectType::Enemy;
}
std::string GetTypeName() override {
return "Enemy";
}
};
This is a situation where text replacement macros are often used. To simplify and standardize the overriding of these methods, we’d often provide a macro that all of our child classes can use. It might look something like this:
#define GAME_OBJECT_TYPE(TypeName) \
GameObjectType GetType() override { \
return GameObjectType::TypeName; \
} \
\
std::string GetTypeName() override { \
return #TypeName; \
} \
Using this macro in a derived class would look like this:
// GameObject.h
// ...
class Projectile : public GameObject {
public:
using GameObject::GameObject;
GAME_OBJECT_TYPE(Projectile)
GameObjectType GetType() override {
return GameObjectType::Projectile;
}
std::string GetTypeName() override {
return "Projectile";
}
};
We covered text replacement macros in more detail in our introductory course:
This chapter gave a brief introduction to the most important topics in game physics. Our simple system doesn’t handle situations like objects colliding with multiple things at once, and there’s a lot of complex, low-level work we could do to optimize its performance.
Creating a fully featured and optimized physics system is a huge undertaking and ongoing area of research. When games need complex physics simulations, it’s extremely common that the work be offloaded to a third-party library that has undergone years of research and development.
A common and free choice for 2D games is Box2D. 3D game engines like Unreal and Unity typically come bundled with a physics system, but standalone options like Havok and Bullet are also available.
We’ve included the complete versions of our GameObject
and Scene
classes below, containing all the techniques and concepts we covered in this lesson:
#pragma once
#include <SDL.h>
#include <vector>
#include "GameObject.h"
using GameObjectPtr = std::unique_ptr<GameObject>;
using GameObjectPtrs = std::vector<GameObjectPtr>;
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));
}
const GameObject& GetPlayerCharacter() const {
return *Objects[0];
}
const GameObjectPtrs& GetObjects() const {
return Objects;
}
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
};
}
Vec2 ToWorldSpace(const Vec2& Pos) const {
auto [vx, vy, vw, vh]{Viewport};
float HorizontalScaling{
WorldSpaceWidth / vw};
float VerticalScaling{
WorldSpaceHeight / vh};
return {
(Pos.x - vx) * HorizontalScaling,
WorldSpaceHeight - (Pos.y - vy)
* VerticalScaling
};
}
void HandleEvent(SDL_Event& E) {
for (GameObjectPtr& Object : Objects) {
Object->HandleEvent(E);
}
}
void Tick(float DeltaTime) {
for (GameObjectPtr& Object : Objects) {
Object->Tick(DeltaTime);
}
}
void Render(SDL_Surface* Surface) {
SDL_GetClipRect(Surface, &Viewport);
for (GameObjectPtr& Object : Objects) {
Object->Render(Surface);
}
}
const SDL_Rect& GetViewport() const {
return Viewport;
}
private:
SDL_Rect Viewport;
GameObjectPtrs Objects;
float WorldSpaceWidth{14}; // meters
float WorldSpaceHeight{6}; // meters
};
#pragma once
#include <SDL.h>
#include "BoundingBox.h"
#include "Vec2.h"
#include "Image.h"
class Scene;
enum class GameObjectType {
GameObject, Player, Floor, FloorTrap
};
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
}} {}
virtual GameObjectType GetType() {
return GameObjectType::GameObject;
}
virtual std::string GetTypeName() {
return "GameObject";
}
bool HasType(GameObjectType TargetType) {
return GetType() == TargetType;
}
virtual void OnHit(GameObject& Target) {}
virtual bool CanPassThrough() { return true; }
void HandleEvent(const SDL_Event& E);
const Vec2& GetPosition() const {
return Position;
}
void Tick(float DeltaTime);
void Render(SDL_Surface* Surface);
void ApplyForce(const Vec2& Force) {
Acceleration += Force / Mass;
}
virtual ~GameObject() = default;
private:
Image Image;
const Scene& Scene;
bool isMovable;
Vec2 Position{0, 0};
Vec2 Velocity{0, 0};
Vec2 Acceleration{0, -9.8};
float Mass{70};
BoundingBox Bounds;
float DragCoefficient{0.2};
Vec2 DragForce() const {
return -Velocity * DragCoefficient
* Velocity.GetLength();
}
float GetFrictionCoefficient() const {
return isOnGround ? 0.5 : 0;
}
Vec2 FrictionForce(float DeltaTime) const {
float MaxMagnitude{
GetFrictionCoefficient()
* Mass * -Acceleration.y};
if (MaxMagnitude <= 0) return Vec2(0, 0);
float StoppingMagnitude{
Mass *
Velocity.GetLength() / DeltaTime};
return -Velocity.Normalize() * std::min(
MaxMagnitude, StoppingMagnitude);
}
void Clamp(Vec2& V) const {
V.x = std::abs(V.x) > 0.01 ? V.x : 0;
V.y = std::abs(V.y) > 0.01 ? V.y : 0;
}
void ApplyImpulse(const Vec2& Impulse) {
Velocity += Impulse / Mass;
}
void ApplyPositionalImpulse(
const Vec2& Origin, float Magnitude
) {
Vec2 Displacement{GetPosition() - Origin};
Vec2 Direction{Displacement.Normalize()};
float Distance{Displacement.GetLength()};
float AdjustedMagnitude{
Magnitude /
((Distance + 0.1f) * (Distance + 0.1f))};
ApplyImpulse(Direction * AdjustedMagnitude);
}
void HandleCollisions();
bool isOnGround;
};
#define GAME_OBJECT_TYPE(TypeName) \
GameObjectType GetType() override { \
return GameObjectType::TypeName; \
} \
\
std::string GetTypeName() override { \
return #TypeName; \
} \
class Player : public GameObject {
public:
using GameObject::GameObject;
GAME_OBJECT_TYPE(Player)
void TakeDamage(int Damage) {
Health -= Damage;
}
int Health{100};
};
class Floor : public GameObject {
public:
using GameObject::GameObject;
GAME_OBJECT_TYPE(Floor)
bool CanPassThrough() override {
return false;
}
};
class FloorTrap : public Floor {
public:
using Floor::Floor;
GAME_OBJECT_TYPE(FloorTrap)
void OnHit(GameObject& Target) override {
if (Player* P{dynamic_cast<Player*>(&Target)}) {
P->TakeDamage(10);
}
}
};
#include <SDL.h>
#include "GameObject.h"
#include "Scene.h"
#include "BoundingBox.h"
#define DRAW_BOUNDING_BOXES
#define DRAW_TRAJECTORIES
#ifdef DRAW_TRAJECTORIES
namespace{
SDL_Surface* Trajectories{
SDL_CreateRGBSurfaceWithFormat(
0, 700, 300, 32,
SDL_PIXELFORMAT_RGBA32
)};
}
#endif
void GameObject::Tick(float DeltaTime) {
if (!isMovable) return;
ApplyForce(FrictionForce(DeltaTime));
ApplyForce(DragForce());
Velocity += Acceleration * DeltaTime;
Position += Velocity * DeltaTime;
Bounds.SetPosition(GetPosition());
HandleCollisions();
Acceleration = {0, -9.8};
Clamp(Velocity);
}
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
)) {
O->OnHit(*this);
if (O->CanPassThrough()) {
continue;
}
if (O->HasType(GameObjectType::Floor)) {
isOnGround = true;
Position.y += Intersection.h;
Velocity.y = 0;
}
}
}
}
void GameObject::Render(SDL_Surface* Surface) {
#ifdef DRAW_TRAJECTORIES
auto [x, y]{Scene.ToScreenSpace(GetPosition())};
SDL_Rect PositionIndicator{
int(x) - 16, int(y), 20, 20};
SDL_FillRect(Trajectories, &PositionIndicator,
SDL_MapRGB(Trajectories->format,
220, 0, 0));
SDL_BlitSurface(Trajectories, nullptr,
Surface, nullptr);
#endif
Image.Render(Surface,
Scene.ToScreenSpace(GetPosition()));
#ifdef DRAW_BOUNDING_BOXES
Bounds.Render(Surface, Scene);
#endif
}
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 (!isOnGround) return;
ApplyImpulse({0.0f, 300.0f});
}
}
}
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:
SDL_FRect
) provides data (width, height) crucial for resolving the collision.isOnGround
to communicate collision state to other parts of the game logic (e.g., jumping).virtual
methods enables defining specific behaviors (like damage on hit or pass-through ability) for different object types.std::unique_ptr
manages memory for polymorphic objects stored in collections like std::vector
.Implement bounding box collision detection and response between game objects
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games