We create a scene management system for our game engine. A scene will contain the logic for a specific part of our game e.g. the main menu, game, and pause scenes.

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.

In this tutorial we will extend our game engine by writing a scene management system. If you’ve used Unity (or a similar game engine), you’ll have noticed how you can separate the game logic into different scenes. Each scene has its own unique properties and objects and will contain the logic for a specific part of our game.

Scene Management in Unity
Scene Management in Unity

 

Most, if not all games, employ a number of scenes; a main menu and a game scene at the very least. Typically, a game will also have a splash screen; as well as a pause, game over, and credits scene. You can also create separate scenes for different levels of your game.

There’s a lot of code to get through so lets start by creating a new class called Scene. This will be the parent class of all our scenes. This is an abstract class that wont be instantiated, instead we’ll create specific scenes that inherit from this class.

Scene.hpp
#ifndef Scene_hpp
#define Scene_hpp

#include “Window.hpp"

class Scene
{
public:
	// Called when scene initially created. Called once.
    virtual void OnCreate() = 0; 
	
	// Called when scene destroyed. Called at most once (if a scene 
	// is not removed from the game, this will never be called).
    virtual void OnDestroy() = 0; 
    
	// Called whenever a scene is transitioned into. Can be 
	// called many times in a typical game cycle.
    virtual void OnActivate() {}; 
	
	// Called whenever a transition out of a scene occurs. 
	// Can be called many times in a typical game cycle.
    virtual void OnDeactivate() {};
	
    // The below functions can be overridden as necessary in our scenes.
    virtual void ProcessInput() {};
    virtual void Update(float deltaTime) {};
    virtual void LateUpdate(float deltaTime) {};
    virtual void Draw(Window& window) {};
};


#endif /* Scene_hpp */

We’ll implement OnCreate and OnDestroy as pure virtual functions to ensure that this class is not instantiated. These two methods will need to be implemented in any class that inherits from Scene. If you find that you are implementing any other method (or combination of methods) more often, then it may be wise to change those methods to pure virtual functions instead.

To manage our scenes we will write a finite-state machine (FSM). A FSM stores one or more states (scenes in this instance) with only a single state active at one time. A FSM also has the ability to transition between states, which suits our needs perfectly.

You can find more information on FSMs here. Our FSM will be very simple (we’ll build on it in future), so if you’ve never implemented a FSM before it shouldn’t be a issue. I will also be discussing FSMs in more detail in a future tutorial on game AI.

Create a SceneStateMachine class. This will be FSM responsible for maintaining our scenes.

SceneStateMachine.hpp
#ifndef SceneStateMachine_hpp
#define SceneStateMachine_hpp

#include <memory>

#include "Scene.hpp"
#include "Window.hpp"

class SceneStateMachine
{
public:
    SceneStateMachine();

    // ProcessInput, Update, LateUpdate, and Draw will simply be 
	// pass through methods. They will call the correspondingly 
	// named methods of the active scene.
    void ProcessInput();
    void Update(float deltaTime);
    void LateUpdate(float deltaTime);
    void Draw(Window& window);
    
	// Adds a scene to the state machine and returns the id of that scene.
    unsigned int Add(std::shared_ptr<Scene> scene); 
	
	// Transitions to scene with specified id.
    void SwitchTo(unsigned int id); 
	
	// Removes scene from state machine.
    void Remove(unsigned int id); 

private:
	// Stores all of the scenes associated with this state machine.
    std::unordered_map<unsigned int, std::shared_ptr<Scene>> scenes; 
	
	// Stores a reference to the current scene. Used when drawing/updating.
    std::shared_ptr<Scene> curScene; 
	
	// Stores our current scene id. This is incremented whenever 
	// a scene is added.
    unsigned int insertedSceneID; 
};

#endif /* SceneStateMachine_hpp */

Lets implement the more straightforward methods first.

SceneStateMachine.cpp
#include “SceneStateMachine.hpp"

SceneStateMachine::SceneStateMachine() : scenes(0), curScene(0) { }

