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 working on updating our drawing system. We’ll add a few new features such as a dynamic sort order and drawing layers. We’ll also change the system so it no longer stores pointers to objects but a pointer to the drawable component itself. This prevents a number of unnecessary redirections where we first have to get the objects drawable component before we can do anything with it. These new features will make our drawing system that little bit more robust and ensures that we can accomplish what we want from the game moving forward.

When we last worked on our drawing system we implemented the ability to sort our spites using a sort order, where sprites with a higher sort order appear in front of sprites with a lower sort order. This is helped us sort our tilemap layers so they appear in the correct order. This week I want to take this concept a step further and implement drawing layers. If you’ve ever used Unity (and most likely other game engines) before then you have most likely used drawing layers.

Spriterenderer with sortable layers in Unity
You can set a sort layer and order in Unity.

An entity is first sorted by layer and then sort order; so for example, if we add a sprite to the background layer with a sort order of 100 and a sprite to the foreground layer with a sort order of 1, the sprite on the background layer will always be behind the sprite on the foreground layer. The sort order now becomes the sprites order within its layer rather than a global order. This will help simplify how we sort and layer our sprites. We can set certain sprites to be a background for example, and then we do not have to worry about setting a lower sort order for our player because he will be on a different layer that will always appear in front of the background sprites.I’ll start implementation by creating the names for the layers. We’ll create an enum in C_Drawable to store them. These will change as we go along and we work out exactly what we need in our game but I’ve added a few as a starting point.

C_Drawable.hpp
enum class DrawLayer
{
Default,
Background,
Foreground,
Entities
};

class C_Drawable
{
public:


void SetDrawLayer(DrawLayer drawLayer);
DrawLayer GetDrawLayer() const;

private:

DrawLayer layer;
};

The order of the layers in the enum is important. This will be the order that the layers are drawn.The drawable now also has a DrawLayer and an accessor/mutator for the layer.

C_Drawable.cpp
C_Drawable::C_Drawable() : sortOrder(0), layer(DrawLayer::Default){}

void C_Drawable::SetDrawLayer(DrawLayer drawLayer)
{
layer = drawLayer;
}

DrawLayer C_Drawable::GetDrawLayer() const
{
return layer;
}

We set the layer to Default in the constructor. The default layer will be drawn behind every other layer.We set the layer to Default in the constructor. The default layer will be drawn behind every other layer. We won’t often want a drawable to be on the default layer so now whenever we create a drawable (such as a sprite) component we will call the SetDrawLayer function.That’s all the changes we need to make in the component class. We need to update the drawable system (S_Drawable) to use the layers. We’ll start by changing how we store the objects.

S_Drawable.hpp
std::map<DrawLayer, std::vector<std::shared_ptr<Object>>> drawables;

Instead of a vector of objects we use a map with the draw layer as a key. Each layer now has a separate vector of objects.Before we can build the project we have to update the Add, ProcessRemovals, Sort and Draw functions. Basically, any function that touched the old vector of drawables. I’ll start with the Add function.

S_Drawable.cpp
void S_Drawable::Add(std::shared_ptr<Object> object)
{
std::shared_ptr<C_Drawable> draw = object->GetDrawable();

if (draw)
{
DrawLayer layer = draw->GetDrawLayer();

auto itr = drawables.find(layer);

if (itr != drawables.end())
{
drawables[layer].push_back(object);
}
else
{
std::vector<std::shared_ptr<Object>> objs;
objs.push_back(object);

drawables.insert(std::make_pair(layer, objs));
}
}
}

We now retrieve the drawables layer and check if our map has an existing entry for that layer. If it does we simply add it to the collection, otherwise we create a new entry in the map.Removing a drawable is a similar process to before but instead of iterating over one vector of objects we need to do it for every layers vector.

S_Drawable.cpp
void S_Drawable::ProcessRemovals()
{
for (auto& layer : drawables)
{
auto objIterator = layer.second.begin();
while (objIterator != layer.second.end())
{
auto obj = *objIterator;

if (obj->IsQueuedForRemoval())
{
objIterator = layer.second.erase(objIterator);
}
else
{
++objIterator;
}
}
}
}

The same change needs to be made to Sort and Draw where we loop through the layers so we’re sorting/drawing the objects in each layers vector rather than our previous single vector.

S_Drawable.cpp
void S_Drawable::Sort()
{
for (auto& layer : drawables)
{
std::sort(layer.second.begin(), layer.second.end(),
[](std::shared_ptr<Object> a,
std::shared_ptr<Object> b) -> bool
{
return a->GetDrawable()->GetSortOrder() <
b->GetDrawable()->GetSortOrder();
});
}
}

void S_Drawable::Draw(Window& window)
{
for (auto& layer : drawables)
{
for (auto& drawable : layer.second)
{
drawable->Draw(window);
}
}
}

And with that change, we should now have support for drawing layers. The last thing we need to do is to set the player and tiles layer in SceneGame and TileMapParser respectively.

SceneGame.cpp
void SceneGame::OnCreate()
{

auto sprite = player->AddComponent<C_Sprite>();
sprite->SetTextureAllocator(&textureAllocator);
sprite->SetDrawLayer(DrawLayer::Entities);

}

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

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

// Set the tiles layer to background for now
sprite->SetDrawLayer(DrawLayer::Background);
}

}

The player should now always appear in front of the tiles regardless of the sort order we use for each. In future, if we want drawables to appear in front of the player we can create a new layer after the Entities layer and assign the drawables to that layer.The next feature I would like to implement in our drawing system is a dynamic sort order. Currently, we can only set a sprites sort order before we add it to the object collection but what if we want an object to be at one time behind the player and then later to appear in front? We could destroy/re-create a cloned object with a different sort order but it would be easier if we could just change the sort order during the game and have this change take effect before we draw the sprites.First move sort to its own function in S_Drawable.

