Entity System Redux...

Published July 18, 2012
Advertisement
It's been a while since I last updated, and I had just recently implemented a Component Based Entity System. However, while it was great fun working with the components, and having the events passed between them all, I think there's a better way. The better way is how the Artemis System works.

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.
0 likes 5 comments

Comments

ZachBethel
Looks good. It looks like you're not using smart pointers for your components. Is your entity manager responsible for cleaning up when an entity is destroyed?
July 18, 2012 06:14 AM
Inferiarum
Why does the damage System handle the collisions? I would rather let a physics system handle collisions and send a message on collision which can be hooked by other systems.

Also, what does the input component do? Of course you have an input system (or maybe better player control system which gets input from the input system). Now lets say a key is pressed which should change the velocity. You should change the velocity i guess. What i want to say is that some systems work on the data which is already there, this is also why I do not like the idea of determining the systems used based on the components.

It should be the other way round. For each entity you define which systems it uses and the systems add the components needed (if not already added by another system)
July 18, 2012 09:02 AM
BeerNutts
[quote name='Inferiarum' timestamp='1342602144']
Why does the damage System handle the collisions? I would rather let a physics system handle collisions and send a message on collision which can be hooked by other systems.

Also, what does the input component do? Of course you have an input system (or maybe better player control system which gets input from the input system). Now lets say a key is pressed which should change the velocity. You should change the velocity i guess. What i want to say is that some systems work on the data which is already there, this is also why I do not like the idea of determining the systems used based on the components.

It should be the other way round. For each entity you define which systems it uses and the systems add the components needed (if not already added by another system)
[/quote]
I responded to your [url="http://www.gamedev.net/topic/628120-new-entity-system-for-c-like-artemis/page__view__findpost__p__4960466"]post in the forum[/url].

Thanks!
July 18, 2012 01:26 PM
BeerNutts
[quote name='ZBethel' timestamp='1342592050']
Looks good. It looks like you're not using smart pointers for your components. Is your entity manager responsible for cleaning up when an entity is destroyed?
[/quote]

ZBethel, I started out using smart pointers, but I felt it was overkill for this system. Yes, the EntityManager will destroy any component in it when the entity is deleted. Also, when a component is removed from the world, it can be destroyed (via a parameter, as many times the component will just be moved to another entity).

If I feel strongly about switching back, I can do it pretty easily since I have typedef'd all the objects.

Thanks for the response!
July 18, 2012 01:30 PM
NetGnome
<3 Artemis. Its what i'm using as the base framework for Vaerydian. With my various nit-pick tweaks of course :)
July 18, 2012 08:38 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement