If you’ve been following the tutorials up to this point, you’ll have a sprite that you can move around the screen using the keyboard. While this is a good start; we will, in the not too distant future, want to add additional functionality to our player object. At the very least we will want our player to have health, physics, a bounding box for collisions, and a way to animate its sprites. This is where the component system comes in, the focus of todays and next weeks tutorial.

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.

If you’ve been following the tutorials up to this point, you’ll have a sprite that you can move around the screen using the keyboard. While this is a good start; we will, in the not too distant future, want to add additional functionality to our player object. At the very least we will want our player to have health, physics, a bounding box for collisions, and a way to animate its sprites. This is where the component system comes in, the focus of todays and next weeks tutorial.

The component design pattern helps to decouple systems as they are written and maintained separately. Instead of creating a huge class/inheritance hierarchy containing all the systems that represent a player; we create individual, flexible components, and then mix and match these to create the desired object (e.g. player, projectile, enemy etc.). To do this we will write separate classes that encapsulate the behaviour we want to implement, we call this functionality ‘components’.

If you’ve used the Unity engine before then you have almost certainly used components. Most classes you write are attached to a GameObject as a component. The screenshot below shows the default Unity camera object with its various components.

Camera, GUILayer, Flare Layer, and Audio Listener are some of the built-in Unity Components. Although you can easily create your own.
Some of the built-in Unity Components.

 

As a side note, the way we implement components differs somewhat from an Entity Component System (ECS) that you may have heard of/implemented before. An ECS consists of three main systems:

  • Entities: this normally provides a method of uniquely identifying a collection of components and systems as belonging to one object. We’ll implement this shortly as an ‘Object’ class.
  • Components: in an ECS model these are usually simple data objects that do not contain any complex logic. In our model, our components can be as complex as we like.
  • Systems: this is usually where all the logic and complexity is situated. It will perform actions using the data found in the components. In our implementation, we do not write separate systems as our components will contain all the logic they need.

Before we create our components lets start on our Object class. This is similar to the ‘Entity’ class in the ECS model and it will contain and maintain a list of our components for each object.

Object.hpp
#ifndef Object_hpp
#define Object_hpp

#include "Window.hpp"

class Object
{
public:
// Awake is called when object created. Use to ensure
// required components are present.
void Awake();

// Start is called after Awake method. Use to initialise variables.
void Start();

void Update(float deltaTime);
void LateUpdate(float deltaTime);
void Draw(Window& window);
};

#endif /* Object_hpp */

These methods will soon loop through all of the components added to our object and call the relevant Update and Draw functions. But before we can implement them we need to create our Component class, which will become the base class of all future components.

Component.hpp
#ifndef Component_hpp
#define Component_hpp

#include "Window.hpp"

class Object; // 1

class Component
{
public:
Component(Object* owner) : owner(owner){}

virtual void Awake() {};
virtual void Start() {};

virtual void Update(float deltaTime) {};
virtual void LateUpdate(float deltaTime) {};
virtual void Draw(Window& window) {};

protected:
Object* owner;
};

#endif /* Component_hpp */

1. As our Object requires a reference to Component and Component requires a reference to Object; we forward declare Object.

Now we have our Component class, lets write the code to add, and retrieve components in our Object class.

Object.hpp

#include <vector>
#include "Component.hpp"

