Jump to content

  • Log In with Google      Sign In   
  • Create Account





My Component Base Entity System, Part 1

Posted by BeerNutts, 22 December 2011 · 3,547 views

As I mentioned in my last post, I become interested in Component Base Entity systems, and I decided to port my SmashPC game over to a component-based system.

Well, the results are mixed. I love the idea, and, I think, for certain game types, it's essential; however, I'm not sure it's totally worth it for a simple game like SmashPC. I'll experiment some more as I move onto my next game, and see if I can re-use much of what I've done for other games.

And, I'll also add, being my 1st ever System of this type, I realize it is lacking in many ways, but, I hope the thoughts here can help others build a better system.

Anyway, onto the interesting stuff. Let me start by saying I have some obvious efficiency issues with how I'm approaching entities, components, and the events that communicate between them. Using strings is a BAD way of filtering some events, but, I'm going to use them until I see an obvious performance hit as it's so much easier for my simple games. So, beware if you use this system in a large project.

Here's a link to my component system library I use as the basis for the work:
ComponentSystem-12-23-11


First, the Component Class (include file)

/******************************************************************
*
*  Component.h - Generic Component class
*
*******************************************************************/
#include <string>

class TEntity;

enum etComponentType
{
    TIMER_COMPONENT = 1,
    HEALTH_COMPONENT,
    ARMOR_COMPONENT,
    VALUE_COMPONENT,
    CAMERA_COMPONENT,
    ENEMY_LOGIC_COMPONENT,
    PLAYER_LOGIC_COMPONENT,
    ENEMY_SPAWN_COMPONENT,
    INPUT_COMPONENT,
    PHYSICAL_OBJECT_COMPONENT,
    GRAPHICS_OBJECT_COMPONENT,
    WEAPONRY_COMPONENT,
    GRAPHICS_SHAPE_COMPONENT,
    SOUND_COMPONENT,
    MAX_COMPONENT
};

class TComponent
{
public:
    TComponent(etComponentType eComponentType);
    ~TComponent();

    void SetOwner(TEntity *pNewOwner) { mpOwnerEntity = pNewOwner; }

    virtual TEntity *GetOwner();

    virtual etComponentType GetType();
    virtual void Initialize() = 0;
    virtual void Cleanup() = 0;

protected:
    TEntity *mpOwnerEntity;
    etComponentType meComponentType;

};

This Component class is just a base class for all the specific components. No-one should invoke the TComponent class directly. The idea is a Component knows it's Entity Owner, it's Compnent Type, and it has a generic Initialize and Cleanup. Notice there aren't any template references here; Your implementation could (and, possibly, should) have them.

The TComponent.cpp file is also quite small

TComponent::TComponent(etComponentType eComponentType)
{
    meComponentType = eComponentType;
    mpOwnerEntity = NULL;
}

TComponent::~TComponent()
{
    //printf("Delete Component %s\n", mType.c_str());

}

TEntity *TComponent::GetOwner()
{
    return mpOwnerEntity;
}

etComponentType TComponent::GetType()
{
    return meComponentType;
}
Just some simple retrieval functions.

Before we get to the Entity, I should introduce Events. In my system, Events are what is used to communicate between Components and Entities. Two examples would be
#1, the Player Logic Component needs to know when the user presses the "move left" key, so it registers for an "INPUT_EVENT" and it will give the player a velocity when it gets a left (or, more specifically, it will give the Player's Physical Object Component a Velocity).
#2, the Enemy Logic component needs to know where the Player is, so it know where to aim it's bullets. So, when it's refire timer expires (notifying it can shoot a bullet), it will ask the Entity Manager to send an event to the Player Entity and get it's Physical Location.

Here's the Event Header:

typedef enum
{
    COLLISION_EVENT,
    TIMER_EXPIRED_EVENT,
    FRAME_UPDATE_EVENT,
    INPUT_EVENT,
    BULLET_FIRED_EVENT,
    DEATH_EVENT,
    ADDED_ENTITY_EVENT,
    EVENT_DATA_EVENT,
    ADD_ENTITY_EVENT,
    REMOVE_ENTITY_EVENT,

} etEventType;

class TEvent
{
public:

