Last week we wrote the code that will enable communication between objects when they collide. This communication can happen at three stages of a collision: when the collision occurs, every frame the collision is maintained, and when the collision ends. This week we are going to write a new component for our projectile that will remove it from the game when it collides with another object (so when collision first happens). It will be a simple component but will hopefully show you how to respond to collision events.

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.

Last week we wrote the code that will enable communication between objects when they collide. This communication can happen at three stages of a collision: when the collision occurs, every frame the collision is maintained, and when the collision ends. This week we are going to write a new component for our projectile that will remove it from the game when it collides with another object (so when collision first happens). It will be a simple component but will hopefully show you how to respond to collision events.Create a new component called C_RemoveObjectOnCollisionEnter. It’s a wordy title but it explains exactly what this class will do.

C_RemoveObjectOnCollisionEnter.hpp
#ifndef C_RemoveObjectOnCollisionEnter_hpp
#define C_RemoveObjectOnCollisionEnter_hpp

#include "Component.hpp"
#include "C_Collidable.hpp"

class C_RemoveObjectOnCollisionEnter : public Component, public C_Collidable
{
public:
C_RemoveObjectOnCollisionEnter(Object* owner);

void OnCollisionEnter(std::shared_ptr<C_BoxCollider> other) override;
};

#endif /* C_RemoveObjectOnCollisionEnter_hpp */

The class inherits from Component but it also is derived from C_Collidable, which is the system we were working on last week. We override OnCollisionEnter as we’ll use that function to remove the projectile at the beginning of a collision.For now, we’ll queue the object for removal as soon as it collides with another object. In future we could check the objects tag name so, for example, we only remove the object when it collides with other objects with the tag “Tile”. We will also want to use some form of object pooling but that’s a topic for a future tutorial.

C_RemoveObjectOnCollisionEnter.cpp
#include "C_RemoveObjectOnCollisionEnter.hpp"
#include "Object.hpp"

C_RemoveObjectOnCollisionEnter::C_RemoveObjectOnCollisionEnter(Object* owner) : Component(owner) {}

void C_RemoveObjectOnCollisionEnter::OnCollisionEnter(std::shared_ptr<C_BoxCollider> other)
{
// Remove the projectile when it collides with any other object
owner->QueueForRemoval();
}

Attach the component to newly spawned projectiles in C_ProjectileAttack.

C_ProjectileAttack.hpp
#include "C_RemoveObjectOnCollisionEnter.hpp"

C_ProjectileAttack.cpp
void C_ProjectileAttack::SpawnProjectile()
{


projectile->AddComponent<C_RemoveObjectOnCollisionEnter>();
owner->context->objects->Add(projectile);
}

If you run the game and fire off a few projectiles you’ll quickly run into a problem. As soon as the projectiles collide with an object we get a “bad access” exception.


Bad Access Exception
Bad Access Exception.

You’ll receive this exception if you attempt to use a pointer that points to memory that has been deallocated. It’s not difficult to imagine what’s happening in our code. We’ve removed the projectile object from the game, and its memory has been automatically deallocated for us as we are using smart pointers; however something (in this case the collision system), still thinks it holds a valid pointer and is trying to call GetCollidable on the removed projectile.


Call Stack on Main Thread.
Call Stack on Main Thread.

To start debugging the issue we should start where the projectile is supposedly removed from the collision systems collection. We can check if the code to remove the projectile is being called by setting a breakpoint.


Setting a breakpoint for removing the projectile from the collision system.
Setting a breakpoint for removing the projectile from the collision system.

If we then set a breakpoint just after we clear the collision tree in the Update function or at the end of the ProcessRemovals function and inspect the contents of collidables you’ll notice something strange: it still contains the pointer to the projectile that we have supposedly removed.


Inspecting contents of collidables.

Inspecting contents of collidables.If we look closely at the breakpoint navigator and step over the debug point we created (when we remove the drawable component) you’ll notice that the component is removed from the ‘layer’ (the name we gave each collidable layer as we iterate over them) however it is not removed from the collidables collection.


Projectile removed from layer but not collision collection.
Projectile removed from layer but not collision collection.

This is because as we iterate over the layers in collidables:

SceneGame.cpp
void SceneGame::OnCreate()
{

//std::shared_ptr<Object> player = std::make_shared<Object>();
std::shared_ptr<Object> player = std::make_shared<Object>(&context);


}

We are actually creating a copy of the layer and then removing the collidables from that copy. This is not the intended behaviour and luckily is easy to fix, all we need to do is retrieve a reference to the layer:

for (auto& layer : collidables) {…}

