This is part of an ongoing series where we write a complete 2D game engine in C++ and SFML. A new tutorial is released every Monday. You can find the complete list of tutorials here and download the source code from the projects GitHub page.We’ve been working on the collision system for the last three weeks and we’ve made a lot of progress in that time. So far we’ve: With that done it’s on to the main event: the collision system. This will follow a similar structure to that of the drawing system. Before we write anything, let’s define what we want from our collision system: In our constructor, we will setup collisionLayers. We’ll use this structure to define which layers collide by storing a map along with a bitmask. Using the bitmask we can set specific bit positions that will represent other layers. Hopefully, this will become clearer shortly. For each collision layer we: We add the colliders to the map along with their collision layer so that when it comes to resolving collisions we can ignore batches of them that have a bitmask of 0. This will be explained more when we write the Resolve function.Now we can add objects we’ll write the function that removes objects from the system. We loop through each collision layer in our collidables map and check if the object is queued for removal. If it is, it is removed from this system. This may look familiar to you as it is the same way we remove objects from the ObjectCollection and our drawing system.The collidable systems update function is called each frame. It will be responsible for initialising the quadtree and then calling the Resolve function, which performs the actual collision checks. Before the system can call the Resolve function it needs to clear the quadtree and re-insert all the collidables. We have to do this because if the objects move around the game and transition from one quadtree node to another, we currently have no way of dynamically updating its node in the quadtree. In future we can look at writing dynamic node updates but for now this will do. Because the quadtree prevents many unnecessary collision checks I am not too concerned about the performance hit we take here. For more information on quadtrees see my previous tutorials. The last function to write is Resolve. This is the method that will actually perform the collision checks. It’s quite a big function as it needs to do a few checks (mostly to minimise the number of collision checks we need to perform each frame) but it is mostly straightforward and brings together everything we’ve written so far.
- Created a collider component. This component holds a rectangular area that we will use to check for collisions. We need to attach this component to every object that we want to collide.
- Created the quadtree. This is a method of efficiently storing collidables to minimise the number of collision checks we need to perform.
Object.hpp
template <typename T> std::shared_ptr<T> GetComponent()
{
// Removed check to see if we are trying to get a class that derives from component.
for (auto& exisitingComponent : components)
{
if (std::dynamic_pointer_cast<T>(exisitingComponent))
{
return std::dynamic_pointer_cast<T>(exisitingComponent);
}
}
return nullptr;
};
Object.hpp
template <typename T> std::shared_ptr<T> GetComponent()
{
// Removed check to see if we are trying to get a class that
// derives from component.
for (auto& exisitingComponent : components)
{
if (std::dynamic_pointer_cast<T>(exisitingComponent))
{
return std::dynamic_pointer_cast<T>(exisitingComponent);
}
}
return nullptr;
};
- To be able to pass the collision system a vector of newly added objects and for the collision system to work out which objects have collidables and only include those in its system.
- When an object is removed from the game we need the collision system to remove it from its collection as well.
- Every frame we need to check if objects are colliding and if they are we want the objects collider components to resolve the collision.
S_Collidable.hpp
#ifndef S_Collidable_hpp
#define S_Collidable_hpp
#include <vector>
#include <memory>
#include <set>
#include "Object.hpp"
#include "Quadtree.hpp"
#include "Bitmask.hpp"
class S_Collidable
{
public:
S_Collidable();
void Add(std::vector<std::shared_ptr<Object>>& objects);
void ProcessRemovals();
void Update();
private:
void Resolve();
void ProcessCollisions(std::vector<std::shared_ptr<Object>>& first, std::vector<std::shared_ptr<Object>>& second);
// This is used to store collision layer data i.e. which layers can collide.
std::map<CollisionLayer, Bitmask> collisionLayers;
// The collision system stores all collidables along with their layer.
std::map<CollisionLayer, std::vector<std::shared_ptr<C_BoxCollider>>> collidables;
// The quadtree stores the collidables in a spatial aware structure.
Quadtree collisionTree;
};
#endif /* S_Collidable_hpp */
S_Collidable.cpp
#include "S_Collidable.hpp"
S_Collidable::S_Collidable()
{
Bitmask defaultCollisions; // 1
defaultCollisions.SetBit((int)CollisionLayer::Default); // 2
collisionLayers.insert(
std::make_pair(CollisionLayer::Default, defaultCollisions)); // 3
collisionLayers.insert(std::make_pair(CollisionLayer::Tile, Bitmask(0)));
Bitmask playerCollisions;
playerCollisions.SetBit((int) CollisionLayer::Default);
playerCollisions.SetBit((int) CollisionLayer::Tile);
collisionLayers.insert(std::make_pair(CollisionLayer::Player, playerCollisions));
}
- Create a bitmask to store the collision data.
- We set the bit for each layer that this layer will collide with. Remember that we specified integer values for our CollisionLayer enum. We use those values to set that position in the bitmask if we want collisions with that layer. For example, we only want the default layer colliding with itself so we only set the bit at position 1 (the integer value we gave to the default layer in the CollisionLayer enum). We don’t want the tile layer colliding with any other layer so we set its bit mask to 0. And we want the player layer colliding with the default and tile layer so we set both of those bits.
- We then add this value to our collisionLayer map. We’ll read from this map when we are resolving collisions.
S_Collidable.cpp
void S_Collidable::Add(std::vector<std::shared_ptr<Object>>& objects)
{
for (auto o : objects)
{
auto collider = o->GetComponent<C_BoxCollider>();
if (collider)
{
CollisionLayer layer = collider->GetLayer();
auto itr = collidables.find(layer);
if (itr != collidables.end())
{
collidables[layer].push_back(collider);
}
else
{
std::vector<std::shared_ptr<C_BoxCollider>> objs;
objs.push_back(collider);
collidables.insert(std::make_pair(layer, objs));
}
}
}
}
S_Collidable.cpp
void S_Collidable::ProcessRemovals()
{
for (auto& layer : collidables)
{
auto itr = layer.second.begin();
while (itr != layer.second.end())
{
if ((*itr)->owner->IsQueuedForRemoval())
{
itr = layer.second.erase(itr);
}
else
{
++itr;
}
}
}
}
S_Collidable.cpp
void S_Collidable::Update()
{
collisionTree.Clear();
for (auto maps = collidables.begin();
maps != collidables.end(); ++maps)
{
for (auto collidable : maps->second)
{
collisionTree.Insert(collidable);
}
}
Resolve();
}
S_Collidable.cpp
void S_Collidable::Resolve()
{
for (auto maps = collidables.begin(); maps != collidables.end(); ++maps) // 1
{
// If this layer collides with nothing then no need to
// perform any further checks.
if(collisionLayers[maps->first].GetMask() == 0)
{
continue;
}
for (auto collidable : maps->second) // 1
{
// If this collidable is static then no need to check if
// it's colliding with other objects.
if (collidable->owner->transform->isStatic())
{
continue;
}
std::vector<std::shared_ptr<C_BoxCollider>> collisions
= collisionTree.Search(collidable->GetCollidable()); // 2
for (auto collision : collisions) // 3
{
// Make sure we do not resolve collisions between the same object.
if (collidable->owner->instanceID->Get()
== collision->owner->instanceID->Get())
{
continue;
}
bool layersCollide =
collisionLayers[collidable->GetLayer()].GetBit(((int)collision->GetLayer()));
if(layersCollide) // 3a
{
Manifold m = collidable->Intersects(collision); // 3b
if(m.colliding)
{
if(collision->owner->transform->isStatic())
{
collidable->ResolveOverlap(m); // 3c
}
else
{
//TODO: How should we handle collisions when both
// objects are not static?
// We could implement rigidbodies and mass.
collidable->ResolveOverlap(m);
}
}
}
}
}
}
}
- We loop over every collidable stored within this system.
- We pass the collidables area to the quadtree and retrieve a vector of objects that intersect with that area.
- For every object we are colliding with:
- We check if the two layers collide. If not then we can skip resolution.
- The collision is passed to the collidable component of the initial object. This performs an additional collision check and returns a manifold that contains the collision data. We need to perform another check here because as we resolve collisions and move objects around we may no longer be colliding with some objects in the vector.
- If they are still colliding we pass the manifold to the collidable component of the initial object, which will resolve the collision by moving the object so it is no longer intersecting.
- If a collision layer does not collide with any other layer (such as the tile layer) we do not perform checks on that layer.
- If an object is static we do not need to check if it is colliding with other objects. We’ll still check if other objects collide with the static object. For example, if the player walks up to a chest that is marked static, they can still collide/interact with the chest. We just don’t need to check every frame if the chest is colliding with anything else.
- If a layer does not collide with the layer of the other object based on its entry in collisionLayers then we do not perform any collision checks/resolutions.
- And of course, by using the quadtree we only retrieve a small subset of objects so we do not need to check if every object is colliding with every other object.