Alright, time for a little update post!
Since the initial post, I have rewritten large parts of the entity system which was needed since it was the oldest part of the framework and I have learnt a lot since then, and as kind people in this thread pointed out, the system had serious flaws.
These flaws have now been fixed and the API is cleaner and much safer to use, and it has also gotten more features. The system has three main parts: The EntityManager, EntityFactory and the EntityComponent.
The EntityManager stores all entities, and with it you can register attributes that the entities hold, and also create and delete entities.
The EntityFactory is a convenience class where you can register entity templates. These are predefined sets of attributes. They can also have default values and inherit from each other. This is shown in the example code below.
The final part consists of an abstract base class EntityComponent which is inherited to create entity logic. How this works is also shown in the example below.
With these parts working together, Feather Kit provides a full system for handling both entity data and logic.
Here follows a code snippet with a full example on how the entity system is used in practice. I hope it is not too long, and if anything is unclear and you want to know more, just ask here or over in the Feather Kit forums.
#include <featherkit/entitysystem.h>
#include <featherkit/util/entity/basictypeadder.h>
#include <featherkit/util/entity/glmtypeadder.h>
#include <featherkit/messaging.h>
#include <iostream>
#include <glm/glm.hpp>
FEA_DECLARE_MESSAGE(EntityDamagedMessage, fea::EntityId, int32_t); //message to sent when an entity is damaged
FEA_DECLARE_MESSAGE(EntityDiedMessage, fea::EntityId); //message is sent when an entity has died
FEA_DECLARE_MESSAGE(EntityMovedMessage, fea::EntityId, const glm::vec2&); //message is sent when an entity has moved
FEA_DECLARE_MESSAGE(FrameMessage); //message is sent once every frame
//the physics component simulates physics on physics entities. if entities have fallen further than 300 units, they start taking 5 damage every frame
class PhysicsComponent : public fea::EntityComponent,
public FrameMessageReceiver
{
public:
PhysicsComponent(fea::MessageBus& bus) : mBus(bus)
{
//subscribe to frame messages to be able to simulate one physics step every frame
mBus.addSubscriber<FrameMessage>(*this);
}
~PhysicsComponent()
{
mBus.removeSubscriber<FrameMessage>(*this);
}
bool keepEntity(fea::WeakEntityPtr e) const override
{
fea::EntityPtr entity = e.lock();
//only simulate on physics entities
return entity->hasAttribute("position") &&
entity->hasAttribute("velocity") &&
entity->hasAttribute("acceleration");
}
void handleMessage(const FrameMessage& message) override
{
(void)message; //supress warning
for(auto entityIterator : mEntities)
{
fea::EntityPtr entity = entityIterator.second.lock();
//dumb physics simulation
entity->addToAttribute("velocity", entity->getAttribute<glm::vec2>("acceleration"));
entity->addToAttribute("position", entity->getAttribute<glm::vec2>("velocity"));
const glm::vec2& position = entity->getAttribute<glm::vec2>("position");
//notify that an entity has moved
mBus.send(EntityMovedMessage(entity->getId(), position));
//apply damage if further down than 300
if(position.y > 300.0f)
{
mBus.send(EntityDamagedMessage(entity->getId(), 5));
}
}
}
private:
fea::MessageBus& mBus;
};
//the health component lets entities with health be able to take damage and die
class HealthComponent : public fea::EntityComponent,
public EntityDamagedMessageReceiver
{
public:
HealthComponent(fea::MessageBus& bus) : mBus(bus)
{
//subscribe to know when entities are damaged
bus.addSubscriber<EntityDamagedMessage>(*this);
}
~HealthComponent()
{
mBus.removeSubscriber<EntityDamagedMessage>(*this);
}
bool keepEntity(fea::WeakEntityPtr e) const override
{
fea::EntityPtr entity = e.lock();
//only deal with entities that have health and can be dead/alive
return entity->hasAttribute("health") &&
entity->hasAttribute("alive");
}
//subtract health according to damage, and kill entity if health goes to zero or below
void handleMessage(const EntityDamagedMessage& message)
{
fea::EntityId id;
int32_t damageAmount;
std::tie(id, damageAmount) = message.mData;
if(mEntities.find(id) != mEntities.end())
{
fea::EntityPtr entity = mEntities.at(id).lock();
int32_t health = entity->getAttribute<int32_t>("health");
health -= damageAmount;
entity->setAttribute("health", health);
//if dead, notify about the death of the entity
if(health <= 0)
{
entity->setAttribute("alive", false);
mBus.send(EntityDiedMessage(entity->getId()));
}
}
}
private:
fea::MessageBus& mBus;
};
//simple class to print what is happening in the world
class Logger
: public EntityDiedMessageReceiver,
public EntityMovedMessageReceiver
{
public:
Logger(fea::MessageBus& bus) : mBus(bus)
{
//subscribe to the messages we are interested to print information about
bus.addSubscriber<EntityDiedMessage>(*this);
bus.addSubscriber<EntityMovedMessage>(*this);
}
~Logger()
{
mBus.removeSubscriber<EntityDiedMessage>(*this);
mBus.removeSubscriber<EntityMovedMessage>(*this);
}
//print when an entity has died
void handleMessage(const EntityDiedMessage& message) override
{
fea::EntityId id = std::get<0>(message.mData);
std::cout << "Entity id " << id << " died!\n";
};
//print when an entity has moved
void handleMessage(const EntityMovedMessage& message) override
{
fea::EntityId id = std::get<0>(message.mData);
const glm::vec2& position = std::get<1>(message.mData);
std::cout << "Entity id " << id << " moved to " << position.x << " " << position.y << "\n";
};
private:
fea::MessageBus& mBus;
};
int main()
{
fea::MessageBus bus;
Logger logger(bus);
fea::EntityManager manager;
fea::EntityFactory factory(manager);
//use utility functions to register common data types like bool, int32 and vec2
fea::util::addBasicDataTypes(factory);
fea::util::addGlmDataTypes(factory);
//register all attributes that our entities will use
factory.registerAttribute("position", "vec2");
factory.registerAttribute("velocity", "vec2");
factory.registerAttribute("acceleration", "vec2");
factory.registerAttribute("health", "int32");
factory.registerAttribute("alive", "bool");
factory.registerAttribute("hardness", "int32");
//setup the physics entity template. physics simulation will be done on these
fea::EntityTemplate physicsEntityTemplate;
physicsEntityTemplate.mAttributes = {{"position" , "0.0f, 0.0f" },
{"velocity" , "0.0f, 0.0f" },
{"acceleration" , "0.0f, 0.8f"}}; //default gravity
factory.addTemplate("physics_entity", physicsEntityTemplate);
//setup the living entity template. these entities will be able to take damage and die
fea::EntityTemplate livingEntityTemplate;
livingEntityTemplate.mAttributes = {{"health" , "20"}, //default to 20 HP
{"alive" , "true"}}; //default to be alive
factory.addTemplate("living_entity", livingEntityTemplate);
//the rock template will inherit the physics entity but will also have a hardness attribute
fea::EntityTemplate rockTemplate;
rockTemplate.mInherits = {"physics_entity"};
rockTemplate.mAttributes = {{"hardness" , "5"}};
factory.addTemplate("rock", rockTemplate);
//the enemy template will inherit both the physics entity and the living entity. so it will both be physics simulated and able to take damage
fea::EntityTemplate enemyTemplate;
enemyTemplate.mInherits = {"living_entity", "physics_entity"};
factory.addTemplate("enemy", enemyTemplate);
//store our entity components in a list
std::vector<std::unique_ptr<fea::EntityComponent>> components;
components.push_back(std::unique_ptr<PhysicsComponent>(new PhysicsComponent(bus)));
components.push_back(std::unique_ptr<HealthComponent>(new HealthComponent(bus)));
//create our entity instances. 2 rocks and 2 enemies. we also change the default attributes for some of them for variation
factory.instantiate("rock").lock()->setAttribute("position", glm::vec2(50.0f, 50.0f));
factory.instantiate("rock").lock()->setAttribute("position", glm::vec2(20.0f, 500.0f));
factory.instantiate("enemy");
factory.instantiate("enemy").lock()->setAttribute("position", glm::vec2(100.0f, 100.0f));
//get all our created entities and pass them to the components. if they have the right attributes, the components will store them
for(auto entity : manager.getAll())
{
for(auto& component : components)
component->entityCreated(entity);
} //(this can be done in a better way but i wanted to keep it simple)
int32_t counter = 0;
//our main loop which will terminate after 30 frames
while(counter < 30)
{
//notify that a frame has passed
bus.send(FrameMessage());
//go through all entities and look for dead ones. these will be removed
for(auto e : manager.getAll())
{
fea::EntityPtr entity = e.lock();
if(entity->hasAttribute("alive"))
{
//if an entity is dead, tell all components to remove it if they have it, and finally remove it from the entity manager
if(!entity->getAttribute<bool>("alive"))
{
for(auto& component : components)
component->entityRemoved(entity->getId());
manager.removeEntity(entity->getId());
}
}
}
counter++;
}
}