22 min read

In this article by Raimondas Pupius, the author of SFML Game Development By Example, we will look into some of the common game programming patterns.

(For more resources related to this topic, see here.)

What is a programming pattern?

Programming patterns, or design patterns, at they’re more commonly referred to, are reusable and widely-implemented solutions to a given problem. That is not to say that these patterns exist as some sort of libraries out there, although there are libraries based on them. Instead, a programming pattern is more of an idea or a strategy. It is a well laid out plan on tackling a certain problem, the best possible answer to a given problematic situation, proven by time and experience, which is one of the the best reasons they should be used.

There are quite a few design patterns out there, as well as books, tutorials, and even classes dedicated solely to understanding and implementing them. For our purposes, we’re going to be covering four: the entity component system, event queue, observer, and factory patterns. We’ll be talking about each one separately, as they’re non overlapping in function, even though they can be working together.

The entity component system

The Entity Component System is a programming pattern, which allows entities to possess properties and functionality through the means of composition, as opposed to inheritance. The biggest benefits of using this pattern include stronger decoupling of logic, easier serialization & de-serialization of entities, better reusability of code, and ease of creating new entities. It does, however, add a fair bit of complexity to your code base.

The typical implementation of this pattern consists of three parts:

  • Entities: Entities, in most cases, are barely anything more than identifiers, slapped on a collection of components.
  • Components: Components are the building blocks of entities, that are nothing more than collections of data.
  • Systems: Systems are specialized classes that deal with a very specific task and are responsible for holding all of the logic in this paradigm.

In addition to working with these three distinct types of elements, our entity component system is also going to need an entity manager to keep and manage all of the entity and component data, as well as the system manager, which will be responsible for updating each system, in addition to some other functionality we’ll be covering soon.

In order to differentiate between different types of components and systems, we’re going to create a new header file, ECS_Types.h, which will be used to store this information:

using ComponentType = unsigned int;
#define N_COMPONENT_TYPES 32
enum class Component{
 Position = 0, SpriteSheet, State, Movable, Controller, Collidable
};
enum class System{
 Renderer = 0, Movement, Collision, Control, State, SheetAnimation
};

In addition to component and system enumerations, we’re also aliasing an unsigned integer to act as the component type and defining a macro N_COMPONENT_TYPES, which represents the maximum number of component types we can have.

What is a component?

Within the entity component system paradigm, a component is the smallest, non-overlapping aspect of an entity, such as its position, velocity, or a sprite. From the programming point of view, however, it is nothing more than a simple data structure, which has no real logic in it. Its only job is storing information about the feature of an entity it represents, as illustrated here:

In order to store components easily, they have to rely on principles of inheritance. Let’s take a look at a base component class definition:

class C_Base{
public:
  C_Base(const Component& l_type): m_type(l_type){}
  virtual ~C_Base(){}
  Component GetType(){ return m_type; }
  friend std::stringstream& operator >>(
    std::stringstream& l_stream, C_Base& b)
  {
    b.ReadIn(l_stream);
    return l_stream;
  }
  virtual void ReadIn(std::stringstream& l_stream) = 0;
protected:
  Component m_type;
};

The constructor of our component base class will take in the type of the component it represents. One thing to note is the overloaded >> operator, which calls a purely virtual function ReadIn. This serves as a quick way to read component data in from a file. Because each component is unique, it defines its own version of the ReadIn method in order to load its data correctly.

The position component

A good example of putting the base component class to work is actually implementing the first and arguably most common type of component: position.