    TEvent(etEventType EventType, TEntity *pEntity, std::string Filter = "");
    TEvent(etEventType EventType, TComponent *pComponent, std::string Filter = "");
    ~TEvent();

    etEventType GetType();
    std::string GetFilter();

    void SetData(void *pEventData) { mpEventData = pEventData; }
    void *GetData(void ) { return mpEventData; }

    TEntity *GetEntity();
    TComponent *GetComponent();

private:

    etEventType mType;
    TEntity *mpEntity;
    TComponent *mpComponent;
    std::string mFilter;

    void *mpEventData;

};

typedef void (*TEventCB)(TEvent &, void *);
An Event can be tied to a Component or an Entity, depending on where it is created.

The Filter is used to filter out different types of events.

SetData() is used to store the data that will be written to for certain events (like the before mentioned Player Location) and GetData() is used to get the location for where to store that data. Now, I'm using void * for this, and I'm not real happy about it. I probably should have used templates in this case, but, my familiarity with void *'s make it easier for me to do. I do come from a strictly-C background.

TEven.cpp is exclusively setting and returning values, so I won't list it here. It probably could have all been done in the header file.

Next, we'll look at our Entity class. An Entity is comprised of Components, and it also keeps track of many of the events certain components want to be notified of. More on that soon. First, the header file:

    // a map using the type of event as the key, and the value
    // is a list of pairs which could have a sub-type it is filtering on
    // and a event handler
    struct TEventCbInfo
    {
        TEntity *pEntity;
        TEventCB EventCB;
        void *pThis;
    };

    typedef std::vector<std::pair<etEventType, TEventCbInfo> > TRegisteredEvents;

class TEntity
{
public:

    enum TUpdateOrder
    {
        UPDATE_EARLY,
        UPDATE_MID,
        UPDATE_LAST
    };

    TEntity(std::string EntityType, TUpdateOrder UpdateOrder = UPDATE_MID);
    ~TEntity();

    void Initialize();

    void AddComponent(TComponent *pComponent);
    void RemoveComponent(TComponent *pComponent);


    std::string GetType();

    TComponent *GetComponent(etComponentType eComponentType);

    void SetEvent(TEvent &Event);

    void RegisterForEvent(TEventCB EventCB, void *pThis,
                          etEventType EventType);
    void UnRegisterForEvent(TEventCB EventCB, void *pThis,
                            etEventType EventType);

    void SetDead(void) { mbDead = true; }
    bool IsDead(void) { return mbDead; }
    uint32_t GetId() { return mId; }
    TUpdateOrder GetUpdateOrder() { return mUpdateOrder; }

private:
    std::string mType;
    uint32_t mId;
    bool mbDead;
    TUpdateOrder mUpdateOrder;

    TRegisteredEvents mRegisteredEvents;
    std::list<TComponent *> mComponents;
    std::list<TRegisteredEvents::iterator> mRegisteredEventsToRemove;

};
First, you'll notice we define TEventCbInfo outside the class; this is because the EntityManager will also use this structure (since things can register for event callbacks with the whole system, and not just a single enity).

The enum TUpdateOrder, mainly helps detail when entities should be updated. For example, you typically want your player to be drawn last, so he doesn't run "under" a health box, (if he's at 100% health), and you probably don't want any "decoration" items to be drawn over enemies, etc.

Add and Remove Components is useful if you want to move some components out of one entity to another. I use it when a player dies. I remove the Weaponry Component (which knows which weapons the player has), delete the dead player entity, and create a new entity with the previous Weaponry Component (so he keeps his weapons). Another example could be to move the Camera Component from the player to a bullet, if the bullet could be controllable by the player, kind of like how the unreal controllable rocket is. That way, the camera follows the bullet, not the player, for a length of time.

The big piece here is the Event system. SetEvent() basically sets an event for that entity, and any components who's registered for that event will get notified. RegisterForEvent() tells the entity that a component wants to be notified of certain events, and UnRegisterForEvent() removes previously registered event.

Here's the TEntity.cpp code to check out:

TEntity::TEntity(std::string EntityType, TUpdateOrder UpdateOrder)
{
    mUpdateOrder = UpdateOrder;

#ifdef EVENT_DEBUG
    if (fEventDebug == NULL) {
        fEventDebug = fopen("EventDebug.log", "w");
        if (fEventDebug == NULL) {
            printf("Failed opening %s for debug!!\n", "EventDebug.log");
        }
    }
#endif
    mType = EntityType;
    mId = TEntityManager::GetInstance()->GetUniqueId();
    mbDead = false;
}