So the full function now looks like this:

S_Collidable.cpp
void S_Collidable::ProcessRemovals()
{
//We access the layer by reference now.
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;
}
}
}
}

And that fixes the bad access exception. Now objects are successfully removed from the collision system and we can fire projectiles as much as we want!


Projectiles are now removed on contact.

Now would be a good time to perform an audit on our other range-based for loops. As a guideline we want to use:

  • auto: when we want to make a copy of the original.
  • auto*: when we want to read and modify the original and be able to change what the pointer references.
  • const auto&: when we want to read and modify a property of the original but do not want to be able to change what the pointer references.
  • const shared_ptr&: when we want a read-only copy of the original.

It should also be noted that a pointer and the thing it points to are two separate objects. Either, none or both may be const and a const pointer simply means that you cannot change where the pointer points. 

// Create pointer to const int
const int* constPointer = new int(1);
(*constPointer) = 5; // not allowed
constPointer = new int(2); // allowed

// Create const pointer to int
int* const constInt = new int(2);
(*constInt) = 5; // allowed
constInt = new int(2); // not allowed

// Create const pointer to const int
const int* const constPointerInt = new int(3);
(*constPointerInt) = 5; // not allowed
constPointerInt = new int(2); // not allowed

This doesn’t affect us much as we generally use references and smart pointers but its good to be aware of.The first change we’ll make is to TilemapParser. As we loop over each layer in the map we are creating unnecessary copies.

TilemapParser.cpp
std::vector<std::shared_ptr<Object>> TileMapParser::Parse(const std::string& file, sf::Vector2i offset)
{


int tileSizeX = std::atoi(rootNode->first_attribute("tilewidth")->value());
int tileSizeY = std::atoi(rootNode->first_attribute("tileheight")->value());

// No longer need to retrieve map size as we don't currently use it. Remove these two lines:
//int mapsizeX = std::atoi(rootNode->first_attribute("width")->value());
//int mapsizeY = std::atoi(rootNode->first_attribute("height")->value());

std::vector<std::shared_ptr<Object>> tileObjects;

// We won’t have a negative layer count so make it unsigned:
unsigned int layerCount = (unsigned int)map->size() - 1;

// Retrieve layer as a reference.
for (const auto& layer : *map)
{
// Retrieve tile as a reference.
for (const auto& tile : layer.second->tiles) {...}
}

...
}

While we’re in this function I’ve also taken the liberty to make a couple of other changes: I’ve removed the references to map size as we didn’t use them and I’ve also changed the type of the layer count to an unsigned int as we don’t want/expect it to be a negative number.The last change we make to the for loops (at least for now) is to change how we loop over the components in the Object class. We want to retrieve them all by const reference.

Object.cpp
void Object::Awake()
{
for (const auto& component : components)
{
component->Awake();
}
}

void Object::Start()
{
for (const auto& component : components)
{
component->Start();
}
}

void Object::Update(float timeDelta)
{
for (const auto& component : components)
{
component->Update(timeDelta);
}
}

void Object::LateUpdate(float timeDelta)
{
for (const auto& component : components)
{
component->LateUpdate(timeDelta);
}
}

There are a couple of small house tidying tasks I want to do before we finish for the week. First, in our collision system we add objects to the quadtree as they are added to a collision layer, however each frame in the collision systems Update function we remove all collidables from the tree and then re-add them (we do this to partition the objects based on their new position). So we don’t need to add objects individually to the tree.

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
{
// Refractored line below.
collidables.insert(std::make_pair(layer, std::vector<std::shared_ptr<C_BoxCollider>>{collider}));
}

// Remove line below.
// collisionTree.Insert(collider);
}
}
}

I also refactored how we insert a new collidable pair. We no longer create a vector on a separate line but use an initialisation list.In C_Projectile::SpawnProjectile we retrieve the current facing direction twice. So we’ll remove one of them:

C_ProjectileAttack.cpp
void C_ProjectileAttack::SpawnProjectile()
{
FacingDirection faceDir = direction->Get();

std::shared_ptr<Object> projectile = std::make_shared<Object>(owner->context);

projectile->transform->SetPosition(owner->transform->GetPosition() +
offsetDirectionBindings.at(faceDir));

// Remove line below.
//FacingDirection currentDir = direction->Get();


}

And last change for the week, in TileMapParser::BuildLayer we retrieve the height value from the levels data file and don’t use it, so we’ll remove that line.

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>();

int width = std::atoi(layerNode->first_attribute("width")->value());

// Remove line below.
//int height = std::atoi(layerNode->first_attribute("height")->value());


}

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 🙂