void SceneStateMachine::ProcessInput()
{
    if(curScene)
    {
        curScene->ProcessInput();
    }
}

void SceneStateMachine::Update(float deltaTime)
{
    if(curScene)    
    {
        curScene->Update(deltaTime);
    }
}

void SceneStateMachine::LateUpdate(float deltaTime)
{
    if(curScene)
    {
        curScene->LateUpdate(deltaTime);
    }
}

void SceneStateMachine::Draw(Window& window)
{
    if(curScene)
    {
        curScene->Draw(window);
    }
}

These methods simply call the relevant methods in the current scene (if there is one).

We need to be able to add and remove scenes to our state machine. This is relatively simple as all we need to do is add/remove the item from our map and call our OnCreate and OnDestroy methods after creation and before destruction.

SceneStateMachine.cpp
unsigned int SceneStateMachine::Add(std::shared_ptr<Scene> scene)
{
    auto inserted = scenes.insert(std::make_pair(insertedSceneID, scene));
    
    insertedSceneID++;

    inserted.first->second->OnCreate();
    
    return insertedSceneID - 1;
}

When we add a scene to the state machine we return an unsigned int. This is the scenes id and is currently required to remove the scene from the state machine. Using an unsigned int as the maps keys gives us the ability to have a maximum of 4,294,967,295 scenes. This should be more than enough for our purposes (or nearly anyones I would imagine). I have consciously not added any overflow protection here yet as this would just complicate the code and I don’t think we are in any danger of inserting more scenes than we have space allocated. You can also add duplicate scenes and retrieve different ids, which is something we can improve in future.