TEntity::~TEntity()
{
#ifdef EVENT_DEBUG
    fprintf(fEventDebug, "DelEn %s this %p\n", GetType().c_str(), this);
    fflush(fEventDebug);
#endif

    // remove all registered events and component
    std::list<TComponent *>::iterator CompIt;
    for (CompIt = mComponents.begin();
     	CompIt != mComponents.end(); CompIt++) {
        (*CompIt)->Cleanup();
        delete (*CompIt);
    }
    mComponents.clear();

    mRegisteredEvents.clear();
}

void TEntity::Initialize()
{
    // initilize all the components
    std::list<TComponent *>::iterator CompIt;
    for (CompIt = mComponents.begin();
     	CompIt != mComponents.end(); CompIt++) {
        (*CompIt)->Initialize();
    }
}

void TEntity::AddComponent(TComponent *pComponent)
{
    pComponent->SetOwner(this);
    //printf("Adding Component %s\n", pComponent->GetType().c_str());
    mComponents.push_back(pComponent);

}

void TEntity::RemoveComponent(TComponent *pComponent)
{
    printf("TEntity::RemoveComponent Ent %s, C%d\n",
       	GetType().c_str(), pComponent->GetType());

    std::list<TComponent *>::iterator it;
    for (it = mComponents.begin();
     	it != mComponents.end(); it++) {
        if ((*it)->GetType() == pComponent->GetType()) {
            mComponents.erase(it);
            break;
        }
    }
}


void TEntity::SetEvent(TEvent &Event)
{
    // If it's a remove event, delete us
    if (Event.GetType() == REMOVE_ENTITY_EVENT) {
        DEBUG_EVENT(Event, GetType().c_str(), NULL);
        TEntityManager::GetInstance()->DeleteEntity(this);
        mbDead = true;
    }
    else if (!mbDead) {

        if ((Event.GetType() == FRAME_UPDATE_EVENT) && !mRegisteredEventsToRemove.empty()) {
            printf("On Tick, remove registered event\n");
            mRegisteredEvents.erase(*(mRegisteredEventsToRemove.begin()));
            mRegisteredEventsToRemove.erase(mRegisteredEventsToRemove.begin());
        }
        // Find all components who register for this event

        // ensure there's someone listen for this event ??
        TRegisteredEvents::iterator it;
        for (it = mRegisteredEvents.begin(); it != mRegisteredEvents.end(); it++) {
            if (it->first == Event.GetType()) {
                if (GetType() != "Wall") DEBUG_EVENT(Event, GetType().c_str(), it->second.pThis);
                it->second.EventCB(Event, it->second.pThis);
            }
        }
    }

    // Do I need to register with any system, saying we're waiting for
    // certain events? I think so
}

std::string TEntity::GetType()
{
    return mType;
}

TComponent *TEntity::GetComponent(etComponentType eComponentType)
{
    TComponent *pComp = NULL;
    std::list<TComponent *>::iterator it;
    for (it = mComponents.begin();
     	it != mComponents.end(); it++) {
        if ((*it)->GetType() == eComponentType) {
            pComp = (*it);
            break;
        }
    }

    return pComp;
}

void TEntity::RegisterForEvent(TEventCB EventCB, void *pThis,
                           	etEventType EventType)
{
    TEventCbInfo EventCbInfo;
    EventCbInfo.EventCB = EventCB;
    EventCbInfo.pThis = pThis;
    EventCbInfo.pEntity = this;

    std::pair<etEventType, TEventCbInfo> EventPair(EventType, EventCbInfo);
    mRegisteredEvents.push_back(EventPair);
}

void TEntity::UnRegisterForEvent(TEventCB EventCB, void *pThis,
                        etEventType EventType)
{
    TRegisteredEvents::iterator it;
    for (it = mRegisteredEvents.begin(); it != mRegisteredEvents.end(); it++) {
        if (it->first == EventType) {
            if ((it->second.EventCB == EventCB) &&
                (it->second.pThis == pThis)) {
                mRegisteredEventsToRemove.push_back(it);
                break;
            }
        }
    }
}

