The main idea behind it is having Components contain ONLY data no logic. Instead you have Systems which operate on Components. Entities are basically a unique value, which binds the Components together.
Escape Entity System from a Users Perspective
In the ECS (Entity Component System) I've designed, named Escape System (since I kept typing Esc instead of Ecs while developing it, I named it Escape), it holds to this premise, with only the slightest exceptions. You can check out the source by by Github repo, Escape-System (TestEcs.cpp/h are the example files). the files in the include folder are the public ones.
From the User point of view, you have your components (which inherit the Esc Component). Here's a Health Component, which would go in any entity that has health and can die:
class THealth : public Esc::TComponent
{
public:
THealth(int maxHealth, int StartingHealth) :
Esc::TComponent("Health"), MaxHealth(maxHealth), CurrentHealth(StartingHealth){}
void AddDamage(int Damage) { CurrentHealth -= Damage; if (CurrentHealth < 0) CurrentHealth = 0; }
void AddHealth(int Damage) {CurrentHealth += Damage; if (CurrentHealth > MaxHealth) CurrentHealth = MaxHealth; };
int GetHealth() { return CurrentHealth; }
int GetHealthPercetage() { return ((CurrentHealth * 100) / MaxHealth); }
bool IsDead() { return CurrentHealth == 0; }
private:
int MaxHealth;
int CurrentHealth;
};
You can see I cheat a little having the little bit of Logic in my class. I could've called GetHealth, addded the Damage, and SetHealth, but I see no reason.
And, here's an example Damage Component. which goes into any entity that can cause damage on collision.
/******************************************************************/
class TDamage : public Esc::TComponent
{
public:
TDamage(int damagevalue) : Esc::TComponent("Damage"), DamageValue(damagevalue) {}
int GetDamage() { return DamageValue; }
private:
int DamageValue;
};
These components are added to an entity recently created. And, eventually, the entity is included into the World, like this:
Esc::TEntityPtr Entity1= pWorld->CreateEntity();
THealth* healthComp(new THealth(100, 100));
TInput* inputComp(new TInput());
pWorld->AddComponent(Entity1, healthComp);
pWorld->AddComponent(Entity1, inputComp);
pWorld->SetGroup(Entity1, "Player");
pWorld->AddEntity(Entity1);
(I threw in the SetGroup, as that is a way to tag entities into different groups)
In order for these entities and components to do anything, they have to be acted on by Systems. Here is the header for the Damage System, and the code ( including adding it to the world) follows:
class TDamageSystem : public Esc::TSystem
{
public:
TDamageSystem();
void Update(Esc::TEntityPtr entity, uint32_t tickDelta);
void Initialize();
};
...
DamageSystem = new TDamageSystem();
pWorld->AddSystem(DamageSystem);
...
TDamageSystem::TDamageSystem() :
Esc::TSystem()
{
}
void TDamageSystem::Initialize()
{
// Set which components we want to deal with
Esc::TSystem::HandleComponent("Collision", true);
Esc::TSystem::HandleComponent("Health", true);
}
void TDamageSystem::Update(Esc::TEntityPtr entity, uint32_t tickDelta)
{
// Check if this is 1st touch, and subtract damage then
TCollision *collisionComp = static_cast(entity->GetComponent("Collision"));
if (collisionComp->IsNewCollision()) {
Esc::TEntity *pOtherEntity;
collisionComp->GetNewCollisionEntity((pOtherEntity));
TDamage *damageComp = static_cast(pOtherEntity->GetComponent("Damage"));
// Ensure the thing we hit has a damage entity
if (damageComp) {
THealth *healthComp = static_cast(entity->GetComponent("Health"));
printf("Hit! Health %d, Damage %d\n", healthComp->GetHealth(), damageComp->GetDamage());
healthComp->AddDamage(damageComp->GetDamage());
}
}
}
Before anything else, Initialize will be called, and it calls it's parent class to describe what kind of entities it wants to be called Update on. In this case, we only want entities with a Collision Component (added to a component when it collides, and removed when it separates) and a Health component. Then, Update is called every tick on any entity with those components. This only happens when entities are colliding, so it happens rarely.
Systems can be setup to be called at certain timed intervals as well.
Escape System Internals
Internally, the Escape System handles all changes to the World (or items living in the World) before an Update. So, everytime you add, remove, or change a System, Entity or Component (assuming they are in the World), the Escape System tracks the changes in their order. When World::Update is called, the 1st thing it does is applies all the changes. This helps alleviate any conflicts. Here's the beginning of the World::Update code:
/********************************************************************
// Update
********************************************************************/
void TWorld::Update()
{
boost::posix_time::ptime lastTime = CurrentTime;
CurrentTime = boost::posix_time::microsec_clock::local_time();
boost::posix_time::time_duration tickDelta = lastTime - CurrentTime;
// Update in the order the commands were given
for (uint32_t i = 0; i < WorldUpdateOrder.size(); i++) {
switch(WorldUpdateOrder) {
case COMPONENT_ADDITION:
if (!ComponentAdditions.empty()) {
TEntityPtr entity = ComponentAdditions[0].Entity;
entity->AddComponent(ComponentAdditions[0].Component);
SystemManager->ComponentAddition(entity, ComponentAdditions[0].Component);
ComponentAdditions.erase(ComponentAdditions.begin());
}
break;
case COMPONENT_REMOVAL:
if (!ComponentRemovals.empty()) {
TEntityPtr entity = ComponentRemovals[0].Component->GetOwnerEntity();
SystemManager->ComponentRemoval(entity, ComponentRemovals[0].Component);
entity->RemoveComponent(ComponentRemovals[0].Component);
if (ComponentRemovals[0].FreeComp) {
delete ComponentRemovals[0].Component;
}
ComponentRemovals.erase(ComponentRemovals.begin());
}
break;
// and it continues
Every time a Component is created, it gets assign a Component bit. The same Component types get the same bit, and the entities hold a bit map of all the components it owns (using boost::dynamic_bitset) These bits are what the Systems use to determine which entities/components need to be called. Here's a what happens when a System is added.
void TSystemManager::Add(TSystemPtr system, uint32_t afterTime, bool isRepeat,
const TEntityPtrs& entities)
{
system->Initialize();
// If this is a timed system, set it up
if (afterTime) {
TTimedSystem sysInfo;
sysInfo.AtTime = afterTime + World->GetMilliSecElapsed();
sysInfo.AfterTime = afterTime;
sysInfo.IsRepeat = isRepeat;
sysInfo.System = system;
// insert in order to limit finding the shortest time
TTimedSystems::iterator it = TimedSystems.begin();
for (; it != TimedSystems.end(); it++) {
if (it->AtTime > sysInfo.AtTime) {
TimedSystems.insert(it, sysInfo);
break;;
}
}
if (it == TimedSystems.end()) {
TimedSystems.insert(it, sysInfo);
}
}
else {
Systems.push_back(system);
}
// Update the entities/components for this system
const boost::dynamic_bitset<> &SystemBits = system->GetComponentBits();
for (uint32_t i = 0; i < entities.size(); i++) {
const boost::dynamic_bitset<> &EntityBits = entities->GetComponentBits();
if (SystemBits.is_subset_of(EntityBits)) {
// If there's only 1 component for this system, just save off the
// Component it will call Update with
if (SystemBits.count() == 1) {
system->Add(entities->GetComponent(SystemBits.find_first()));
}
else {
system->Add(entities);
}
}
}
}
Each System can Overload the PreStep() function which will get called once before a round of Updates. This allows Systems to preload any content they might need for every component/entity called in Update.
Each Entity can belong to a group, and anyone can get a list of all the Entities in a Group. I use this for a Tagging system as well, by just assuming only 1 entity will be in that specific group.
Also, the User can assign a Local ID to the world. This will allow the entities to be truly unique if playing a multi-player game. Typical Entities Id' use an uint64_t, but the top 32 bits are reserved for the LocalID (which could be the IP address, or a User ID assigned by the networking system used).
So, using this System, I'm planning my next game. Hopefully I'll have an update on that soon. Please add any comments or questions you might have.