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.
This week we’ll be starting our object collision system. As this is a big topic it will be split into a number of tutorials over a few weeks.
Let’s start with an overview of how we would like our collision system to function:
  • First, we need some way to let our engine know which objects we want colliding. To do this we’ll use our component system by creating a new component called a collider component. The collider component will store the collision shape, this could be a rectangle, circle, polygons or any other shape you can think of. At first we’ll create a rectangle collider component (as this is one of the more straightforward methods of collision) but eventually, we’ll also add different types of collider shapes. We can query the collider component to check if it is colliding with another component on a different object, and it will also be responsible for resolving the collisions.
  • Once we’ve created new objects and added a collider component to them, we’ll then send them to our object collection so they are updated and drawn (as we currently do with all objects). The object collection will then pass the objects to a separate collision system. This pass-through of objects will work similarly to how it currently does with our drawing system. The collision system will store the colliders in a special structure called a quadtree. I’ll explain the why and how of quadtrees in next weeks tutorial but for now its enough to know that we do this to reduce the number of collision checks we need to perform. This is accomplished by storing our collidables in a space-aware data structure and then we only need to check if an object is colliding with entities in the same or adjacent areas. Again more on this next week.
  • Each frame our collision system will check if any objects with a collider component are colliding. If two are found to be colliding then the collision will need to be resolved so that they are no longer colliding. As we are only using box colliders to start with, we will only need to resolve collisions between rectangles, which is relatively straightforward.
  • When two objects collide we want to be able to inform any interested components. For example, if a thrown weapon collides with an enemies collider we need some way of informing the enemy of this so it can reduce its health accordingly. We’ll most likely implement this when we start work on collision damage.
  • We should have the ability to mark an object as static, which means that we will not resolve its collisions. This objects will stay ‘static’ in the scene and will not move even when an object collides with it. This will help to reduce the number of collision checks as we do not need to check if two static objects are colliding. We’ll mark all of the tiles in our level as static as we do not want them moving (unless we specifically implement moving platforms) and we do not want to have to check if every tile is colliding with every other tile.
  • Every collidable will be on a specific layer. We’ll start with Default, Player, and Tile layers but will expand the number of layers as necessary. We will be able to set which layers collide. For example, we can set the Player layer to collide with every other layer and the Tile layer to collide with no other layers. This will further reduce the number of collision checks we need to perform.
This sounds like quite a bit of work but we’ll slowly build up our collision system week by week and before we know it we’ll have implemented all of these features and be discovering new things that we want our collision system to do. This week we’ll start with the first bullet point above and write the collider components. However, before we do that we’re going to make one quick change to our TileMapParser. The last few weeks we’ve been working on the ability to draw multiple layers. Which is great but as a side effect we are also drawing our collision layer (the red tiles), which is not so great. So we need a method of being able to toggle a layer’s visibility and only draw the layers we want to.
In Tiled you can set which layer is drawn by toggling the eye image next to the layer name. When you set a layer to be invisible a new attribute for that layer appears in the TMX file with the name ‘visible’. We’ll use that to decide if we should draw a layer.
Hiding Layers in Tiled
Hiding Layers in Tiled.
There are only a few changes we need to make. In TileMapParser change:
TileMapParser.hpp
using Layer = std::vector<std::shared_ptr<Tile>>;
to:
TileMapParser.hpp
struct Layer
{
    std::vector<std::shared_ptr<Tile>> tiles;
    bool isVisible;
};
We now store whether a layer is visible along with its tiles. To integrate these changes we need to edit the BuildLayer function.
TileMapParser.cpp
std::pair<std::string, std::shared_ptr<Layer>> 
TileMapParser::BuildLayer(xml_node<>* layerNode, 
						  std::shared_ptr<TileSheets> tileSheets)
{
    TileSet tileSet;
    std::shared_ptr<Layer> layer = std::make_shared<Layer>();
    
    …   std::string line;
    while (fileStream.good())
    {
      …
			// We add the tile differently.
            layer->tiles.emplace_back(tile);  // 1
        }
        
        count++;
    }
    
    const std::string layerName = layerNode->first_attribute("name")->value();
    
	// We now check if a visible attribute is present and use that. 
	// If no attribute present, layer defaults to visible.
    bool layerVisible = true;
    xml_attribute<>* visibleAttribute = layerNode->first_attribute("visible");
    if(visibleAttribute) // 2
    {
        layerVisible = std::stoi(visibleAttribute->value());
    }
    layer->isVisible = layerVisible;
    
    return std::make_pair(layerName, layer);
}
  1. Make sure you change the way tiles are added to our Layer data structure.
  2. We default a layer’s visibility to true. If we do not find the visible attribute it is because the layer is visible in Tiled.