Finally, before I bore you to death, here's the EntityManager class:

class TEntityManager
{
public:

    static TEntityManager *GetInstance();

    TEntityManager();
    ~TEntityManager();

    void AddEntity(TEntity *pEntity,
               	bool bImmediate = false);
    void RemoveEntity(TEntity *pEntity);
    void DeleteEntity(TEntity *pEntity, bool bImmediate = false);

    void RemoveAllEntities();

    TEntity *GetEntity(std::string EntityType);

    void GetEntities(std::list<TEntity *> &Entities);

    void SetEvent(TEvent &Event, bool bNotifyEntities = false);

    void RegisterForEvent(TEventCB EventCB, void *pThis,
                          etEventType EventType,
                          TEntity *pEntity = NULL);

    void UnRegisterForEvent(TEventCB EventCB, void *pThis,
                            etEventType EventType);

    uint32_t GetUniqueId() { return mUniqueId++; }

private:

    TRegisteredEvents mRegisteredEvents;

    std::list<TEntity *> mEntities;
    std::list<TEntity *> mEntityDeletions;
    std::list<TEntity *> mEntityAdditions;

    uint32_t mUniqueId;
};
The idea here is Entity Manager holds all the entities in the system, and allows entities and components to set and register for system-wide events. It's the interface for adding and removing entities to the system as well. It is a singleton, and can be accessed via the TEntityManager::GetInstance() call.

In the AddEntity() and DeleteEntity() calls, we have a bImmediate parameter. This basically allows the EntityManager to Queue up Entities to add or remove until the next system tick; otherwise, an entity might get removed while the EntityManager is looping through all the entities, and it could corrupt the list.

Here's the .cpp code for it:

static TEntityManager *pEntityManager = NULL;

TEntityManager *TEntityManager::GetInstance()
{
    if (pEntityManager == NULL) {
        pEntityManager = new TEntityManager();
    }

    return pEntityManager;
}

TEntityManager::TEntityManager()
{
    mUniqueId = 1;
}

TEntityManager::~TEntityManager()
{
    // remove all register events and components
    mRegisteredEvents.clear();

    std::list<TEntity *>::iterator it;
    for (it = mEntities.begin();
     	it != mEntities.end(); it++) {
        delete (*it);
    }
}

void TEntityManager::RemoveAllEntities()
{
    // remove all register events and components
    mRegisteredEvents.clear();

    std::list<TEntity *>::iterator it;
    for (it = mEntities.begin();
     	it != mEntities.end(); it++) {
        delete (*it);
    }
    mEntities.clear();

    mEntityDeletions.clear();
    mEntityAdditions.clear();

}

void TEntityManager::AddEntity(TEntity *pEntity,
                           	bool bImmediate)
{
    DEBUG_ADD_ENTITY(pEntity, bImmediate);
    if (bImmediate) {
        pEntity->Initialize();

        // Send event notify this entity has been added
        TEvent Event(ADDED_ENTITY_EVENT, pEntity);
        SetEvent(Event, true);
        //printf("Added Entity %s, %p\n", pEntity->GetType().c_str(), pEntity);

        // Add entities in proper update order
        std::list<TEntity *>::iterator it;
        for (it = mEntities.begin();
         	it != mEntities.end(); it++) {
            if ((*it)->GetUpdateOrder() > pEntity->GetUpdateOrder()) {
                if (it == mEntities.begin()) {
                    mEntities.push_front(pEntity);
                }
                else {
                    mEntities.insert(--it, pEntity);
                }
                break;
            }
        }
        if (mEntities.end() == it) {
            mEntities.push_back(pEntity);
        }
        //mEntities.push_back(pEntity);
    }
    else {
        mEntityAdditions.push_back(pEntity);
    }
}

void TEntityManager::RemoveEntity(TEntity *pEntity)
{
    std::list<TEntity *>::iterator it;
    for (it = mEntities.begin();
     	it != mEntities.end(); it++) {
        if (pEntity == *it) {
            mEntities.erase(it);
            break;
        }
    }
}

