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 take a break from working on our projectile code to create a shared repository of useful classes, which we’ll call the shared context. The shared context is a class that contains references to other classes that we want our components to have access to. It all sounds very simple and luckily it is. Creating a shared context involves a number of steps:

  1. Writing the shared context and adding references to the classes we want shared.
  2. Ensuring every object has access to the shared context.
  3. Removing any local references that components currently store and instead using the references in the shared context.
  4. Removing any setters/mutators from components for classes that are now included in the shared context.

At the moment the classes we want to include in the context are:

  • Input: we want any component to be able to query the keyboard state.
  • ObjectCollection: we want any component to be able to add other objects to the game, such as our projectile attack component creating new arrow objects.
  • WorkingDirectory: to retrieve anything from the disk we first need to retrieve the location of the asset.
  • TextureAllocator: some of our components (like our projectile attack) can add textures to objects.
  • Window: any component that acts as a camera or interacts with the player’s view will require access to the Window class.

This list will almost certainly expand in future as we create new components. Now that we’ve decided which classes we want to include we can write the SharedContext class.

SharedContext.hpp
#ifndef SharedContext_hpp
#define SharedContext_hpp

#include "Input.hpp"
#include "WorkingDirectory.hpp"
#include "ResourceAllocator.hpp"
#include "Window.hpp"

class ObjectCollection;

struct SharedContext
{
Input* input;
ObjectCollection* objects;
WorkingDirectory* workingDir;
ResourceAllocator<sf::Texture>* textureAllocator;
Window* window;
};

#endif /* SharedContext_hpp */

The class is simple especially as I’ve opted not to write mutators and accessors, although feel free to include them if you wish.We want every object and it’s components to have access to the shared context. To achieve this change the Objects constructor so that it accepts a reference to the shared context.

Object.hpp
class Object
{
public:
Object(SharedContext* context);



SharedContext* context;

};

Object.cpp
Object::Object(SharedContext* context) : context(context), queuedForRemoval(false)
{

}

Again you can create a getter/setter for the context if you wish. Now when you create an object you need to pass it the shared context. Right now we’re only creating objects in our game scene so that’s where we’ll instantiate SharedContext. We may wish to change this in future so that we can share these classes between scenes.

SceneGame.hpp
class SceneGame : public Scene
{


private:

SharedContext context;
};

Now SceneGame owns the context it will also be its job to initialise the pointers. We’ll do this first thing in the OnCreate function.

SceneGame.cpp
void SceneGame::OnCreate()
{
context.input = &input;
context.objects = &objects;
context.workingDir = &workingDir;
context.textureAllocator = &textureAllocator;
context.window = &window;

}

This ensures that any object created does not have to worry about null pointers in the context. As we’ve changed the objects default constructor we also need to change how we create new objects: we now have to pass the shared context. This includes the creation of our player object in the OnCreate function (just after initialising the pointers).

SceneGame.cpp
void SceneGame::OnCreate()
{

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


}

I kept the previous way of instantiating objects as a reminder of how we used to do it. The only change is that when creating the shared object we pass a reference to the context. Our tilemap parser will also need a context to create each tile object. We’ll pass the context in the constructor and store a reference to be used when creating tiles.

TileMapParser.hpp
class TileMapParser
{
public:
TileMapParser(ResourceAllocator<sf::Texture>& textureAllocator,
SharedContext& context);

private:
SharedContext& context;

};

TileMapParser.cpp
TileMapParser::TileMapParser(ResourceAllocator<sf::Texture>& textureAllocator, 
SharedContext& context) :
textureAllocator(textureAllocator), context(context){}

Now when we create new tile objects in the Parse function we pass a pointer to the context.

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


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


}

We need to change the call to the TileMapParser constructor in SceneGame.

SceneGame.cpp
SceneGame::SceneGame(WorkingDirectory& workingDir, 
ResourceAllocator<sf::Texture>& textureAllocator,
Window& window) :
workingDir(workingDir),
textureAllocator(textureAllocator),
mapParser(textureAllocator, context), // Pass in the SharedContext
window(window) { }

The last object instantiation that needs to be changed is the creation of the projectile object in the C_ProjectileAttack component. As each object has a reference to the context and every component can access its own object we can use that reference.

C_ProjectileAttack.cpp
void C_ProjectileAttack::SpawnProjectile()
{
// Retrieve the shared context from the parent object.
std::shared_ptr<Object> projectile = std::make_shared<Object>(owner->context);


}

This shows how we can access the context from any component. Now that it’s possible to access the shared context we no longer need many of the mutator functions we created previously. We need to remove pointers and their corresponding set functions in the C_Sprite, C_KeyboardMovement, C_Camera, and our recently written C_ProjectileAttack classes. Let’s start with C_Sprite.

