Entity Component System 2018-02-17 15:13:25

Est. read time: 16 minutes, 9 seconds

ECS (Entity Component System) is a great architectural pattern that is perfect for building medium to big games, that offers some advantages over traditional OOP (Object Oriented Programming). In short, when using ECS, every game object consists of Entity - unique ID, multiple Components that hold data, and Systems that operate on and manipulate components. This separation allows for much easier implementation of independent features than big objects built with inheritance.



It is an amazing design pattern that fits perfectly when you need your AI to share logic with the player, or have an ability to create or change any entity from a defined set of building blocks. Think of a goblin that you slap a pair of wings to, and just by a single line of code, he gains the ability to fly. It can be accomplished simply by adding a FlyingComponent to the goblin entity, either when you define the goblin in your game, or maybe when someone casts a spell on it while you're in the middle of a battle.



Before we get into the meat, I would like to warn you however that I do not believe this design pattern should be used to build the whole game upon. Classes that operate on your assets, or persist the game state (save / load), hold data about NPC guilds can and potentially should be implemented in traditional OOP. It is perfectly viable to calculate Field of View, pathfinding, AI finite state machines and many more systems inside this pattern.



To start implementing ECS, you will need a Manager class. It will hold all components in their respective stores and act as an entry point for the rest of your game. Let's get the heavy code chunk out of the way first.



// Manager.h

#include "Entity.h"

#include "Component.h"

#include "ComponentTypes.h"

#include "System.h"



#include <map>

#include <unordered_map>

#include <set>

#include <vector>

#include <memory>

#include <cassert>

#include <limits>

#include <stdexcept>



/** ComponentStore holds Components assigned to Entities. */

typedef std::unordered_map<Entity, std::shared_ptr<Component>> ComponentStore;



class Manager {

public:

Manager();

virtual ~Manager();



/** Create a ComponentStore for a certain type of Component. */

void registerComponent(ComponentType type);



/**

* Add a System to the stack.

* Shared pointer used (instead of a unique_ptr) to be able to handle multiple entries

* into the vector of managed Systems, for example if you would need to run the same System

* at the beginning of the loop and in the middle after some other calculations are done.

* As always, implementation choices vary depending on needs of your project.

*/

void addSystem(const System::Ptr& systemPtr);



/**

* Create a new Entity - simply allocate a new Id.

* @return unsigned int Id of the new Entity.

*/

inline Entity createEntity() {

assert(lastEntity < std::numeric_limits<Entity>::max());

entities.insert(std::make_pair((lastEntity + 1), ComponentTypeSet()));

return (++lastEntity);

}



/**

* This is how you would access component assigned to entity:

* std::shared_ptr position = manager->getComponent (entity)

* @return std::shared_ptr

*/

template<typename C>

inline std::shared_ptr<C> getComponent(const Entity entity) {

if (entity == invalidEntity) return nullptr;



static_assert(std::is_base_of<Component, C>::value, "C must derived from the Component struct");

static_assert(C::type != invalidComponentType, "C must define a valid non-zero type");

auto componentStore = componentStores.find(C::type);

if (componentStores.end() == componentStore) {

throw std::runtime_error("The ComponentStore does not exist");

}

return std::dynamic_pointer_cast<C>(componentStore->second.at(entity));

}



/**

* Access the whole component store for a given component type. It is useful if you want to loop

* over all entities that have certain component

* @return ComponentStore*

*/

inline ComponentStore* getComponentStore(const ComponentType type) {

auto componentStore = componentStores.find(type);

if (componentStores.end() == componentStore) {

throw std::runtime_error("The ComponentStore does not exist");

}

return &componentStore->second;

}



/**

* Assign Component to Entity

* @return true if insertion succeeded

*/

template<typename C>

inline bool addComponent(const Entity entity, std::shared_ptr<C> component) {

static_assert(std::is_base_of<Component, C>::value, "C must derived from the Component struct");

static_assert(C::type != invalidComponentType, "C must define a valid non-zero type");



// Access corresponding Entity

auto iEntity = entities.find(entity);

if (entities.end() == iEntity) {

throw std::runtime_error("The Entity does not exist");

}

// Add the ComponentType to the Entity

(*iEntity).second.insert(C::type);

// Add the Component to the corresponding Store

this->componentStores[C::type].insert(std::make_pair(entity, component));



return true;

}



/**

* Register an Entity to all matching Systems, based on components the entity has.

*/

void insertEntity(const Entity entity);



/**

* Unregister an Entity from all matching Systems, based on components the entity has.

*/

void deleteEntity(const Entity entity);



/**

* Clear all entities. If you need to start fresh, maybe when player moved to a different

* zone where you load a whole new set of entities.

*/

void clearAll();



/**

* Update all Entities of all Systems. This is used in a loop, it executes all systems

* registered with the Manager.

*/

size_t updateEntities(float elapsedTime);



/**

* Remove component from an entity, for example if you would want the entity to become a ghost

* that can pass through walls, you could remove collidable component for a time.

*/

void removeComponent(const Entity entity, const ComponentType component);



/**

* Check if entity has registered component of given type

*/

bool entityHasComponent(const Entity entity, const ComponentType component) const;



/**

* Draw function - use systems with draw methods to draw entities on screen

*/

void drawEntities(ASEngine::Renderer& renderer);



/**

* Get components available to given entity

*/

ComponentTypeSet getAvailableComponents(Entity entity);



/**

* Get player entity

*/

Entity getPlayer();



/// Id of the last created Entity (start with invalid Id 0).

Entity lastEntity = 0;



/**

* Hashmap of all registered entities, listing the Type of their Components.

*

* This only associates the Id of each Entity with Types of all it's Components.

*/

std::unordered_map<Entity, ComponentTypeSet> entities;



/**

* Map of all Components by type and Entity.

* Store all Components of each Entity, by ComponentType.

*/

std::map<ComponentType, ComponentStore> componentStores;



private:



/**

* List of all Systems, ordered by insertion (first created, first executed).

* If a pointer to a System is inserted twice, it is executed twice in each iteration

* (in the order of insertion).

*/

std::vector<System::Ptr> systems;

};