void TEntityManager::DeleteEntity(TEntity *pEntity, bool bImmediate)
{
    DEBUG_DEL_ENTITY(pEntity, bImmediate);

    if (bImmediate) {
        TRegisteredEvents::iterator it;

        // Find the Events registered to this entity, or entity's components
        // and UnRegister
        for (it = mRegisteredEvents.begin(); it != mRegisteredEvents.end(); ) {
            if (it->second.pEntity == pEntity) {
                mRegisteredEvents.erase(it);

                if (it == mRegisteredEvents.end())
                {
                    break;
                }
            }
            else {
                it++;
            }
        }

        RemoveEntity(pEntity);
        delete pEntity;
    }
    else {
        if (!pEntity->IsDead()) {
            mEntityDeletions.push_back(pEntity);
            pEntity->SetDead();
        }
    }
}

TEntity *TEntityManager::GetEntity(std::string EntityType)
{
    TEntity *pEntity = NULL;
    std::list<TEntity *>::iterator it;
    for (it = mEntities.begin();
     	it != mEntities.end(); it++) {
        if (EntityType == (*it)->GetType()) {
            pEntity = *it;
            break;
        }
    }

    return pEntity;
}

void TEntityManager::GetEntities(std::list<TEntity *> &Entities)
{
    Entities = mEntities;
}

void TEntityManager::SetEvent(TEvent &Event, bool bNotifyEntities)
{
    // Check for update event, and delete entities in list
    if (Event.GetType() == FRAME_UPDATE_EVENT) {
        RESET_EVENTS();

        for (std::list<TEntity *>::iterator it2 = mEntityAdditions.begin();
         	it2 != mEntityAdditions.end(); it2++) {
            AddEntity(*it2, true);
        }
        mEntityAdditions.clear();

        for (std::list<TEntity *>::iterator it = mEntityDeletions.begin();
         	it != mEntityDeletions.end(); it++) {
            DeleteEntity(*it, true);
        }
        mEntityDeletions.clear();
    }

    // ensure there's someone listen for this event ??

    TRegisteredEvents::iterator it;
    for (it = mRegisteredEvents.begin(); it != mRegisteredEvents.end(); it++) {
        if (it->first == Event.GetType()) {
            DEBUG_EVENT_MGR2(Event);
            it->second.EventCB(Event, it->second.pThis);
        }
    }

    if (bNotifyEntities) {
        // tell all the entites about this
        std::list<TEntity *>::iterator it2;

        for (it2 = mEntities.begin();
         	it2 != mEntities.end(); it2++) {
#ifdef EVENT_DEBUG
            if ((*it2)->GetType() != "Wall") fprintf(fEventDebug, "EntMgr ->Entity: ");
            fflush(fEventDebug);
#endif
            (*it2)->SetEvent(Event);
        }
    }
}

void TEntityManager::RegisterForEvent(TEventCB EventCB, void *pThis,
                                      etEventType EventType, TEntity *pEntity)
{
    TEventCbInfo EventCbInfo;
    EventCbInfo.EventCB = EventCB;
    EventCbInfo.pThis = pThis;
    EventCbInfo.pEntity = pEntity;

    std::pair<etEventType, TEventCbInfo> EventPair(EventType, EventCbInfo);
    mRegisteredEvents.push_back(EventPair);
}

void TEntityManager::UnRegisterForEvent(TEventCB EventCB, void *pThis,
                        etEventType EventType)
{
    TRegisteredEvents::iterator it;
    for (it = mRegisteredEvents.begin(); it != mRegisteredEvents.end(); it++) {
        printf("EventType %d, if->first %d\n", EventType, it->first);
        if (it->first == EventType) {
            if ((it->second.EventCB == EventCB) &&
                (it->second.pThis == pThis)) {
                    printf("\n\nAGGGG! removing System Event, not safely!!!\n\n");
                mRegisteredEvents.erase(it);
                break;
            }
        }
    }
}

So, that's the basis for my system. There are some obvious faults with it, not least of which being my games probably don't need this, and could be over-kill. But, it's been a really good experience so far, and I'm going to be curious how it will help me in future games.

Next time I'll get into specific Components I use for SmashPC, and their interactions.

Remeber, you can see my old blog here: 2dGameMaking It has the details about the SmashPC game.




October 2014 »

S M T W T F S
   1234
567891011
12131415161718
19202122232425
2627282930 31  
PARTNERS