• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
  • entries
    11
  • comments
    15
  • views
    49790

My Component Base Entity System, Part 1

Sign in to follow this  
Followers 0
BeerNutts

4403 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

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 > 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 mComponents;
std::list 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::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::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::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::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 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 &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 mEntities;
std::list mEntityDeletions;
std::list 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::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::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::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::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::iterator it;
for (it = mEntities.begin();
it != mEntities.end(); it++) {
if (EntityType == (*it)->GetType()) {
pEntity = *it;
break;
}
}

return pEntity;
}

void TEntityManager::GetEntities(std::list &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::iterator it2 = mEntityAdditions.begin();
it2 != mEntityAdditions.end(); it2++) {
AddEntity(*it2, true);
}
mEntityAdditions.clear();

for (std::list::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::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 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.

2
Sign in to follow this  
Followers 0


0 Comments


There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now