Components

//Entity.h



/**

* An Entity represents an object, but does not contain any data by its own, nor any logic.

* It is only defined as an aggregation of Components, processed and updated by associated Systems.

*/

typedef unsigned int Entity;



/**

* Entities are strictly positive Ids, so we reserve lowest one for casses

* when we need to return information that entity for given conditions could not be found

*/

static const Entity invalidEntity = 0;



//Components/Component.h

struct Component {

virtual ~Component() {}

/// Default invalid component type

static const ComponentType type = ComponentType::NONE;

};



// Components/Player.h



// Input states descibes current state of the player, for example when player press `i`,

// state changes to INVENTORY and DrawInventory System could draw box with player's equipment.

enum class InputState : int {

NONE, CRAFTING, WOODCUTTING, INVENTORY, MAP

};



struct Player : public Component {

static const ComponentType type = ComponentType::PLAYER;



// Current state of the player

InputState inputState = InputState::NONE;

};



// Components/Name.h

#include <string>



struct Name : public Component {

Name() = default;

Name(const int index, const std::string& name) : index(index), name(name) {}



static const ComponentType type = ComponentType::NAME;



// Name of the entity

std::string name;

};



// Components/Collidable.h

struct Collidable : public Component {

static const ComponentType type = ComponentType::COLLIDABLE;

};



// Components/Position2D.h

#include <glm/glm.hpp>



struct Position2D : public Component {

Position2D() = default;

Position2D(const glm::ivec2& position) {

this->setPosition(position);

}

Position2D(const glm::vec2& position) {

this->setPosition(position);

}



static const ComponentType type = ComponentType::POSITION_2D;



// Setters for position

void setPosition(const glm::ivec2 & tilePosition) {

this->tilePosition = tilePosition;

// Below line fetches tile dimensions from a global settings object, it will probably differ for your code.

auto tileDimensions = Systems::inst().getUserSettings().getTileDimensions();

this->position = static_cast<glm::vec2>(this->tilePosition * tileDimensions + tileDimensions / 2);

}

void setPosition(const glm::vec2 & position) {

this->position = position;

this->tilePosition = static_cast<glm::ivec2>(this->position / static_cast<glm::vec2>(this->tilePosition));

}



// Position in the world

glm::vec2 position;

// Position on the map (x, y in terms of tiles)

glm::ivec2 tilePosition;

// Set up next movement to be processed for the entity, if it's player, process keyboard input,

// in case of AI, separate system implementing pathfinding

glm::ivec2 moveTo = glm::ivec2(-1);

}



Systems

// System.h



#include <ASEngine/Graphics/Renderer.h>

#include "ComponentTypes.h"

#include "Entity.h"

#include <set>

#include <memory>



class Manager;



/**

* A System manages any Entity having all required Components.

*/