class Object
{
public:

template <typename T> std::shared_ptr<T> AddComponent() // 1
{
// This ensures that we only try to add a class the derives
// from Component. This is tested at compile time.
static_assert(std::is_base_of<Component, T>::value,
"T must derive from Component”);

// Check that we don't already have a component of this type.
for (auto& exisitingComponent : components)
{
// Currently we prevent adding the same component twice.
// This may be something we will change in future.
if (std::dynamic_pointer_cast<T>(exisitingComponent))
{
return std::dynamic_pointer_cast<T>(exisitingComponent); // 2
}
}

// The object does not have this component so we create it and
// add it to our list.
std::shared_ptr<T> newComponent = std::make_shared<T>(this);
components.push_back(newComponent);

return newComponent;
};

template <typename T> std::shared_ptr<T> GetComponent()
{
static_assert(std::is_base_of<Component, T>::value,
"T must derive from Component”);

// Check that we don't already have a component of this type.
for (auto& exisitingComponent : components)
{
if (std::dynamic_pointer_cast<T>(exisitingComponent))
{
return std::dynamic_pointer_cast<T>(exisitingComponent);
}
}

return nullptr;
};

private:
std::vector<std::shared_ptr<Component>> components;
};

1.  As AddComponent and GetComponent are template functions, i’ve implemented them in the header. These functions will work with any class that inherits from Component and when we implement our first components it will become clear how we use these functions.

2. Both template functions make use of dynamic casting. This will cast a superclass to a subclass. As we are using smart pointers, if the cast is invalid it will return an empty shared ptr.

Currently we only allow a user to add a component of a certain type once. In future we may want to be more flexible and allow the user to add and retrieve multiple objects of the same type.

With our component data structure implemented, we can write our Awake, Start, Update, LateUpdate, and Draw methods of our Object class.

Object.cpp
#include "Object.hpp"

void Object::Awake()
{
for(int i = components.size() - 1; i >= 0; i--)
{
components[i]->Awake();
}
}

void Object::Start()
{
for(int i = components.size() - 1; i >= 0; i--)
{
components[i]->Start();
}
}

void Object::Update(float timeDelta)
{
for(int i = components.size() - 1; i >= 0; i--)
{
components[i]->Update(timeDelta);
}
}

void Object::LateUpdate(float timeDelta)
{
for(int i = components.size() - 1; i >= 0; i--)
{
components[i]->LateUpdate(timeDelta);
}
}

void Object::Draw(Window& window)
{
for(int i = components.size() - 1; i >= 0; i--)
{
components[i]->Draw(window);
}
}

We loop through our components in reverse order as the component vector can be changed in our update or draw calls with components adding/removing other components. If we looped in the normal way (start to end), removing a component will cause our index to point to what it thinks is the last component but is in fact memory address space it no longer owns.

To see how these systems work we first need to create a component. Lets start by creating a simple sprite component. It will, as the name suggests, draw a sprite to the screen. We’ll call the class C_Sprite. I prepend any component class with ‘C_’ to differentiate them, but you do not need to follow this convention.

C_Sprite.hpp
#ifndef C_Sprite_hpp
#define C_Sprite_hpp

#include "Component.hpp"

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

// Loads a sprite from file.
void Load(const std::string& filePath);

// We override the draw method so we can draw our sprite.
void Draw(Window& window) override;

private:
sf::Texture texture;
sf::Sprite sprite;
};

#endif /* C_Sprite_hpp */

C_Sprite.cpp
#include "C_Sprite.hpp"

C_Sprite::C_Sprite(Object* owner) : Component(owner) {}

void C_Sprite::Load(const std::string& filePath)
{
texture.loadFromFile(filePath);
sprite.setTexture(texture);
}

void C_Sprite::Draw(Window& window)
{
window.Draw(sprite);
}

Now with our component system mostly complete, and one component ready to test, lets make some changes to our SceneGame class to re-create our player character using the new component system.

Include our new classes:

SceneGame.hpp
#include “Object.hpp"
#include “C_Sprite.hpp"

We no longer need a reference to our viking sprite as we have a component to do that so replace the lines:

SceneGame.hpp
sf::Texture vikingTexture;
sf::Sprite vikingSprite;

With our new player object:

SceneGame.hpp
std::shared_ptr<Object> player;

We now need to update our OnCreate method to remove the references to our recently deleted texture and add code to instantiate our new player object and add our first component.

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

// Adds a component by calling our previously written template function.
auto sprite = player->AddComponent<C_Sprite>();
sprite->Load(workingDir.Get() + "viking.png");
}

This is how to add components to objects. When adding a component it returns a reference to the newly created component, in this example, once we have the reference to the sprite component, we pass it a file path so we can load a sprite.

As we no longer have references to the sprite in the SceneGame class we need to delete the movement code in our Update method.

SceneGame.cpp
void SceneGame::Update(float deltaTime)
{
// Removed movement code. We will re-implement this in the next tutorial.
}

And draw our player in the draw method:

SceneGame.cpp
void SceneGame::Draw(Window& window)
{
player->Draw(window); // This will draw our new sprite component.
}

Now if you run the game you’ll have the sprite being drawn but you can’t move it (as we’ve removed the movement code). So while it may seem like a step backwards, we have in fact improved our code and prepared it for future expansion. In the next tutorial we will re-implement the movement code in our new component 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 🙂