C_Sprite.hpp
class C_Sprite : public Component, public C_Drawable
{
public:
C_Sprite(Object* owner);

// Remove the below function
//void SetTextureAllocator(ResourceAllocator<sf::Texture>* allocator);

private:
// Remove the reference to the texture allocator.
//ResourceAllocator<sf::Texture>* allocator;

};

Remove the function from the implementation file as well and change any references to the local texture allocator to use the one in our shared context instead.

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

void C_Sprite::Load(int id)
{
if(id >= 0 && id != currentTextureID)
{
currentTextureID = id;
// Retrieve texture allocator from shared context.
std::shared_ptr<sf::Texture> texture = owner->context->textureAllocator->Get(id);
sprite.setTexture(*texture);
}
}

void C_Sprite::Load(const std::string& filePath)
{
// Retrieve texture allocator from shared context.
int textureID = owner->context->textureAllocator->Add(filePath);

if(textureID >= 0 && textureID != currentTextureID)
{
currentTextureID = textureID;

// Also retrieve texture allocator from shared context.
std::shared_ptr<sf::Texture> texture = owner->context->textureAllocator->Get(textureID);
sprite.setTexture(*texture);
}
}

Remove the references to the recently removed SetTextureAllocator function in TileMapParser::Parse, SceneGame::OnCreate and C_ProjectileAttack::SpawnProjectile.

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

auto sprite = tileObject->AddComponent<C_Sprite>();
// Remove line below:
// sprite->SetTextureAllocator(&textureAllocator);
sprite->Load(tileInfo->textureID);
sprite->SetTextureRect(tileInfo->textureRect);
sprite->SetScale(tileScale, tileScale);
sprite->SetSortOrder(layerCount);
sprite->SetDrawLayer(DrawLayer::Background);

}

SceneGame.cpp
void SceneGame::OnCreate()
{
std::shared_ptr<Object> player = std::make_shared<Object>();

player->transform->SetPosition(100, 700);

auto sprite = player->AddComponent<C_Sprite>();

// Remove line below.
//sprite->SetTextureAllocator(&textureAllocator);
sprite->SetDrawLayer(DrawLayer::Entities);

}
C_ProjectileAttack.cpp
void C_ProjectileAttack::SpawnProjectile()
{
std::shared_ptr<Object> projectile = std::make_shared<Object>();

projectile->transform->SetPosition(owner->transform->GetPosition());

auto projSprite = projectile->AddComponent<C_Sprite>();
// Remove line below.
//projSprite->SetTextureAllocator(textureAllocator);
projSprite->Load(projectileTextureID);
projSprite->SetDrawLayer(DrawLayer::Entities);
projSprite->SetSortOrder(100);

objects->Add(projectile);
}

That’s it for C_Sprite, now we’ll do the same for C_KeyboardMovement by removing it’s SetInput function and its local reference to the Input class (which has now been moved to the shared context).

C_KeyboardMovement.hpp
class C_KeyboardMovement : public Component
{
public:

// Remove below line:
//void SetInput(Input* input);



private:

// Remove below line:
//Input* input;

};

Again we need to remove the function from the implementation file and replace any references to the Input class with that in the context.

C_KeyboardMovement.hpp
// Delete function below.
/*
void C_KeyboardMovement::SetInput(Input* input)
{
this->input = input;
}
*/

void C_KeyboardMovement::Update(float deltaTime)
{
//TODO: keyboardmovement should not interact with animation component.
if(animation->GetAnimationState() == AnimationState::Projectile)
{
velocity->Set(0.f, 0.f);
return;
}

float xMove = 0.f;

// Use input class from shared context.
if(owner->context->input->IsKeyPressed(Input::Key::Left))
{
xMove = -moveSpeed;
}
// Use input class from shared context.
else if(owner->context->input->IsKeyPressed(Input::Key::Right))
{
xMove = moveSpeed;
}

float yMove = 0.f;
// Use input class from shared context.
if(owner->context->input->IsKeyPressed(Input::Key::Up))
{
yMove = -moveSpeed;
}
// Use input class from shared context.
else if(owner->context->input->IsKeyPressed(Input::Key::Down))
{
yMove = moveSpeed;
}

velocity->Set(xMove, yMove);
}

Remove the call to SetInput from SceneGame::OnCreate.

SceneGame.cpp
void SceneGame::OnCreate()
{
std::shared_ptr<Object> player = std::make_shared<Object>();

player->transform->SetPosition(100, 700);

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

// Replace these two lines:
//auto movement = player->AddComponent<C_KeyboardMovement>();
//movement->SetInput(&input);

// With this one:
player->AddComponent<C_KeyboardMovement>();

}

We’re almost there, only the camera and projectile class to go. Let’s update the camera class next by removing its SetWindow function and the local pointer to the Window class.

C_Camera.hpp
class C_Camera : public Component
{
public:
C_Camera(Object* owner);

void LateUpdate(float deltaTime) override;

// Remove line below.
//void SetWindow(Window* gameWindow);

private:
// Remove line below.
//Window* window;
};