class System {

public:

/**

* @param manager Reference to the manager needed to access Entity Components.

*/

explicit System(Manager& manager);



virtual ~System() = default;



/**

* Get the Types of all the Components required by the System.

* Only entities having all of the specified components will be processed by the system.

*/

inline const ComponentTypeSet& getRequiredComponents() const {

return this->requiredComponents;

}



/**

* Register an Entity, it has all required Components.

*

* @param entity

*

* @return true if the Entity has been inserted successfully

*/

inline bool registerEntity(Entity entity) {

return matchingEntities.insert(entity).second;

}



/**

* Remove registered Entity, if it was registered.

*

* @param entity

*

* @return true if the Entity has been removed successfully

*/

inline bool unregisterEntity(Entity entity) {

return matchingEntities.erase(entity);

}



/**

* Clear all entities from a given system

*/

inline void clearEntities() {

matchingEntities.clear();

}



/**

* Check if system will process this entity

*

* @param entity

*

* @return true if Entity was found.

*/

inline bool hasEntity(Entity entity) const {

return (matchingEntities.end() != matchingEntities.find(entity));

}



/**

* Update all entities that are processed by this system.

*

* @param elapsedTime Elapsed time since last update call, in seconds.

*/

virtual void updateEntities(float elapsedTime);



/**

* Draw function - use systems with draw methods to draw entities on screen

*/

virtual void drawEntities(ASEngine::Renderer& renderer);



protected:

/**

* Specify what are required Components of the System.

*

* @param requiredComponents List the Types of all the Components required by the System.

*/

inline void setRequiredComponents(ComponentTypeSet&& requiredComponents) {

this->requiredComponents = std::move(requiredComponents);

}



/**

* Reference to the manager needed to access Entity Components.

*/

Manager& manager;



/**

* List the types of all the Components required by the System.

*/

ComponentTypeSet requiredComponents;



/**

* List all the matching Entities having required Components for the System.

*/

std::set<Entity> matchingEntities;

};

// Systems/NameTooltip.h

#include "../System.h"

#include "../Manager.h"

#include <ASEngine/GUI/Tooltip.h>



class NameTooltip : public System

{

public:

explicit NameTooltip(Manager& manager);



size_t updateEntities(float elapsedTime) override;



// Nothing to draw

void drawEntities(ASEngine::Renderer& renderer) override;



protected:

ASEngine::Tooltip tooltip;

};



// Systems/NameTooltip.cpp

#include "NameTooltip.h"

#include "../Components/Position2D.h"

#include "../Components/Name.h"

#include <memory>



NameTooltip::NameTooltip(Manager& manager) : System(manager)

{

this->setRequiredComponents({ ComponentType::NAME, ComponentType::POSITION_2D });

}



void NameTooltip::updateEntities(float elapsedTime)

{

// Clear tooltip state, hide it before checking if and what it should display.

tooltip.show = false;



// Get screen coordinates of the mouse

ASEngine::vec2 mouseCoords = this->manager.inputManager->getMouseCoords();

ASEngine::vec2 tileDimensions = Systems::inst().getUserSettings().getTileDimensions();

const ASEngine::ivec2 tileCoords = ASEngine::ivec2(

static_cast (floor((mouseCoords.x - tileDimensions.x / 2.0f) / tileDimensions.x)),

static_cast (floor((mouseCoords.y - tileDimensions.y / 2.0f) / tileDimensions.y))

);



for (const Entity entity : this->matchingEntities) {

std::shared_ptr position = this->manager.getComponent (entity);

if (position->tileDimensions == tileCoords) {

// Tile with entity is under mouse cursor, display it's name on the screen

std::shared_ptr name = this->manager.getComponent (entity);

this->tooltip.setContent(name->name);

this->tooltip.show = true; // Display tooltip

break;

}

}

}



void NameTooltip::drawEntities(ASEngine::Renderer & renderer)

{

// No entity with name is under mouse cursor, don't draw anything.

if (!this->tooltip.show) return;



this->tooltip.draw();

}



// Systems/TileMovement.h



#include "../System.h"

#include "../Manager.h"

#include "../Components/Position2D.h"



class TileMovement : public System

{

public:

explicit TileMovement(Manager& manager);



// Nothing to update

void updateEntities(float elapsedTime) override;



// Nothing to draw

void drawEntities(ASEngine::Renderer& renderer) override {}



protected:

// Check if it's possible to move to given tile

bool canMoveTo(const ASEngine::ivec2& target, ComponentStore* positions, Entity currentEntity) const;

};



// Systems/TileMovement.cpp

#include "TileMovement.h"





TileMovement::TileMovement(Manager & manager) : System(manager)

{

this->setRequiredComponents({ ComponentType::POSITION_2D });

}



void TileMovement::updateEntities(float elapsedTime)

{

ComponentStore* positions = this->manager.getComponentStore(Position2D::type);



for (auto entity : this->matchingEntities) {

std::shared_ptr<Position2D> position = this->manager.getComponent<Position2D>(entity);



// If cant move to given tile, reset movement

if (!this->canMoveTo(position->moveTo, positions, entity)) {

position->moveTo = glm::ivec2(-1);

continue;

}



position->setPosition(position->moveTo);

}

}



bool TileMovement::canMoveTo(const ASEngine::ivec2 & target, ComponentStore* positions,

const Entity currentEntity) const

