Skip to content
This repository has been archived by the owner on May 14, 2020. It is now read-only.

ECS State System

catageek edited this page Nov 10, 2014 · 8 revisions

The Entity Component System is used for storing entity/component state that is used by various systems throughout the engine.

To create a state, i.e a persistent variable that may be attached to one or more entity, you must create a component. A component is in ECS like a member variable in OOP, except that the component is not in a class hierarchy. Unlike in OOP, entities may have or not have the component.

In this article we will create a component called Health that can store the health level of an entity.

#Creation

The component must be created in component-enum.hpp.

We add 1 line in the Component enum class:

enum class Component : uint32_t {
[...]
Health
};

We also add a line at the end of the file:

TRILLEK_MAKE_COMPONENT(Health,"health",uint32_t,SystemValue)

This line tells us that

  • the friendly name of the component is health,
  • the type stored is an unsigned int with a 32-bit width,
  • the category is SystemValue.

This macro will create a Health_type type that refers to uint32_t, so Health_type can replace uint32_t.

#Component category

We mentioned previously the SystemValue component category. There are 3 main types of components:

  • Shared
  • System
  • SystemValue

##Shared

Shared component category is a work in progress. A Shared component will have the ability to be broadcasted to other threads or through the network. You should not use this category unless you know what you are doing. See System or SystemValue.

##System

System component category is the right choice for class or struct objects. The components will be stored using smart pointers and are accessed through dereference.

##SystemValue

SystemValue component category is similar to System, except that smart pointers are not used. Instead, the data is copied in the container element. This is the right choice for primitive data.

Currently only bool and uint32_t types are supported in SystemValue.

#Initialization

##Initialization function

If the component must be initialized externally, we must implement a function to tell how it will be initialized. This is done in component.cpp:

template<>
uint32_t Initialize<Component::Health>(bool& result, const std::vector<Property> &properties) {
     id_t entity_id;
     uint32_t health = 100; // default value;
     result = false;
     for (const Property& p : properties) {
         std::string name = p.GetName();
         if (name == "health") {
             health = p.Get<uint32_t>();
         }
         else if (name == "entity_id") {
             entity_id = p.Get<id_t>();
             // tell to the caller that we must add the component
             result = true;
         }
         else {
             LOGMSG(ERROR) << "Health: Unknown property: " << name;
         }
     }
     return health;
}

The initialization function has 2 possible signatures:

Component_type Initialize<Component::Component_name>(bool& result, const std::vector<Property> &properties)

std::shared_ptr<Container> Initialize<Component::Component_name>(const std::vector<Property> &properties)

The former is for SystemValue components, the latter for System or Shared components.

If there is an error during initialization, an empty smart pointer must be returned, or the bool result must be set to false in the SystemValue version.

##Registration

If there is an Initialize() function, it must be declared in registration.cpp:

void ComponentFactory::RegisterTypes() {
    [...]
    RegisterComponentType(ComponentAdder<SYSTEM,Component::Health,uint32_t>(system_value));
    [...]
}

This declaration repeats the information given previously.

#Usage

##Functions The component can be used in the code. Useful functions are declared in the component namespace.

Get<Component::Health>(id): returns the value of the health for a specific entity.
Insert<Component::Health>(id): creates the health component for a specific entity.
Update<Component::Health>(id): updates the value of the health for a specific entity.
Remove<Component::Health>(id): removes the the health component for a specific entity.
Bitmap<Component::Health>() : returns the BitMap object for the component.

The Bitmap returned by the Bitmap() function is a map of bits with 1 bit for each entity. '1' indicates that the entity has the component.

##Container object

You must not create some std:::shared_ptr<AnyComponentType> because you won't be able to store it. Instead, you must now create and manage a std::shared_ptr<Container> or std::shared_ptr<const Container>.

System and Shared components use the component::Container object internally. GetContainer() and GetConstContainer() returns a std::shared_ptr<Container> or std::shared_ptr<const Container> to allow some manipulation.

To get the component type pointer itself, you must call component::GetSharedPtr(id) that will return a smart pointer of the component type. There is also a static function Container::GetSharedPtr<Type>(std::shared_ptr<Container>) to convert a Container shared pointer to a typed shared pointer.

Insert() and Update() can take a std::shared_ptr<Container> as argument. To create a shared_ptr from a value, call component::Create() or component::CreateConst().

If you get a std::shared_ptr<Container> and store it in another component, or in the same component using a different entity id, you will create an additional reference on a unique component. You will NOT create a copy, you just copy the pointer. To copy a component by value, you must dereference it.

##Iteration on components

The better way to iterate on components is to use the OnTrue() function. The signature is:

Template<class T>
static void OnTrue(const BitMap<T>& bitmap, const std::function<void(id_t)>& operation)

OnTrue() will execute operation on each entity with the bit set to 1 in the bitmap.

The bitmap is a BitMap object such as the one returned by BitMap(). The function is a lambda taking an entity id as parameter:

// Kill entities with health and whose health is 0
OnTrue(Bitmap<Component::Health>(),
    [&](id_t entity_id) {
         // this function is executed only on entitities that has a health component
         auto health = Get<Component::Health>(entity_id);
         if (health == 0) {
             //kill entity
             LOGMSG(INFO) << "Entity #" << entity_id << " should die now";
             // set health to 300
             Update<Component::Health>(entity_id, 300);
         }
         else {
             // decrement health
             Update<Component::Health>(entity_id, --health);
         }
    }
);

##Bitmap combinations

Bitmap objects have bool operations:

  • NOT (~)
  • AND (&)
  • OR (|)
  • XOR (^)

The result is a Bitmap object itself. This object can be used in OnTrue() to make a complex selection of entities based on their components:

// Kill entities with health and whose health is 0 and are not immune
Bitmap<Component::Health>() & ~Bitmap<Component::Immune>()

##Comparison and arithmetic functions

Some functions are provided to make some comparison or arithmetic operations with a constant or another component. Check in component.hpp.

##Copy all components

To make a copy of all components of a type, use GetRawContainer<Component::MyComponent>().