Again remember to remove the function from the implementation file and change the calls to the window class to retrieve it from the shared context instead.

C_Camera.cpp
// Delete this function:
/*
void C_Camera::SetWindow(Window* gameWindow)
{
window = gameWindow;
}
*/

void C_Camera::LateUpdate(float deltaTime)
{
// Use the Window class from shared context.
sf::View view = owner->context->window->GetView();

const sf::Vector2f& targetPos = owner->transform->GetPosition();
view.setCenter(targetPos.x, targetPos.y);

// Use the Window class from shared context.
owner->context->window->SetView(view);
}

We once again need to edit SceneGame::OnCreate, this time to remove the call to SetWindow.

SceneGame.cpp
void SceneGame::OnCreate()
{
std::shared_ptr<Object> player = std::make_shared<Object>();



// Replace these two lines:
//auto camera = player->AddComponent<C_Camera>();
//camera->SetWindow(&window);
// With:
player->AddComponent<C_Camera>();



}

Last up is our new projectile attack component, we no longer need to set the input, object collection, working directory, or texture allocator manually.

C_ProjectileAttack.hpp
#include “ObjectCollection.hpp"


class C_ProjectileAttack : public Component
{
public:
C_ProjectileAttack(Object* owner);

void Awake() override;

void Start() override;

void Update(float deltaTime) override;

// Remove lines below.
// void SetInput(Input* input);
// void SetObjectCollection(ObjectCollection* objects);
// void SetWorkingDirectory(WorkingDirectory* workingDirectory);
// void SetTextureAllocator(ResourceAllocator<sf::Texture>* textureAllocator);

private:
void SpawnProjectile();

std::shared_ptr<C_Animation> animation;

// Remove lines below.
// Input* input;
// ObjectCollection* objects;
// WorkingDirectory* workingDirectory;
// ResourceAllocator<sf::Texture>* textureAllocator;
int projectileTextureID;
};

Once again, and for the last time, we need to remove the functions from the implementation file and change how we reference the classes we’ve just removed.

C_ProjectileAttack.cpp
// Remove the mutator functions below:
/*
void C_ProjectileAttack::SetInput(Input* input)
{
this->input = input;
}

void C_ProjectileAttack::SetObjectCollection(ObjectCollection* objects)
{
this->objects = objects;
}

void C_ProjectileAttack::SetWorkingDirectory(WorkingDirectory* workingDirectory)
{
this->workingDirectory = workingDirectory;
}

void C_ProjectileAttack::SetTextureAllocator(ResourceAllocator<sf::Texture>* textureAllocator)
{
this->textureAllocator = textureAllocator;
}
*/

void C_ProjectileAttack::Start()
{
// Use the texture allocator class from shared context.
projectileTextureID = owner->context->textureAllocator->Add(
owner->context->workingDir->Get() + "LPC/Weapons/arrow.png");
}

void C_ProjectileAttack::Update(float deltaTime)
{
// Use the input class from shared context.
if(owner->context->input->IsKeyDown(Input::Key::E))
{
SpawnProjectile();
animation->SetAnimationState(AnimationState::Projectile);
}
// Use the input class from shared context.
else if (owner->context->input->IsKeyUp(Input::Key::E))
{
animation->SetAnimationState(AnimationState::Idle);
}
}



void C_ProjectileAttack::SpawnProjectile()
{
std::shared_ptr<Object> projectile = std::make_shared<Object>(owner->context);

projectile->transform->SetPosition(owner->transform->GetPosition());

auto projSprite = projectile->AddComponent<C_Sprite>();
projSprite->Load(projectileTextureID);
projSprite->SetDrawLayer(DrawLayer::Entities);
projSprite->SetSortOrder(100);

// Use the object collection class from shared context.
owner->context->objects->Add(projectile);
}

We’re nearly there I promise! The last step to implementing the shared context is to remove the calls to the mutator functions (the ones we’ve just deleted) from SceneGame::OnCreate.

SceneGame.cpp
void SceneGame::OnCreate()
{
std::shared_ptr<Object> player = std::make_shared<Object>();


// Change the below lines:
/*
auto projectileAttack = player->AddComponent<C_ProjectileAttack>();
projectileAttack->SetInput(&input);
projectileAttack->SetObjectCollection(&objects);
projectileAttack->SetWorkingDirectory(&workingDir);
projectileAttack->SetTextureAllocator(&textureAllocator);
*/
// To:
player->AddComponent<C_ProjectileAttack>();


}

Because we’ve made numerous small changes to the SceneGame::OnCreate function over the course of this tutorial you may want to refer to the implementation in the Github repo to make sure that your code matches mine.

And that’s it, when you run the game everything should still work in the same way (if not then let me know in the comments), but in future we no longer need to write mutator functions in our components for common classes, we can simply retrieve them from the shared repository that we created this week.

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 🙂