Wednesday, October 24, 2018

c++ - How do components in a component based entity system


---Most of the stuff I am talking about I read here---



I am currently tring to wrap my head around component based systems to make a simple game engine of sorts.


I am having a hard time understanding how its supposed to work. I get that there are components which all inherit from the same base, which provides basic functions (init, update, end) and lets all of the different kind of components be stored in the same array of pointers. I get that. However I dont get how any efficient interactions of the components in a game object are supposed to happen.


Lets say I made a component that stores the amount of gold in a treasure chest (lets call it dataComponent). Lets also assume I made a component that does something when its update function is called (lets call it logicComponent)


There is also a class called Entity


class entity{
private:
vector components;
public:
void addComponent(component* comp);
void removeComponent(component* comp);

void getComponent(int id);
}

Lets now say I plan to make treasureChest, without any functions. Its just a background model. Then I will make a new enitity and using addComponent I will add the dataComponent(and give it 100 gold pieces in its constructor) and a logicComponent(and give it nothing in its constructor). The player might take out, or put in as much gold as he likes. We dont know how much gold there is in the chest. There might even be multiple dataComponents storing how many jewels or weapons are in the chest.


Example:


void main(){
Entity chest;
chest.addComponent(new dataComponent("gold",100)); //100 gold pieces
chest.addComponent(new dataComponent("guns",2)); //2 weapons
chest.addComponent(new logicComponent()); //this is going to do the printing

updateEnt(Entity);//simply iterates through all components of the entity and calls thier update functions.
}
EXPECTED OUTPUT:
This chest contains 100 gold and 2 guns

The logic components job is to somehow get its hands on the data and print it to the screen.


My question is: How can the logic component get access to the other components in the same object as it is in and access the approperate ones (the ones it needs to function) without looking through the entire array every single time?



Answer



Components are just a collection of data and logic. How you store and relate them to the logical entity is where you gain or lose performance depending on the strategy you choose.


My example and explanation is in Java, but the theory can be applied to any language.



The way I usually handle custom Component engines is with HashMap registries of components. This ends up with a game engine that looks a lot like a relational database. Lookups and data manipulation are fast, and it can easily handle millions of entities at a time in memory. It has been a while since I've built one, so I don't remember any cons to the approach, but maybe someone does and can improve this answer.


Essentially it boils down to HashMaps and GUIDs. Each Entity is nothing more than a Globally Unique Identifier (GUID) which serves as the key in each HashMap. A HashMap exists for every ComponentType in your game. There are ways to make this more general purpose, but I'm going with the naive solution for simplicity of argument.


We first need to track existing Entities. I do this with a HashMap with a GUID as the Key, and a boolean as the Value which indicates whether or not the entity should be destroyed.


public class Entity
{
HashMap entities =new HashMap();

public Entity()
{
}


public GUID create()
{
GUID entity= new GUID();
entities.put(entity,false);
return entity;
}

public HashMap getAll()
{

return entities;
}

public void markPurge(GUID entity)
{
entities.put(entity, true);
}

public void purgeReady()
{

//removes all entities that have a value of true,
//only called once all other purges have been called
for(GUID entity : entities)
{
if(entities.get(entity))
entities.remove(entity);
}
}
}


Lets say the object needs health, lets start with creating a Health component. During the initialization of your game, you'd create a HashMap to register Entities that have the HealthComponent.


public class Health
{
private HashMap healths=new HashMap();

public Health(){}

public void set(GUID entity, int health)
{
healths.put(entity, health);

}

public int get(GUID entity)
{
return healths.get(entity);
}

public void remove(GUID entity)
{
healths.remove(entity);

}

public void add(GUID entity)
{
healths.put(entity, 100);
}

public void add(GUID entity, int health)
{
healths.put(entity, health);

}

public void purge(ArrayList entities)
{
for(GUID entity : entities)
healths.remove(entity);
}
}

To spawn a new Entity with 100 health in our game, we first generate a new Entity GUID, then assign it a Health component.



public void setup()
{
//spawn an entity, and add a Health component in one step
Health.add(Entity.Create());
}

To actually use the components and assign game logic to them, I use Systems. Each System performs a piece of game logic, like destroying an entity if its health reaches zero.


public class DeathSystem
{
Entity entities;

Health healths;
public DeathSystem(Entity e, Health h)
{
entities=e;
healths=h;
}

public void update()
{
for(GUID entity : health.keySet())

{
if(healths.get(entity)==0)
entities.markPurge(entity);
}
}
}

Now we simply call our systems in our main game loop, after handling user input. Altogether yielding the following


Entity entities;
Health healthComps;

DeathSystem deathSystem;
GUID player;
boolean run = true;

public void main(String args)
{
setup();
while(run)
{
update();

}
}

public void setup()
{
entities= new Entity();
healthComps = new Health();
deathSystem = new DeathSystem(entities, healthComps);
player=entities.create();
healthComps.add(player);

}

public void update()
{
handleInput();
//attackSystem.update();
//healSystem.update();
deathSystem.update();
//finally, call the function that calls purge on all the component registries to remove any destroyed objects
purgeAll();

if(entities.getAll().size()==0)
run=false;
}

The beauty of this kind of structure is that all of your entity lookups are O(1) operations, and each System only operates on Entities currently registered as having the relevant components. Because of this, the systems only do what is absolutely necessary. If none of your entities have health, the death system has nothing to iterate over, and returns in constant time. This also keeps the memory footprint at an absolute minimum.


There are other approaches as well, but this is the one that I've had the most success with, and I think this illustrates that a component isn't so much an object, as a fuzzy collection of data and logic.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...