It is worth noting that the actual size of the int can vary depending on the compiler used. This will not be an issue for us as we will still be able to create a larger number of scenes that we are likely to ever use. If we wanted to ensure the size of the int is the same regardless of the system we are compiling on, then we would use fixed-sized data types (which we did when writing our bit mask class.

Removing scenes is also relatively simple. As previously stated, it requires passing in the scene id that is provided when we add a scene to our state machine.

SceneStateMachine.cpp
void SceneStateMachine::Remove(unsigned int id)
{
    auto it = scenes.find(id);
    if(it != scenes.end())
    {
        if(curScene == it->second)
        {
			// If the scene we are removing is the current scene, 
			// we also want to set that to a null pointer so the scene 
			// is no longer updated.
            curScene = nullptr;
        }
        
		// We make sure to call the OnDestroy method 
		// of the scene we are removing.
        it->second->OnDestroy(); 
        
        scenes.erase(it);
    }
}

This method attempts to find a key with the provided id and if found it is removed from our map after we call the scenes OnDestroy method so it can perform any cleanup.

Lastly we can create the SwitchTo method that transitions to a scene based on its id.

SceneStateMachine.cpp
void SceneStateMachine::SwitchTo(unsigned int id)
{
    auto it = scenes.find(id);
    if(it != scenes.end())
    {
        if(curScene)
        {
			// If we have a current scene, we call its OnDeactivate method.
            curScene->OnDeactivate(); 
        }
        
		// Setting the current scene ensures that it is updated and drawn.
        curScene = it->second; 
        
        curScene->OnActivate();
    }
}

Now we have the backend, lets create a couple of scenes for testing. Lets start with a splash screen, where we will briefly display our logo before loading another scene (which we will create shortly). Create a new class called SceneSplashScreen. We’ll start all scenes with the word ‘scene’ so we can easily find them. Of course, you do not have to follow this convention.

SceneSplashScreen.hpp
#ifndef SceneSplashScreen_hpp
#define SceneSplashScreen_hpp

#include <SFML/Graphics.hpp>

#include "SceneStateMachine.hpp"
#include "WorkingDirectory.hpp"

class SceneSplashScreen : public Scene
{
public:
    SceneSplashScreen(WorkingDirectory& workingDir, 
		SceneStateMachine& sceneStateMachine, Window& window);

    void OnCreate() override;
    void OnDestroy() override;
    
    void OnActivate() override;
        
    void SetSwitchToScene(unsigned int id);

    void Update(float deltaTime) override;
    void Draw(Window& window) override;
    
private:
    sf::Texture splashTexture;
    sf::Sprite splashSprite;
    
    WorkingDirectory& workingDir;
    SceneStateMachine& sceneStateMachine;
    Window& window;
    
	// We want to show this scene for a set amount of time
    float showForSeconds;
	
	// How long the scene has currently been visible.
    float currentSeconds;

	// The state we want to transition to when this scenes time expires.
    unsigned int switchToState;
};

#endif /* SceneSplashScreen_hpp */

We’ll inherit the methods that we have to (OnCreate and OnDestroy) and want to (OnActivate, Update and Draw).

SplashScreen.cpp
#include "SceneSplashScreen.hpp"

SceneSplashScreen::SceneSplashScreen(WorkingDirectory& workingDir, 
									 SceneStateMachine& sceneStateMachine,
									 Window& window) 
: sceneStateMachine(sceneStateMachine), workingDir(workingDir), 
window(window), switchToState(0), currentSeconds(0.f), 
showForSeconds(3.f) // We’ll show this splash screen for 3 seconds.
{}

void SceneSplashScreen::OnCreate() 
{
	// We’ll initialise our splash screen image here.
	
    splashTexture.loadFromFile(workingDir.Get() 
							   + "that_games_guy_logo.png");
    splashSprite.setTexture(splashTexture);
    
    sf::FloatRect spriteSize = splashSprite.getLocalBounds();
	
	// Set the origin of the sprite to the centre of the image:
    splashSprite.setOrigin(spriteSize.width * 0.5f, 
						   spriteSize.height * 0.5f); 
    splashSprite.setScale(0.5f, 0.5f);
    
    sf::Vector2u windowCentre = window.GetCentre();
	
	// Positions sprite in centre of screen:
    splashSprite.setPosition(windowCentre.x, windowCentre.y); 
}

void SceneSplashScreen::OnActivate()
{
	// Resets the currentSeconds count whenever the scene is activated.
	currentSeconds = 0.f; 
}

void SceneSplashScreen::OnDestroy() { }

void SceneSplashScreen::SetSwitchToScene(unsigned int id) 
{
	// Stores the id of the scene that we will transition to.
    switchToState = id; 
}

void SceneSplashScreen::Update(float deltaTime)
{
    currentSeconds += deltaTime;
    
    if(currentSeconds >= showForSeconds) 
    {
		// Switches states.
        sceneStateMachine.SwitchTo(switchToState); 
    }
}

void SceneSplashScreen::Draw(Window& window)
{
    window.Draw(splashSprite);
}

Lets also create the game scene. Theres nothing new here, we are just moving all the logic from the Game class to our new scene.

SceneGame.hpp
#ifndef SceneGame_hpp
#define SceneGame_hpp

#include "Scene.hpp"
#include "Input.hpp"
#include "WorkingDirectory.hpp"

class SceneGame : public Scene
{
public:
    SceneGame(WorkingDirectory& workingDir);
    
    void OnCreate() override;
    void OnDestroy() override;
    
    void ProcessInput() override;
    void Update(float deltaTime) override;
    void Draw(Window& window) override;
    
private:
    sf::Texture vikingTexture;
    sf::Sprite vikingSprite;
    
    WorkingDirectory& workingDir;
    Input input;
};

#endif /* SceneGame_hpp */

SceneGame.cpp
#include "SceneGame.hpp"

SceneGame::SceneGame(WorkingDirectory& workingDir) : workingDir(workingDir)
{
    
}

void SceneGame::OnCreate()
{
    vikingTexture.loadFromFile(workingDir.Get() + "viking.png");
    vikingSprite.setTexture(vikingTexture);
}

void SceneGame::OnDestroy()
{
    
}

void SceneGame::ProcessInput()
{
    input.Update();
}

void SceneGame::Update(float deltaTime)
{
    const sf::Vector2f& spritePos = vikingSprite.getPosition();
    const int moveSpeed = 100;
    
    int xMove = 0;
    if(input.IsKeyPressed(Input::KEY::LEFT))
    {
        xMove = -moveSpeed;
    }
    else if(input.IsKeyPressed(Input::KEY::RIGHT))
    {
        xMove = moveSpeed;
    }
    
    int yMove = 0;
    if(input.IsKeyPressed(Input::KEY::UP))
    {
        yMove = -moveSpeed;
    }
    else if(input.IsKeyPressed(Input::KEY::DOWN))
    {
        yMove = moveSpeed;
    }
    
    float xFrameMove = xMove * deltaTime;
    float yFrameMove = yMove * deltaTime;
    
    vikingSprite.setPosition(spritePos.x + xFrameMove, spritePos.y + yFrameMove);
}

void SceneGame::Draw(Window& window)
{
    window.Draw(vikingSprite);
}

The final changes we need to make are to the Game class. Our Game class will be responsible for creating the scenes and updating our SceneStateMachine. We also need to remove the logic that is now implemented in the SceneGame class.

Game.hpp
#ifndef Game_hpp
#define Game_hpp

#include <SFML/Graphics.hpp>

#include "WorkingDirectory.hpp"
#include "Window.hpp"
#include "Input.hpp"
#include “SceneStateMachine.hpp” // New
#include “SceneSplashScreen.hpp" // New
#include “SceneGame.hpp" // New

class Game
{
public:
    Game();
    
    void ProcessInput();
    void Update();
    void LateUpdate();
    void Draw();
    void CalculateDeltaTime();
    bool IsRunning() const;
    
private:
    Window window;
    WorkingDirectory workingDir;
    
    sf::Clock clock;
    float deltaTime;
    
    SceneStateMachine sceneStateMachine; // New
};

#endif /* Game_hpp */

We also no longer need a reference to Input as this is now implemented in our game scene.

In our Game class constructor, we will create two scenes, the splash and game scene.

Game.cpp
Game::Game() : window("that game engine")
{
     std::shared_ptr<SceneSplashScreen> splashScreen = 
		 std::make_shared<SceneSplashScreen>(workingDir, 
											   sceneStateMachine, 
											   window); //1
    
    std::shared_ptr<SceneGame> gameScene = 
		std::make_shared<SceneGame>(workingDir);
       
    unsigned int splashScreenID = sceneStateMachine.Add(splashScreen); //2
    unsigned int gameSceneID = sceneStateMachine.Add(gameScene);
    
    splashScreen->SetSwitchToScene(gameSceneID); //3
    
    sceneStateMachine.SwitchTo(splashScreenID); //4
    
    deltaTime = clock.restart().asSeconds();
}

1. This creates a smart pointer to a splash screen scene. And the next line does the same but for a game scene.

2. We add our newly created scenes to the state machine. This returns the scenes id within the state machine.

3. Now that we have our game scenes id we can set the splash screen to transition to the game scene.

4. We want the game to start at the splash screen, so we transition to that scene using its id.

We then need to call the relevant update and draw methods on our scene state machine.

Game.cpp
void Game::ProcessInput()
{
    sceneStateMachine.ProcessInput();
}

void Game::Update()
{
    window.Update();
    
    sceneStateMachine.Update(deltaTime);
}

void Game::LateUpdate()
{
    sceneStateMachine.LateUpdate(deltaTime);
}

void Game::Draw()
{
    window.BeginDraw();
    
    sceneStateMachine.Draw(window);
    
    window.EndDraw();
}

So now when you run the game you’ll be greeted with a splash screen for a few seconds before the game is loaded.

Our New Splash Screen
Our New Splash Screen

In future we may convert our scene system to make use of components; where each scene, rather than being its own class, will consist of a collection of ‘components’. These components are smaller logical units that can be mixed and matched to create scenes. This topic will be covered further in the next tutorial when we look at implementing an Entity Component System for our player object.

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 🙂