S_Drawable.hpp
class S_Drawable
{


private:
static bool LayerSort(std::shared_ptr<Object> a, std::shared_ptr<Object> b);

};

S_Drawable.cpp
bool S_Drawable::LayerSort(std::shared_ptr<Object> a, std::shared_ptr<Object> b)
{
return a->GetDrawable()->GetSortOrder() < b->GetDrawable()->GetSortOrder();
}

The comparison function must be static because it must have two arguments (in this case object a and object b), and non-static functions have a hidden first parameter that points to the class that owns the function. I won’t go into detail about how this works but you can find more information here if you are interested.

Now that we have a separate sort function we can update Sort to take advantage of it.

S_Drawable.cpp
void S_Drawable::Sort()
{
for (auto& layer : drawables)
{
if(!std::is_sorted(layer.second.begin(), layer.second.end(), LayerSort))
{
std::sort(layer.second.begin(), layer.second.end(), LayerSort);
}
}
}

I’ve also added a new check to see if a layer is sorted before calling the sort function. I’m assuming that we will not be changing a sprites sort order most frames and therefore we not need to do any sorting for those frames. So it makes sense to first check if a layer is sorted as I believe this check is quicker than calling sort on an already sorted collection. This is something that we should confirm by timing the function, which if it does cause performance issues in future is something we will do, however in theory, this should reduce the time the game spends dealing with drawable sorting. Now we just need to move the call to Sort from the Add function to the Draw function just before we draw our sprites.

S_Drawable.cpp
void S_Drawable::Add(std::vector<std::shared_ptr<Object>>& objects)
{
for (auto o : objects)
{
Add(o);
}

// Removed call to sort
}


void S_Drawable::Draw(Window& window)
{
// Added call to sort before we draw.
Sort();

for (auto& layer : drawables)
{
for (auto& drawable : layer.second)
{
drawable->Draw(window);
}
}
}

And that’s it, now we can change a drawables sort order dynamically in-game. We won’t be using this feature for a little while yet but its good to know that it’s there when we need it.The last change I want to make this week is to edit what the drawing system actually stores. Currently, we store a vector of objects along with the objects draw layer, which means that whenever we need to access the objects drawable (which is often in a drawing system) we first have to call its GetDrawable function. This seems unnecessary so rather than storing the object we are going to store the drawable component itself.

S_Drawable.hpp
std::map<DrawLayer, std::vector<std::shared_ptr<C_Drawable>>> drawables;

With that change we once again need to modify any function that interacts with the collection. First up is the Add function.

S_Drawable.cpp
void S_Drawable::Add(std::shared_ptr<Object> object)
{
std::shared_ptr<C_Drawable> objectsDrawable = object->GetDrawable();

if(objectsDrawable)
{
DrawLayer layer = objectsDrawable->GetDrawLayer();

auto itr = drawables.find(layer);

if (itr != drawables.end())
{
drawables[layer].push_back(objectsDrawable);
}
else
{
std::vector<std::shared_ptr<C_Drawable>> objs;
objs.push_back(objectsDrawable);

drawables.insert(std::make_pair(layer, objs));
}
}
}

Not a huge change: rather than adding the object we now add the drawable component.

Next up, we need to update our new LayerSort comparator so it takes two drawables instead of objects.

S_Drawable.hpp
static bool LayerSort(std::shared_ptr<C_Drawable> a, std::shared_ptr<C_Drawable> b);

And now we can call GetSortOrder directly rather than calling GetDrawable first.

S_Drawable.cpp
bool S_Drawable::LayerSort(std::shared_ptr<C_Drawable> a, 
std::shared_ptr<C_Drawable> b)
{
return a->GetSortOrder() < b->GetSortOrder();
}

One issue with storing the drawable component is that as it does not inherit from Component so we do not have access to the IsQueuedForRemoval check. We use this to determine when to remove an item from the system’s collection. To fix this we’ll add a new pure virtual function to C_Drawable that returns a bool to determine if we should continue drawing the object.

C_Drawable.hpp
class C_Drawable
{
public:

virtual bool ContinueToDraw() const = 0;

};

This has to be overridden in any child classes, which is only C_Sprite at the moment.

C_Sprite.hpp
class C_Sprite : public Component, public C_Drawable
{
public:

bool ContinueToDraw() const override;

};

C_Sprite.cpp
bool C_Sprite::ContinueToDraw() const
{
return !owner->IsQueuedForRemoval();
}

While we are using the same IsQueuedForRemoval function to determine if an object should continue to be drawn, we may want to change this at some point so we can continue to update an object (so not set it to be queued for removal) but no longer draw it. If this becomes necessary, we could in a future tutorial create separate flags for each object: one for updating and one for drawing.With our new function complete we can update ProcessRemovals in our drawing class.

S_Drawable.cpp
void S_Drawable::ProcessRemovals()
{
for (auto& layer : drawables)
{
auto objIterator = layer.second.begin();
while (objIterator != layer.second.end())
{
auto obj = *objIterator;

if (!obj->ContinueToDraw())
{
objIterator = layer.second.erase(objIterator);
}
else
{
++objIterator;
}
}
}
}

And that’s all the changes for this week but by no means the last time we’ll update our drawing system. Off the top of my head I can think of a couple of changes I would like to make: we may want to implement variable drawing layers (similar to our dynamic sort order) so that we can change a drawables layer after it has been added to the object collection. We will also definitely be adding some 2D viewport/frustum culling where we only draw the sprites that are visible to the player. And I’m sure that as we develop the game we will be adding many different features that I have not yet thought of.

Next week rather than editing an existing system we’ll create something new. We’ll begin work on a camera component that follows the player so we can finally see the end of the level!

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 🙂