{

// Search through position components.

// If some entity is at the position where currentEntity want to go, make sure it's not collidable

for (const auto& position : *positions) {

std::shared_ptr<Position2D> pos = std::dynamic_pointer_cast<Position2D>(position.second);



// if Entity found on the targeted position, movement is not allowed.

if (pos->tilePosition == target && this->manager.entityHasComponent(position.first,

ComponentType::Collidable)) {

return false;

}

}



return true;

}



manager->addComponentToEntity(entity, std::make_shared<Collidable>());

Conclusion

{

"components": ["background", "glyph", "mineable", "name", "position_2d", "collidable", "block_light"],

"background": { "color": [ 64, 63, 67 ] },

"description": "A vein of black coal.",

"glyph": { "color": [ 217, 144, 88 ], "id": 211, "tile_type": 0, "tiles": [ 0 ] },

"id": 202,

"mineable": { "amount": 50, "resource": 148, "time": 30.0 },

"name": "Coal vein"

}



If you are familiar with SQL databases, this concept might be easier to grasp. Entity is your primary key, that is just a unique integer, but it is shared across all tables in your database.on the other hand would represent different records in a table (). So if a player would have ID = 1, every(one per) should hold data about the player under index = 1.Let’s start by defining some simple components that can be used by our player and/or other objects in our game:Player, Name, Collidable, PositionThis is a parent class, everywill inherit from it. It's very simple - type parameter is a unique enum element used to differentiate components. Everyin your game will have a different type assigned.is for the player. It just holds the state enum that can define which menu panel is currently active, that a tree is currently being cut down (animation playing), or player is trying to attack with ranged weapon and targetting should be drawn on the screen. This component can also serve as a way to identify if entity is a player, by simply askingif entity by given ID has Player Component assigned to it, if so, we know we are dealing with the player.holds an information about the name of an. Essentially anything that will display it’s name in your game can use this same component, not just the player. It is probably worth mentioning that components don't have to hold so little data. In a real world example,could also have a description field, to offer some more insight for the Player into what exactly he is looking upon, or index for the entity template loaded from json file.is an example of an empty component. You can think of it as a simple boolean flag - does the entity collide with everything else or not. Such a component will be enough for tile based game, where 2 collidables cannot enter the same field. It could be expanded to hold information about shape of collision, or which layer of other entities thiswould collide with (like for example you would want bullets to collide with players and walls, but not with other bullets).is just a little bit more complex. It holds 2 types of position information. A 2-dimensional integer position (glm::ivec2) represents which tile on a grid this entity occupies, and a 2-dimensional float position (glm::vec2) of where the entity should be rendered on screen.is a vector math library, which I'm not going to get too much into detail here, as this would get way too long. The unique thing about thisis that it has data mutating functions and not just data attributes. Keep in mind that you probably shouldn’t put other functions than getters and setters here for your data, there are more appropriate places for them.So we have some data to work with, let’s create some systems that will operate on it: NameTooltip, TileMovement. Systems are usually a bit heavy on the code as they contain most of your game logic, so I hope you can forgive me for simplifying it a bit.This is the basefrom which every custom one will inherit.that have all of thespecified by theas required are held in the std::set. It has methods to process or draw based on information given by the components. ASEngine is a custom engine that I've created for my games, only renderer is used here to draw something on the screen, be it some GUI elements, or Sprites.displays the name of theif it's under mouse cursor. Rather small functionality, but this piece of code will not affect any other part of your game. This system is focused on drawing stuff on the screen, it does some calculations and searching for proper information under updateEntities function, and simply draws what it has in drawEntities. This separation of logic and rendering might come in handy some day since these two can do their things independently they can become good candidates for multithreading if performance needs will justify it. Let's see one moreexample, a bit more logic / physics focused with no drawing.simply moves entities according to either their input or pathfinding calculations that are done in separateso this part of the code can be shared between Player and AI. You can do many more things here on collision, like animation, play a sound, or automatically make an attack action against encountered. So now, after we have implemented our components and systems, to make an entity collide with others we simply add a line:And after that, every time thewill move and useit will not be allowed to occupy same tile as other collidable objects in your game.Well that is essentially it. At this point you just add more components and systems, growing your game. If you would like to have full code for the ECS, I would point you to check https://github.com/SRombauts/ecs which is a great starting point to build your own implementation. Unfortunatelly there would be no point in me giving the exact source code used by my game, as it is heavily catered to my specific needs. I hope examples presented here were interesting enough to spike your imagination. Last thing I would like to show you, is the goal of this design pattern.This is a json template for a vein of coal that can be mined taken straight out of a game I'm currently working on. Just by combining components you can create some very unique things and expand your game much easier with such a well structured codebase. If you have questions or would like to offer some constructive criticism about the content of this page, please don't hesitate to leave a comment.Thank you for reading.