With those changes made we need to check this flag before we add a sprite component to the tiles.
TileMapParser.cpp
std::vector<std::shared_ptr<Object>> 
TileMapParser::Parse(const std::string& file, sf::Vector2i offset)
{
…
    rapidxml::xml_document<> doc;
    doc.parse<0>(xmlFile.data());
    xml_node<>* rootNode = doc.first_node("map");
    
	// Renamed from tiles to map.
    std::shared_ptr<MapTiles> map = BuildMapTiles(rootNode);
    
…
    for (const auto layer : *map)
    {
        for (const auto tile : layer.second->tiles)
        {

			// Checks if layer is visible before adding sprite component.            
            if(layer.second->isVisible)
            {
                auto sprite = tileObject->AddComponent<C_Sprite>();
                sprite->SetTextureAllocator(&textureAllocator);
                sprite->Load(tileInfo->textureID);
                sprite->SetTextureRect(tileInfo->textureRect);
                sprite->SetScale(tileScale, tileScale);
                sprite->SetSortOrder(layerCount);
            }
          …
        }
        
        layerCount--;
    }
    
    return tileObjects;
}
 
If you run the game now, you should no longer see the collision layer. If you do still see it, make sure it is hidden with Tiled and then restart the game.
We're no longer drawing our collision layer
We’re no longer drawing our collision layer.
Now with that diversion over we can start writing our collision components. We’ll start with C_Collider. This will be the base component for all of our collider components.
C_Collider.hpp
#ifndef C_Collider_hpp
#define C_Collider_hpp

#include<memory>
#include <SFML/Graphics.hpp>

#include "Component.hpp"

// enum ints used when defining collision layers
enum class CollisionLayer
{
    Default = 1,    // bit 0
    Player = 2,     // bit 1
    Tile = 3        // bit 2
};

struct Manifold
{
    bool colliding = false;
    const sf::FloatRect* other;
};

#endif /* C_Collider_hpp */
I’ve added a CollisionLayer enum. Every collider will exist on a specific layer and we will be able to set which layers collide. You don’t need to worry about the comment regarding ints and bitmasks but it does provide a hint on how we will set up our layers in our collision system in a later tutorial.
The manifold will store the data for an intersection. Right now it only stores a rectangle and a flag signifying if the object is colliding with that rectangle. But in future when we implement circle colliders we’ll have to create something a bit more clever.
C_Collider.cpp
class C_Collider : public Component
{
public:
    C_Collider(Object* owner);
    ~C_Collider();
    
    virtual Manifold Intersects(std::shared_ptr<C_Collider> other) = 0;
    virtual void ResolveOverlap(const Manifold& m) = 0;
    
    CollisionLayer GetLayer() const;
    void SetLayer(CollisionLayer layer);
    
private:
    CollisionLayer layer;
};
There are two main functions of every collider component: Intersects and ResolveOverlap. Intersects returns the collision manifold and ResolveOverlap accepts a manifold as a parameter. It will make more sense when we come to developing the collision system why I’ve chosen to implement it this way.
There are also a set of accessors/mutators for the CollisionLayer of the collider. As the other functions are pure virtual we only need to implement these two functions.
C_Collider.cpp
#include "C_Collider.hpp"

C_Collider::C_Collider(Object* owner) 
	: Component(owner), layer(CollisionLayer::Default){}

C_Collider::~C_Collider(){}

CollisionLayer C_Collider::GetLayer() const
{
    return layer;
}

void C_Collider::SetLayer(CollisionLayer layer)
{
    this->layer = layer;
}
Next up is the box collider component which will inherit from the collider component.
C_BoxCollider.hpp
#ifndef C_BoxCollider_hpp
#define C_BoxCollider_hpp

#include "Component.hpp"
#include "C_Collider.hpp"

class C_BoxCollider : public C_Collider
{
public:
    C_BoxCollider(Object* owner);
    
    Manifold Intersects(std::shared_ptr<C_Collider> other) override;
    void ResolveOverlap(const Manifold& m) override;
    
    void SetCollidable(const sf::FloatRect& rect);
    const sf::FloatRect& GetCollidable();
    
private:
    void SetPosition();
    
    sf::FloatRect AABB;
    sf::Vector2f offset;
};

#endif /* C_BoxCollider_hpp */
It overrides the Intersects and ResolveOverlap function from our C_Collider component and adds a few functions that are used to access the colliders collidable. The collidable component is the rectangle that we will perform the collision checks on. We can set the rects area to any size we would like and this will differ based on the object to which the component is attached. For example, we would want the rect on our tiles to be the same size as the tile but we can make the rect on our player slightly smaller.
Our player and a tile colliding.
A possible player and tile collision rects.
C_BoxCollider.cpp
#include "C_BoxCollider.hpp"
#include "Object.hpp"

C_BoxCollider::C_BoxCollider(Object* owner) : C_Collider(owner),
offset(sf::Vector2f(0.f, 0.f)){}

void C_BoxCollider::SetCollidable(const sf::FloatRect& rect)
{
    AABB = rect;
    SetPosition();
}

