C++ Game Engine Development Part 9 – Resource Management

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.

As games become more complex, the issue of how to manage a games assets and resources is only becoming more difficult; which is why, even though we do not have many resources at the moment, it is a good idea to start the groundwork on what will become a robust and generic (it can handle any resource we throw at it) resource management system. 

A good resource manager will be efficient and allow us to easily re-use our assets without having to re-allocate memory for previously used resources. For example, we do not want to create copies of the same texture, when we can just create the one and pass the reference to any system that needs it.

In the context of this article, a resource can include sprites, audio files, and fonts; but they can also be text, XML, and JSON data files. It is pretty much anything that is not code.

Resource management is not just a consideration for players of RTS games.
Resource management is not just a consideration for players of RTS games.

Our resource manager will handle instantiation, allocation, and retrieval (using a resource id) of our games assets. It will also be generic so that we can easily instantiate different resource managers that are responsible for allocating different resources. 

I attempt to avoid using the word ‘manager’ in my class names (you’ve probably seen many examples of GameManagers, LevelManagers, etc.) for many of the reasons outlined here. With that in mind, I’ll call our resource manager ResourceAllocator.hpp. This provides a bit more information about what the class actually does.

ResourceAllocator.hpp
#ifndef ResourceAllocator_hpp
#define ResourceAllocator_hpp

#include <map>
#include <memory>

template<typename T>
class ResourceAllocator
{
   
private:
    int currentId;
    std::map<std::string, std::pair<int, std::shared_ptr<T>>> resources;
};

#endif /* ResourceAllocator_hpp */

The class makes use of template functions. As the methods are templates they will need to be created in the header file. We’ll create the Add function first. This will be responsible for loading the resource and adding it to our resource allocator.

ResourceAllocator.hpp
int Add(const std::string& filePath)
{
    auto it = resources.find(filePath); // 1
    if (it != resources.end())
    {
        return it->second.first;
    }

    std::shared_ptr<T> resource = std::make_shared<T>();
    if (!resource->loadFromFile(filePath)) // 2
    {
        return -1;
    }

	resources.insert(
		std::make_pair(filePath, std::make_pair(currentId, resource)));
        
    return currentId++;
}

1. We store the file path as well as the resource so we can use it as a unique identifier. Two assets with the same file path are assumed to be the same asset. This prevents us from creating the same assets twice.

2. As we call LoadFromFile on our new resource, it is important that any asset we use has this method implemented. SFML resources (sf::Texture, sf::Font etc.) have this method by default so we do not need to worry about writing our own implementation for them.

The Add method is relatively simple. It provides the bare bones of what we require from a resource manager. We will change things in future tutorials but for now, it works and does everything we need it to. It is worth noting that it returns an integer, this is the id of the resource. If we want to retrieve or remove a resource we will need this id.

Now that we can add resources lets create the Remove method.

ResourceAllocator.hpp
void Remove(int id)
{
    for (auto it = resources.begin(); it != resources.end(); ++it)
    {
        if (it->second.first == id)
        {
            resources.erase(it->first);
        }
    }
}

This iterates over our resource collection and if it finds the resource with the associated id it is removed from the collection. 

Adding and removing won’t do us any good if we cannot retrieve our resources, so let’s create the Get method. We will also create a way of telling if an allocator has a resource with the specified id.

ResourceAllocator.hpp
    std::shared_ptr<T> Get(int id)
    {
        for (auto it = resources.begin(); it != resources.end(); ++it)
        {
            if (it->second.first == id)
            {
                return it->second.second;
            }
        }
        
        return nullptr;
    }
    
    bool Has(int id)
    {
        return (Get(id) != nullptr);
    }

A shared_ptr  is returned to a resource (if present).

With the resource allocator complete for now, we can change our existing code base to make use of it, starting with our sprite component.

C_Sprite.hpp
…
#include "ResourceAllocator.hpp"

class C_Sprite : public Component
{
public:
…
	// We add a new overloaded Load method that accepts a texture id.
    void Load(int id); 

    void SetTextureAllocator(ResourceAllocator<sf::Texture>* allocator); // 1
    
private:
    ResourceAllocator<sf::Texture>* allocator;
    sf::Sprite sprite;
};

  1. We currently need to manually pass the reference to the resource allocator to the component; in future, we can look at different ways of accomplishing this.

Make sure you delete the existing reference to the texture as we will retrieve this from our new resource allocator. We’ll keep the reference to the sprite as this is a lightweight structure. You can find out more about SFML sprites and textures here.

C_Sprite.cpp
void C_Sprite::SetTextureAllocator(ResourceAllocator<sf::Texture>* allocator) 
{
    this->allocator = allocator;
}

void C_Sprite::Load(int id)
{
    if(id >= 0)
    {
        std::shared_ptr<sf::Texture> texture = allocator->Get(id);
        sprite.setTexture(*texture);
    }
}