class C_Position : public C_Base{
public:
  C_Position(): C_Base(Component::Position), m_elevation(0){}
  ~C_Position(){}
  void ReadIn(std::stringstream& l_stream){
    l_stream >> m_position.x >> m_position.y >> m_elevation;
  }
  const sf::Vector2f& GetPosition(){ return m_position; }
  const sf::Vector2f& GetOldPosition(){ return m_positionOld; }
  unsigned int GetElevation(){ return m_elevation; }
  void SetPosition(float l_x, float l_y){
    m_positionOld = m_position;
    m_position = sf::Vector2f(l_x,l_y);
  }
  void SetPosition(const sf::Vector2f& l_vec){

    m_positionOld = m_position;
    m_position = l_vec;
  }
  void SetElevation(unsigned int l_elevation){
    m_elevation = l_elevation;
  }
  void MoveBy(float l_x, float l_y){
    m_positionOld = m_position;
    m_position += sf::Vector2f(l_x,l_y);
  }
  void MoveBy(const sf::Vector2f& l_vec){
    m_positionOld = m_position;
    m_position += l_vec;
  }
private:
  sf::Vector2f m_position;
  sf::Vector2f m_positionOld;
  unsigned int m_elevation;
};

The constructor of our component base class is invoked in the initializer list, with the component type being passed in as the only argument. Although there are better ways of assigning individual component types their own unique identifiers, it’s better to start simple for clarity’s sake.

This component keeps track of three pieces of data: its current position, the position it was at during the previous cycle, and the current elevation of an entity, which is simply a value that represents how high the entity is in relation to the map.

Much like any other component we will be covering later in the article, it offers a number of methods for modifying and obtaining its data members. While making its data members publically available is perfectly valid, offering helper methods reduces code redundancy and offers a familiar interface.

Lastly, note the implementation of the ReadIn method. It uses a stringstream object as an argument and loads the relevant pieces of data from it.

The bitmask

Having a lightweight, easy to use as well as easy to expend data structure, representing the makeup of any given entity, as well as a set of requirements imposed by a system saves a lot of headaches. For us, that data structure is a bitmask.

The standard template library provides its own version of a bitmask: the std::bitset. For educational purposes, we’re going to be implementing our own version of this class.

As you probably know already, in binary, any and all numbers can be represented as a combination of zeroes and ones. However, who’s to say that those two values have to be used only to represent a number? With some quick bitwise operator magic, any simple integer can be turned into a string of continuous flags that represent different aspects of an entity, such as which components it has, or types of components it needs to have, in order to belong to a system. Consider the following illustration:

The only real difference in practise would be a lot more than 8 flags available. Let’s get coding:

