This week we will start work on a collision communication system. Currently, when two objects collide the only action they can take is to prevent the collision by moving apart, however what if we want more complicated behaviour? Such as causing damage to an enemy when the projectile collides with them or making the arrows disappear when they collide with a tile.

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 will start work on a collision communication system. Currently, when two objects collide the only action they can take is to prevent the collision by moving apart, however what if we want more complicated behaviour? Such as causing damage to an enemy when the projectile collides with them or making the arrows disappear when they collide with a tile. When we first discussed the collision system way back here, we summarised the expected functionality:

  1. ‘We need some way to let our engine know which objects we want to collide”. We accomplished this by creating the C_BoxCollider component, which we can attach to any object we want to collide.
  2. “The objects will be passed to a structure called a quadtree”. Yep done that as well.
  3. “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”. Our collision system takes care of that.
  4. “We should have the ability to mark an object as static, which means that we will not resolve its collisions”. This has also been implemented. We use it to reduce the number of collision checks.
  5. “Every collidable will be on a specific layer. We will be able to set which layers collide”. We have our layer system setup. Another useful tool used to reduce the number of collision checks we need to perform.
  6. “When two objects collide we want to be able to inform any interested components”. This is yet to be completed and is what I want to work on this week.

So how are we going to accomplish this? As with any programming task, there are loads of ways we can go about it. However, I want to take advantage of our component system. First, we’ll create a new component that will be the base class for any component that wants to be notified of a collision change. This component will have three virtual functions: OnCollisionEnter, OnCollisionStay, and OnCollisionExit. The names of the functions may give you a clue on when the functions will be called:

  • OnCollisionEnter will be called when two objects first collide.
  • OnCollisionStay will be called every frame that the objects stay colliding.
  • OnCollisionExit is called when two objects stop colliding by moving apart or when either of the objects has been queued for removal.

When a new component is added to an object, the object will check if the component derives from this base class and if it does it stores it in a separate collection. The object class will also have an OnCollisionEnter, Stay, and Exit functions. These will call the corresponding functions in any collidable components added to the object. We will call these functions on the objects rather than interacting with the components directly.

The enter, stay, and exit functions will accept a shared_ptr to a C_BoxCollider component. This is the collider component of the object that is being collided with. We can use this to do many things such as: check the object that collided with us is an arrow and if it is we can reduce its health, or from the arrows point of view we can check if the object we collided with is part of the environment and if it is the arrow can queue itself for removal. These are only two very arrow-specific examples but there are many different ways we can use the data as will become evident as the project progresses.

That’s a lot of theory but it will hopefully make sense as we write the code. This week we will create the base collidable component, add the data structure and method of storing collidable components in the Object class, and write the collision functions for the Object class. Next week we’ll write the code that calls these functions when two objects collide.

Start by creating a new class called C_Collidable. This will be the base component for any class that wants to be notified when a collision occurs.

C_Collidable.hpp
#ifndef C_Collidable_hpp
#define C_Collidable_hpp

#include <memory>

#include "C_BoxCollider.hpp"

class C_Collidable
{
public:
virtual void OnCollisionEnter(std::shared_ptr<C_BoxCollider> other) {};
virtual void OnCollisionStay(std::shared_ptr<C_BoxCollider> other) {};
virtual void OnCollisionExit(std::shared_ptr<C_BoxCollider> other) {};
};

#endif /* C_Collidable_hpp */

The class has the three functions that I described earlier. This is a pure virtual class and will not be instantiated. As all the functions are virtual we do not need an implementation file.Now we have this parent class we can modify our Object class to store these components in a separate collection. We do this so that when a collision does occur we can call the relevant components quickly.

Object.hpp

#include "C_Collidable.hpp"

class Object
{
public:


template <typename T> std::shared_ptr<T> AddComponent()
{
static_assert(std::is_base_of<Component, T>::value, "T must derive from Component");

// Check that we don't already have a component of this type.
for (auto& exisitingComponent : components)
{
if (std::dynamic_pointer_cast<T>(exisitingComponent))
{
return std::dynamic_pointer_cast<T>(exisitingComponent);
}
}

std::shared_ptr<T> newComponent = std::make_shared<T>(this);

components.push_back(newComponent);

if (std::dynamic_pointer_cast<C_Drawable>(newComponent))
{
drawable = std::dynamic_pointer_cast<C_Drawable>(newComponent);
}

// Check if the component inherits from C_Collidable. If it does store it in a separate vector.
if (std::dynamic_pointer_cast<C_Collidable>(newComponent))
{
collidables.push_back(std::dynamic_pointer_cast<C_Collidable>(newComponent));
}

return newComponent;
};
private:

std::vector<std::shared_ptr<C_Collidable>> collidables;

};

Checking if a component inherits from C_Collidable is the same process as checking if it is a drawable component. If it does inherit from our collidable component, we add it to a new vector called collidables. Whenever there is a collision involving this object we will iterate over this vector and call the corresponding collision function. Let’s write the functions that will do that now.

Object.hpp
class Object
{
public:


void OnCollisionEnter(std::shared_ptr<C_BoxCollider> other);
void OnCollisionStay(std::shared_ptr<C_BoxCollider> other);
void OnCollisionExit(std::shared_ptr<C_BoxCollider> other);


};
Object.hpp
void Object::OnCollisionEnter(std::shared_ptr<C_BoxCollider> other)
{
for (const auto& component : collidables)
{
component->OnCollisionEnter(other);
}
}

void Object::OnCollisionStay(std::shared_ptr<C_BoxCollider> other)
{
for (const auto& component : collidables)
{
component->OnCollisionStay(other);
}
}

void Object::OnCollisionExit(std::shared_ptr<C_BoxCollider> other)
{
for (const auto& component : collidables)
{
component->OnCollisionExit(other);
}
}

These functions will be called by our collision system, which we will modify next week.
One last thing to do this week is to change how we store the layers and collidables in S_Collidable. Currently we are using a map, however, I want to swap to using unordered_maps as we don’t need them in any specific order. 

S_Collidable.hpp

#include "EnumClassHash.hpp"

class S_Collidable
{


private:

void

std::unordered_map<CollisionLayer, Bitmask, EnumClassHash> collisionLayers;
std::unordered_map<CollisionLayer, std::vector<std::shared_ptr<C_BoxCollider>>, EnumClassHash> collidables;


};

That’s all for this week! Next week we’ll finish what we started here and edit the collision system to call the OnCollisionEnter, OnCollisionStay, and OnCollisionExit functions.

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 🙂