const sf::FloatRect& C_BoxCollider::GetCollidable()
{
    SetPosition();
    return AABB;
}
Before we get and set the collidable we need to update its position based on the object’s position. We get the position using the transform component.
C_BoxCollider.cpp
void C_BoxCollider::SetPosition()
{
    const sf::Vector2f& pos = owner->transform->GetPosition();
    
    AABB.left = pos.x - (AABB.width / 2) + offset.x;
    AABB.top = pos.y - (AABB.height / 2) + offset.y;
}
We’ll write the Intersects function next. This is responsible for checking if this component is colliding with another.
C_BoxCollider.cpp
Manifold C_BoxCollider::Intersects(std::shared_ptr<C_Collider> other)
{
    Manifold m;
    m.colliding = false; // We default to not colliding.
    
    std::shared_ptr<C_BoxCollider> boxCollider 
		= std::dynamic_pointer_cast<C_BoxCollider>(other); // 1
	
    if (boxCollider) // 2
    {
        const sf::FloatRect& rect1 = GetCollidable();
        const sf::FloatRect& rect2 = boxCollider->GetCollidable();
        
        if (rect1.intersects(rect2)) // 3
        {
            m.colliding = true; // 4
            m.other = &rect2;
        }
    }
    
    return m;
}
  1. We attempt to cast the other collider to a box collider. I’m doing this at the moment because we only have box colliders. Once we start introducing different types of colliders we will have to re-jig this function.
  2. If the collider was not able to be cast to a box collider it returns a nulptr.
  3. We use a SFML function that checks if the two rects intersect. If you’re interested in knowing how to write your own function that checks if two rectangles intersect (it is very straightforward), I would recommend the demonstration here.
  4. If the rects are colliding we build the manifest file and return it. 
Last up is the ResolveOverlap function. This function is called when we know this component is colliding with the rect in the manifold and we want to resolve the collision by pushing the object outside the bounds of the rectangle.
C_BoxCollider.cpp
void C_BoxCollider::ResolveOverlap(const Manifold& m)
{
    auto transform = owner->transform;
    
    if(transform->isStatic()) { return; } // 1
    
    const sf::FloatRect& rect1 = GetCollidable();
    const sf::FloatRect* rect2 = m.other;
    
    float resolve = 0; // 2
    float xDiff = (rect1.left + (rect1.width * 0.5f)) 
		- (rect2->left + (rect2->width * 0.5f)); // 3
    float yDiff = (rect1.top + (rect1.height * 0.5f))
		- (rect2->top + (rect2->height * 0.5f));
    
    if (fabs(xDiff) > fabs(yDiff)) // 4
    {
        if (xDiff > 0) // Colliding on the left.
        {
			// We add a positive x value to move the object to the right.
            resolve = (rect2->left + rect2->width) - rect1.left; 
        }
        else // Colliding on the right.
        {
			// We add a negative x value to move the object to the left.
            resolve = -((rect1.left + rect1.width) - rect2->left);
        }
        
        transform->AddPosition(resolve, 0); // 5
    }
    else
    {
        if (yDiff > 0) // Colliding above.
        {
			// We add a positive y value to move the object down.
            resolve = (rect2->top + rect2->height) - rect1.top;
        }
        else // Colliding below
        {
			// We add a negative y value to move the object up.
            resolve = -((rect1.top + rect1.height) - rect2->top);
        }
        
        transform->AddPosition(0, resolve); // 5
    }
}
  1. We don’t want to move the object if we’ve set it as static. We’ll write this function in our transform component shortly.
  2. We’ll use this to store the distance we’ll have to move the object to resolve the collision.
  3. We store the difference between the two collidables centre points. By starting at the collidables left point and then adding half the width we get the centre x value. And it is a similar process for the y value: starting at the top and adding half the height value.
  4. We compare the x and y differences to check which is greater and then resolve on that axis. Once we’ve worked out what edge the colliders are colliding on we know which way we have to move the object so they are no longer colliding. 
  5. We add the resolve to our transforms position.

The last thing we’ll do this week is create the static flag in our transform component.
C_Transform.hpp
class C_Transform : public Component
{
…
    
    void SetStatic(bool isStatic);
    bool isStatic() const;

private:
…
    bool isStaticTransform;
};
C_Transform.cpp
C_Transform::C_Transform(Object* owner) 
	: Component(owner), position(0.f, 0.f), isStaticTransform(false) { }

void C_Transform::SetStatic(bool isStatic) { isStaticTransform = isStatic; }

bool C_Transform::isStatic() const { return isStaticTransform; }
And that’s it for this week. In the next tutorial we’ll write the data structure we’ll use to store and sort all of our collidable components: the quadtree.
As always, if you have any suggestions for what you would like covered or are having any trouble implementing a feature, then let me know in the comments and I’ll get back to you as soon as I can. Thank you for reading 🙂