#include 
using Bitset = uint32_t;
class Bitmask{
public:
  Bitmask() : bits(0){}
  Bitmask(const Bitset& l_bits) : bits(l_bits){}
  Bitset GetMask() const{ return bits; }
  void SetMask(const Bitset& l_value){ bits = l_value; }
  bool Matches(const Bitmask& l_bits,
    const Bitset& l_relevant = 0)const
  {
    return(l_relevant ?
      ((l_bits.GetMask() & l_relevant) == (bits & l_relevant))
      :(l_bits.GetMask() == bits));
  }
  bool GetBit(const unsigned int& l_pos)const{
    return ((bits&(1 

We begin by defining the data type for our bitset, kindly provided by the stdint.h header. As the name implies, the uint32_t type is exactly 32 bits wide. Using this type, let’s say, a typical integer, eliminates the possibility of cross-platform differences. A regular integer could take up less or more memory, depending on the platform our code is executed on. Using specialized types from the stdint.h header ensures the exact same results, regardless of platform differences.

The majority of the Bitmask class consists of nothing but bitwise operations, which are an essential part of the C/ C++ background. If you are not yet familiar with them, it’s not the end of the world, however, it would be more beneficial to at least understand how they work before moving forward.

Managing entities

Now that we have the building blocks of entities defined, it’s time to talk about storing and managing them. As mentioned previously, all an entity is at this point is a single identifier. Knowing that, we can begin shaping the way this data is going to be stored, beginning, as always, with the definition of data types to be used:

using EntityId = unsigned int;
using ComponentContainer = std::vector;
using EntityData = std::pair;
using EntityContainer = std::unordered_map;
using ComponentFactory = std::unordered_map>;

The first data type we’ll be working with is the entity identifier, once again represented by an unsigned integer. Next, a container is needed to hold all of the components for an entity. A vector works just fine for this purpose. Following that, we define a pair a bitmask and the component container, which will hold all of the information about the entity. The bitmask is used here in order to alleviate the need to iterate over containers searching for components, when it can be quickly queried for the same purpose. The last piece of the entity puzzle is mapping an entity identifier to all of its data, for which we’ll be using the unordered_map.

In order to generate different component types with as little code as possible, we’ll be using our trusty lambda-expression factory method here as well. The last four lines of type definitions here make that possible.

Having all of the data types defined allows us to finally take a look at the entity manager class declaration:

class SystemManager;
class EntityManager{
public:
  EntityManager(SystemManager* l_sysMgr,
    TextureManager* l_textureMgr);
  ~EntityManager();
  int AddEntity(const Bitmask& l_mask);
  int AddEntity(const std::string& l_entityFile);
  bool RemoveEntity(const EntityId& l_id);
  bool AddComponent(const EntityId& l_entity,
    const Component& l_component);
  template
  T* GetComponent(const EntityId& l_entity,
    const Component& l_component){ ... }
  bool RemoveComponent(const EntityId& l_entity,
    const Component& l_component);
  bool HasComponent(const EntityId& l_entity,
    const Component& l_component);
  void Purge();
private:
  template
  void AddComponentType(const Component& l_id){
    m_cFactory[l_id] = []()->C_Base* { return new T(); };
  }
  // Data members
  unsigned int m_idCounter;
  EntityContainer m_entities;
  ComponentFactory m_cFactory;
  SystemManager* m_systems;
  TextureManager* m_textureManager;
};

In a fairly predictable fashion, we have all of the methods that would exist in any other class that serves as a container. Two different versions of adding entities are provided, one based on a bitmask passed in as an argument, and the other for loading an entity configuration from a file. The method for obtaining a component from a specific entity is templated, reducing the amount of code that has to be written outside of this class in order to obtain the type of component that is desired. Let’s take a look at how it’s implemented:

template
T* GetComponent(const EntityId& l_entity,
  const Component& l_component)
{
  auto itr = m_entities.find(l_entity);
  if (itr == m_entities.end()){ return nullptr; }
  // Found the entity.
  if (!itr->second.first.GetBit((unsigned int)l_component))
  {
    return nullptr;
  }
  // Component exists.
  auto& container = itr->second.second;
  auto component = std::find_if(container.begin(),container.end(),
    [&l_component](C_Base* c){
      return c->GetType() == l_component;
    });
  return (component != container.end() ?
    dynamic_cast(*component) : nullptr);
}

The entity argument passed into the method is evaluated first, in order to determine if one with the provided identifier exists. If it does, the bitmask of that entity is checked to verify that a component with the requested type is part of it. The component is then located in the vector and returned as the dynamically-cast type of the template.

Implementing the entity manager

With the class definition out of the way, we can start implementing its methods. As per usual, let’s address the constructor and destructor of the entity manager class first:

EntityManager::EntityManager(SystemManager* l_sysMgr,
  TextureManager* l_textureMgr): m_idCounter(0),
  m_systems(l_sysMgr), m_textureManager(l_textureMgr)
{
  AddComponentType(Component::Position);
  AddComponentType(Component::SpriteSheet);
  AddComponentType(Component::State);
  AddComponentType(Component::Movable);
  AddComponentType(Component::Controller);
  AddComponentType(Component::Collidable);
}
EntityManager::~EntityManager(){ Purge(); }

The constructor takes in a pointer to the SystemManager class, which we will be implementing shortly, as well as a pointer to the TextureManager. In its initializer list, the idCounter data member is set to zero. This is simply a variable that will be used to keep track of the last identifier that was given to an entity. Additionally, both the system manager and the texture manager pointers are stored for later reference. The last purpose of the constructor is adding all of the different types of components to the component factory.

The destructor simply invokes a Purge method, which will be used to clean up all of the dynamically allocated memory and clear all possible containers in this class.

int EntityManager::AddEntity(const Bitmask& l_mask){
  unsigned int entity = m_idCounter;
  if (!m_entities.emplace(entity,
    EntityData(0,ComponentContainer())).second)
  { return -1; }
  ++m_idCounter;
  for(unsigned int i = 0; i EntityModified(entity,l_mask);
  m_systems->AddEvent(entity,(EventID)EntityEvent::Spawned);
  return entity;
}

In the case of adding an entity based on the provided bitmask, a new entity pair is inserted into the entity container first. If the insertion was successful, a for loop iterates over all possible types of components and checks the mask for that type. The AddComponent method is then invoked, if the bitmask has the said type enabled.

After the component insertion, the system manager is notified of an entity being modified, or, in this case, inserted. The entity identifier, along with the bitmask of the said entity is passed into the EntityModified method of the system manager. An event is also created to alert the systems that this entity just spawned.

The identifier of the newly created entity is then returned. If the method failed to add an entity, -1 is returned instead, to signify an error.

Removing an entity is every bit as easy, if not more so:

bool EntityManager::RemoveEntity(const EntityId& l_id){
  auto itr = m_entities.find(l_id);
  if (itr == m_entities.end()){ return false; }
  // Removing all components.
  while(itr->second.second.begin() != itr->second.second.end()){
    delete itr->second.second.back();
    itr->second.second.pop_back();
  }
  m_entities.erase(itr);
  m_systems->RemoveEntity(l_id);
  return true;
}

After the entity is successfully located in the entity container, the dynamically allocated memory of every single component it has is first freed, and the component is then removed from the vector. The entity itself is then erased from the entity container and the system manager is notified of its removal.

bool EntityManager::AddComponent(const EntityId& l_entity,
  const Component& l_component)
{
  auto itr = m_entities.find(l_entity);
  if (itr == m_entities.end()){ return false; }
  if (itr->second.first.GetBit((unsigned int)l_component))
  {
    return false;
  }
  // Component doesn't exist.
  auto itr2 = m_cFactory.find(l_component);
  if (itr2 == m_cFactory.end()){ return false; }
  // Component type does exist.
  C_Base* component = itr2->second();
  itr->second.second.emplace_back(component);
  itr->second.first.TurnOnBit((unsigned int)l_component);
  // Notifying the system manager of a modified entity.
  m_systems->EntityModified(l_entity,itr->second.first);
  return true;
}

Adding a component to an entity begins by verifying an entity with the provided identifier existing. If it does, and if there isn’t already a component of that type added to the entity, the lambda-function container is queried for the desired type. Once the memory for the component is allocated, it’s pushed into the component vector. The bitmask is then modified to reflect the changes made to the entity. The system manager is notified of those changes as well.

Predictably, a very similar process takes place when removing the component from an entity:

bool EntityManager::RemoveComponent(const EntityId& l_entity,
  const Component& l_component)
{
  auto itr = m_entities.find(l_entity);
  if (itr == m_entities.end()){ return false; }
  // Found the entity.
  if (!itr->second.first.GetBit((unsigned int)l_component))
  {
    return false;
  }
  // Component exists.
  auto& container = itr->second.second;
  auto component = std::find_if(container.begin(),container.end(),
    [&l_component](C_Base* c){
      return c->GetType() == l_component;
    });
  if (component == container.end()){ return false; }
  delete (*component);
  container.erase(component);
  itr->second.first.ClearBit((unsigned int)l_component);
  m_systems->EntityModified(l_entity, itr->second.first);
  return true;
}

After confirming that both the entity and component exist, the memory allocated for the component is freed and the component itself is erased. The bitmask also gets modified to reflect these changes. Much like before, the system manager needs to know if an entity was altered, so the EntityModified method is invoked.

A fairly useful method to have for outside classes is one that checks if an entity has a certain type of component:

bool EntityManager::HasComponent(const EntityId& l_entity,
  const Component& l_component)
{
  auto itr = m_entities.find(l_entity);
  if (itr == m_entities.end()){ return false; }
  return itr->second.first.GetBit((unsigned int)l_component);
}

This follows the same pattern as before by checking if an entity exists first, then checking its bitmask for a certain component type.

It’s cleanup time. Correctly disposing of all allocated resources is left up to the Purge method:

void EntityManager::Purge(){
  m_systems->PurgeEntities();
  for(auto& entity : m_entities){
    for(auto &component : entity.second.second){delete component;}
    entity.second.second.clear();
    entity.second.first.Clear();
  }
  m_entities.clear();
  m_idCounter = 0;
}

The system manager is notified to remove all of its entities first. While iterating over all of the entities in storage, it frees up the memory of every single component. The component container is then cleared. Lastly, the entity container itself is cleared and the identification counter is set back to 0.

The factory pattern

With complex data structures such as entities, chances are the programmer will not be setting up and initializing every single component by hand. Setting up entities with any arrangement of components quickly and with as little repeated code as possible is very much the goal here. Luckily, a programming pattern exists to solve this particular problem. It is simply referred to as the factory pattern.

The philosophy of use for this neat pattern is quite simple. There exists a class with some abstract method that takes in one or two arguments, pertaining to some vague identifying qualities. This class then generates, based on the information it was given, a class or a number of classes and returns a handle to them, effectively cutting out the part where data allocation or member initialization is done by hand. In other words, it is given a blueprint and produces a product based on it, hence the name “factory”. This functionality was already achieved in a way by creating entities based on a bitmask, however, no actual data was initialized, only the defaults. Having a more pristine way of actually setting up these entities requires a more elaborate blueprint, so why not use text files? For example:

Name Player
Attributes 63
|Component|ID|Individual attributes|
Component 0 0 0 1

This format would allow everything there is to an entity to be stored in plain text as a blueprint and loaded at any time to produce any number of entities with exactly the same qualities. Let’s take a look at how processing entity files could be achieved:

int EntityManager::AddEntity(const std::string& l_entityFile){
  int EntityId = -1;
  std::ifstream file;
  file.open(Utils::GetWorkingDirectory() +
    "media/Entities/" + l_entityFile + ".entity");
  if (!file.is_open()){
    std::cout > type;
    if(type == "Name"){
    } 
    else if(type == "Attributes"){
      if (EntityId != -1){ continue; }
      Bitset set = 0;
      Bitmask mask;
      keystream >> set;
      mask.SetMask(set);
      EntityId = AddEntity(mask);
      if(EntityId == -1){ return -1; }
    } 
       else if(type == "Component"){
      if (EntityId == -1){ continue; }
      unsigned int c_id = 0;
      keystream >> c_id;
      C_Base* component = GetComponent
        (EntityId,(Component)c_id);
      if (!component){ continue; }
      keystream >> *component;
      if(component->GetType() == Component::SpriteSheet){
        C_SpriteSheet* sheet = (C_SpriteSheet*)component;
        sheet->Create(m_textureManager);
      }
    }
  }
  file.close();
  return EntityId;
}

Loading entity files isn’t much different from any other files we’ve processed in the past. For now, reading entity names hasn’t yet been implemented. The attributes line is simply the numeric value that the bitmask has with the desired components enabled. Once that value is read in, we pass it in to the other version of AddEntity, in order to create it and have all of the components properly allocated.

Reading in the actual components is slightly more complicated. First, we must make sure that the entity has been created. This means that the “Attributes” line has to come before the individual component data in the entity file. If the entity ID is greater than -1, we proceed with reading in the component ID and obtaining the actual object based on it. The overloaded >> operator comes in handy here, since it greatly simplifies actually streaming in the component data.

Lastly, due to the nature of resource handling, the component type has to be checked in order to provide its instance with a pointer to the texture manager class, if it needs it. We haven’t yet created such components, however one of them will be the sprite sheet component that will represent some entities.

Summary

In this article, we shed some light on components and patterns that form a vital part of game programming.

Resources for Article:


Further resources on this subject:



Subscribe to the weekly Packt Hub newsletter. We'll send you the results of our AI Now Survey, featuring data and insights from across the tech landscape.

* indicates required

LEAVE A REPLY

Please enter your comment!
Please enter your name here