void C_Sprite::Load(const std::string& filePath)
{
    if(allocator)
    {
        int textureID = allocator->Add(filePath);
        
        if(textureID >= 0)
        {
            std::shared_ptr<sf::Texture> texture = allocator->Get(textureID);
            sprite.setTexture(*texture);
        }
    }
}

We now set our sprites texture by retrieving a smart pointer to a texture from our resource allocator. As the texture is on the stack the local reference is removed at the end of the function. Each sprite component no longer needs to store there own texture as our resource allocator handles that for us. This prevents us from allocating duplicate textures as we create more sprite components.

Now that our sprite component requires a texture allocator, any class that instantiates the component will need to pass through that class. Therefore our game and splash screen scene will need to either create or have a reference to the allocator. As they both need the reference, and it’s likely that future scenes will also require access to a texture allocator, we can create the texture allocator in the game class and pass it through the constructor to our scenes.

SceneGame.hpp
…
#include “ResourceAllocator.hpp"
class SceneGame : public Scene
{
public:
	// We now pass the texture allocator to our game scene.
    SceneGame(WorkingDirectory& workingDir, 
		ResourceAllocator<sf::Texture>& textureAllocator); 
…
    
private:
…
	// We’ll store a reference to the texture allocator.
    ResourceAllocator<sf::Texture>& textureAllocator; 
};

SceneGame.cpp
SceneGame::SceneGame(WorkingDirectory& workingDir, 
					 ResourceAllocator<sf::Texture>& textureAllocator) 
	: workingDir(workingDir), textureAllocator(textureAllocator) { }

Now we need to set the texture allocator in our OnCreateMethod.

SceneGame.cpp
void SceneGame::OnCreate()
{ 
    player = std::make_shared<Object>();
    
    auto sprite = player->AddComponent<C_Sprite>();
    sprite->SetTextureAllocator(&textureAllocator); // Add this line
    sprite->Load(workingDir.Get() + "viking.png");
    
    auto movement = player->AddComponent<C_KeyboardMovement>();
    movement->SetInput(&input);
}

We’ll also pass a resource allocator to the SceneSplashScreen.

SceneSplashScreen.hpp
#include "ResourceAllocator.hpp"

class SceneSplashScreen : public Scene
{
public:
    SceneSplashScreen(WorkingDirectory& workingDir, 
		SceneStateMachine& sceneStateMachine, Window& window, 
		ResourceAllocator<sf::Texture>& textureAllocator);
…

private:
…
    ResourceAllocator<sf::Texture>& textureAllocator;
};

#endif /* SceneSplashScreen_hpp */

SceneSplashScreen.cpp
SceneSplashScreen::SceneSplashScreen(WorkingDirectory& workingDir, 
									 SceneStateMachine& sceneStateMachine, 
									 Window& window, 
						ResourceAllocator<sf::Texture>& textureAllocator) 
	: sceneStateMachine(sceneStateMachine), workingDir(workingDir), 
						window(window), switchToState(0), currentSeconds(0.f), 
						showForSeconds(3.f), textureAllocator(textureAllocator)
{
    
}

void SceneSplashScreen::OnCreate()
{
    int textureID = textureAllocator.Add(workingDir.Get() 
										 + “that_games_guy_logo.png"); //1
    
    if(textureID >= 0)
    {
        std::shared_ptr<sf::Texture> texture = textureAllocator.Get(textureID);
        splashSprite.setTexture(*texture);
        
        sf::FloatRect spriteSize = splashSprite.getLocalBounds();
        splashSprite.setOrigin(spriteSize.width * 0.5f, 
							   spriteSize.height * 0.5f);
        splashSprite.setScale(0.5f, 0.5f);
        
        sf::Vector2u windowCentre = window.GetCentre();
        splashSprite.setPosition(windowCentre.x, windowCentre.y);
    }
}

1. We assume that this is the first time we will be using the logo so this scene will add it to the texture allocator.

Now we need to pass the texture allocator to our splash screen and game scene in the Game class constructor.

Game.cpp
Game::Game() : window("that game engine")
{
	// Pass in the texture allocator to our splash screen.
    std::shared_ptr<SceneSplashScreen> splashScreen =
		std::make_shared<SceneSplashScreen>(workingDir, sceneStateMachine, 
											window, textureAllocator); 

	// We now pass the textureAllocator to our game scene.
    std::shared_ptr<SceneGame> gameScene = 
		std::make_shared<SceneGame>(workingDir, textureAllocator); 
…
}

We also need to create a reference to the texture allocator in our Game header. We’ll only have the one copy of the texture allocator for now but in future, we may want different systems to be responsible for their own resources.

Game.hpp
class Game
{
private:
…
    ResourceAllocator<sf::Texture> textureAllocator;
};

Make sure you run the game to check we have not broken anything, and you should (fingers crossed) be greeted with the now familiar sprite. Next week we will look at one method of handling the large number of objects that our game will eventually have and then we will finally start making changes to what is displayed on screen when we start our animation 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 🙂