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.
Four weeks ago we started work on our collision system and in that time we’ve done a lot: we created collider components, the data structure to store the components, and the collision system to check for and resolve any collisions. All that’s left is to add the collider components to the tiles and player object and call the new collision system that we wrote last week. Which means that by the end of this tutorial we should finally have collisions working!
We’ll jump right in by implementing the collision system. We need to call three functions: Add, ProcessRemovals, and Resolve functions. We’ll call these in the ObjectCollection.
…
#include "S_Collidable.hpp"
class ObjectCollection
{
…
private:
…
S_Collidable collidables;
};
We’ll call the collidable systems Update in the ObjectCollections Update function, right after we update the objects.
void ObjectCollection::Update(float deltaTime)
{
for(const auto& o : objects)
{
o->Update(deltaTime);
}
collidables.Update();
}
Whenever we add an object to the object collection we’ll also want to add these objects to the collidables system. This can be done in the ProcessNewObjects function. This is the exact same process as our drawing system.
void ObjectCollection::ProcessNewObjects()
{
if (newObjects.size() > 0)
{
…
collidables.Add(newObjects);
}
}
The last thing we need to do before our collision system is up and running is to process the removals. You’ve probably guessed already but we’ll do this in the ProcessRemovals function.
void ObjectCollection::ProcessRemovals()
{
bool removed = false;
auto objIterator = objects.begin();
while (objIterator != objects.end())
{
auto obj = *objIterator;
if (obj->IsQueuedForRemoval())
{
objIterator = objects.erase(objIterator);
removed = true;
}
else
{
++objIterator;
}
}
if(removed)
{
drawables.ProcessRemovals();
collidables.ProcessRemovals();
}
}
I’ve also added a check to see if objects have been removed from the object collection before we call the systems ProcessRemovals. This prevents unnecessary loops through the systems vectors checking for removals when there haven’t been any.Now our collision system is up and running but it has nothing to process. So the last step we need to do is add collidable components to anything that we want to collide, for now, that’s only the tiles and the player. I’ll start with the tiles by adding the component in the Parse function of TileMapParser.
#include "C_BoxCollider.hpp"
std::vector<std::shared_ptr<Object>>
TileMapParser::Parse(const std::string& file, sf::Vector2i offset)
{
…
for (const auto layer : *map)
{
for (const auto tile : layer.second->tiles)
{
…
// We only add colliders to the one layer.
if (layer.first == "Collisions")
{
auto collider = tileObject->AddComponent<C_BoxCollider>();
float left = x - (tileSizeX * tileScale) * 0.5f;
float top = y - (tileSizeY * tileScale) * 0.5f;
float width = tileSizeX * tileScale;
float height = tileSizeY * tileScale;
collider->SetCollidable(sf::FloatRect(left, top, width, height));
collider->SetLayer(CollisionLayer::Tile);
}
tileObjects.emplace_back(tileObject);
}
}
return tileObjects;
}
We first check if the layer is named “Collisions”, so make sure the layer containing the collision tiles has this name in Tiled. For more information on layers and Tiled see my earlier tutorial. Doing it this way allows us more control over what tiles we collide with, and reduces the collision calls drastically than if we were to collide with every tile.
Once we know the tile is on the collision layer we add the collision component and calculate the size of the collider based on the size of the tile and set the collision layer to Tile. Lastly we need to add the box collider to the player. We’ll do this after we create the player object in SceneGame.
#include "C_BoxCollider.hpp"
SceneGame.cpp
void SceneGame::OnCreate()
{
std::shared_ptr<Object> player = std::make_shared<Object>();
…
auto collider = player->AddComponent<C_BoxCollider>();
collider->SetCollidable(sf::FloatRect(0, 0, frameWidth, frameHeight));
collider->SetLayer(CollisionLayer::Player);
objects.Add(player);
…
}
We add the collider, set the size (based on the size of the animation frames), and layer and then we should be good to go. If you run the game now and attempt to pass through the tiles hopefully you’ll be prevented from doing so. Success, our collision system works!You can stress test our new collision system by adding 100+ players to the game (in the SceneGame.OnCreate function) and setting their colliding layer to default so that they collide with each other. You’ll find that they resolve the collisions and look something like this:
100+ players colliding. The system works!
While this will give you a good idea of how the system resolves collisions (and its fun to watch them resolve), we’ll look at a better way to test it next week when we start writing our debug system.
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 🙂