Component-based Entities and how they communicate

A forum to store posts deemed exceptionally wise and useful
Trefall
Posts: 45
Joined: Tue Dec 05, 2006 8:49 pm

Component-based Entities and how they communicate

Post by Trefall »

Component-based Entities and how they communicate

ENTIERLY NEW APPROACH TO COMPONENTS POSTED IN CODE SNIPPETS FORUM
http://irrlicht.sourceforge.net/phpBB2/ ... hp?t=35666

Prerequisites:
- Singleton pattern
http://www.inquiry.com/techtips/cpp_pro ... in0200.asp
- Decorator pattern / Component pattern
http://en.wikipedia.org/wiki/Decorator_pattern
- Observer pattern
http://en.wikipedia.org/wiki/Observer_pattern

Third party dependencies:
- Sigslot event system
http://sigslot.sourceforge.net/

Lately I've been working on modular entities and to prevent deep hierarchies. I stumbled over the component based design in some of the Game Programming Gems books, and thought I'd give it a try. It turned out great for myself and my game's framework.

A component-based approach makes entities data-driven, which means that entities can be described in feks. XML. It's less prone to difficulties during optimization stages than the deep hierarchy approach, and you split up entity functionality in logical and manageable chunks of code.

In my approach, I decided to go with the observer pattern for communication. I wanted to use this approach to prevent many to many relationships where it could be helped, lessening unnecessary recompilation of files when coding and, in my opinion, cleaning up code and making it all more logical to interpret.

Thus the following HOWTO describes the approach for a component based entity system using sigslot for communication. Please note that the code here won't work off the bat, you can download a complete codebase from my game's repository over at sourceforge here:
http://sourceforge.net/projects/ravinescrpg/



Let's first start with the ComponentContainer. This is a baseclass who's only functionality is to hold on to a collection of components.

ComponentContainer.h

Code: Select all

#ifndef COMPONENTCONTAINER_H
#define COMPONENTCONTAINER_H

#include "../Managers/GameManager.h"
#include <sigslot/sigslot.h>
#include <map>
class Component;

class ComponentContainer
{
public:
	void addComponent(const char* name, Component* component);
	Component* getComponent(const char* name) const;

	const size_t newComponentID() { return _componentIDs++; }
	const size_t lastComponentID() const { return _componentIDs; }

protected:
	ComponentContainer(const size_t entityID);
	virtual ~ComponentContainer();

private:
	std::map<const char*, Component*> _componentContainer;
	const size_t _entityID;
	size_t _componentIDs;
};

#endif
ComponentContainer.cpp

Code: Select all

#include "ComponentContainer.h"
#include "Component.h"
#include "HealthComponent.h"
#include "AnimatedNodeComponent.h"
#include <iostream>

ComponentContainer::ComponentContainer(const size_t entityID)
: _entityID(entityID), _componentIDs(0)
{
}

ComponentContainer::~ComponentContainer() 
{
	std::map<const char*, Component*>::iterator iter = _componentContainer.begin();
	for(; iter != _componentContainer.end(); ++iter)
	{
		delete iter->second;
	}
}

void ComponentContainer::addComponent(const char* name, Component* component) 
{
	if(_componentContainer.find(name) != _componentContainer.end())
	{
		delete _componentContainer.find(name)->second;
		_componentContainer.find(name)->second = component;
	}
	else
	{
		_componentContainer[name] = component; 
	}
}

Component* ComponentContainer::getComponent(const char* name) const 
{
	if(!this)
		return 0;
	if(_componentContainer.find(name) != _componentContainer.end())
		return _componentContainer.find(name)->second; 
	else
	{
		std::cout << "Could not find component " << name << std::endl;
		return 0;
	}
}
From this class, we'll mostly be using addComponent() which adds a new component to the component container map, and newComponentID() which ensures every component of the container has a unique id.

Note that components are stored into the map with const char* as key. We can do this because there will only be one instance of any component inside the container.

Let's now move on to the Entity class. The entity class is a ComponentContainer with a unique id.

Entity.h

Code: Select all

#ifndef ENTITY_H
#define ENTITY_H

#include "../Managers/GameManager.h"
#include "../Managers/ActionManager.h"
#include "../Managers/EntityManager.h"
#include "../Components/ComponentContainer.h"

class Entity : public ComponentContainer, public sigslot::has_slots<>
{
	friend class EntityManager;
public:
	virtual ~Entity();

	virtual void initialize() {}

	const unsigned int getID() const { return _entityID; }
	const char* getName() const { return _name; }
	bool isAlive() const { return _isAlive; }
	void setAlive(bool alive = true) { _isAlive = alive; }

protected:
	Entity(const size_t id, const char* name);

	//Actions
	void onUpdate(void* fDeltaTime, const size_t id);

	const size_t _entityID;
	const char* _name;
	bool _isAlive;
	
};

#endif
Entity.cpp

Code: Select all

#include "Entity.h"
#include <iostream>

Entity::Entity(const size_t id, const char* name) 
: ComponentContainer(id), _entityID(id), _name(name), _isAlive(true)
{
	ActionManager::ptr()->getCoreAction("OnUpdate")->connect(this, &Entity::onUpdate);
}

Entity::~Entity()
{
	this->disconnect_all();
}

void Entity::onUpdate(void* fDeltaTime, const size_t id) 
{ 
	float dt = ActionManager::ptr()->toFloat(fDeltaTime);
	//std::cout << _name << " was updated" << std::endl;
	ActionManager::ptr()->emitIngameAction(_entityID, "OnUpdate", &dt, _entityID);
	static float tot = 0.0f;
	if(tot > 150.0f)
	{
		ActionManager::ptr()->emitIngameAction(_entityID, "OnDying", " ", _entityID);
	}
	tot+=dt;
	if(_entityID == 101)
		std::cout << tot << std::endl;
}
An Entity is the shell of an object in our scene. Note also that our entity constructor is protected, and that we befriend EntityManager in the header. This is to use the Factory pattern for constructing entities, but more on this later when we get to the EntityManager.

Note that here's also a first example of how we use the observer pattern, using the Actionmanager. Here we have a functor onUpdate, which on construction is said to be the reaction to the event "OnUpdate" for the object Entity.

In the onUpdate functor, we tell all the components of this entity to update (if they are listening to the ingame update event, that is).

In addition, we're counting time here, saying that when the variable tot reaches 150, we'll emit the ingame action event "OnDying".

We'll dig deeper into the ActionManager and events later in this HOWTO, but for now, let's be content with what I've said and move on.


Let's take a look at the Component baseclass now. This is the base for all components which describes an entity.

Component.h

Code: Select all

#ifndef COMPONENT_H
#define COMPONENT_H

#include <sigslot/sigslot.h>

class Component : public sigslot::has_slots<>
{
public:
	Component(const size_t entityID) : _name(0), _compID(0), _entityID(entityID) {}
	virtual ~Component() {}
	virtual const char* getName() const { return "N/A"; }
	const size_t getID() const { return _compID; };

protected:
	char* _name;
	size_t _compID;
	const size_t _entityID;
};

#endif
A base component simply holds the id of it's owner (entityID), it's own component ID, as well as it's own name. See how it also inherits from sigslot's has_slots, which indicates that this object can subscribe to events.


Before we move on to any examples for components, I'd like us to take a look at the different managers we have in place to manage this whole thing. I like to use managers to kind of have a mainframe of functionality that I can access from any other point in my project. It creates obvious dependency chains, as I try to link most of the communication in my code through the managers, which is clean and easy to maintain. For this HOWTO, I'll give example code to three managers:
- GameManager, ActionManager and EntityManager.

Why do I call it ActionManager instead of EventManager? It's quite simple, I didn't want it to intervene with Irrlicht's event system, thus it's easy to add in Irrlicht's event handling for input and gui, while the entities are handled via actions.

Let's start with the EntityManager.

EntityManager.h

Code: Select all

#ifndef ENTITYMANAGER_H
#define ENTITYMANAGER_H

#include "../Managers/GameManager.h"
#include "../Entities/Entity.h"
#include <sigslot/sigslot.h>
#include <map>

class Entity;

class EntityManager : public sigslot::has_slots<>, public Singleton<EntityManager>
{
    friend class Singleton<EntityManager>;
public:
	Entity* createEntity( const char* name);
	
	Entity* getEntity( const char* name ) const;
	Entity* getEntity( const size_t id) const;
	
	const size_t getCount() const { return _ents.size(); }

protected:
	EntityManager();
	virtual ~EntityManager();

	//Actions
	void onUpdate(void* fDeltaTime, const size_t senderID);

	std::map<const char*, Entity*> _ents;
};
#endif
EntityManager.cpp

Code: Select all

#include "EntityManager.h"
#include <iostream>

EntityManager::EntityManager() 
{
	//ActionManager::ptr()->getCoreAction("OnUpdate")->connect(this, &EntityManager::onUpdate);
}
EntityManager::~EntityManager() 
{
	this->disconnect_all();
	std::map<const char*, Entity*>::const_iterator it = _ents.begin();
	for(; it != _ents.end(); ++it)
	{
		delete (*it).second;
	}
}

Entity* EntityManager::createEntity( const char* name )
{
	this->_ents[name] = new Entity(GameManager::ptr()->newEntityID(), name);
	return this->_ents.find(name)->second;
}

Entity* EntityManager::getEntity( const char* name ) const 
{ 
	if(_ents.find(name) != _ents.end()) 
		return _ents.find(name)->second;
	else
	{
		std::cerr << "Entity, " << name << ", does not exist!" << std::endl;
		return 0;
	}
}

Entity* EntityManager::getEntity( const size_t id) const 
{ 
	std::map<const char*, Entity*>::const_iterator it = _ents.begin();
	for(; it != _ents.end(); ++it)
	{
		if((*it).second->getID() == id)
		{
			return (*it).second;
		}
	}
	std::cerr << "Entity, with ID " << id << ", does not exist!" << std::endl;
	return NULL;
}

void EntityManager::onUpdate(void* fDeltaTime, const size_t senderID)
{
	//float dt = ActionManager::ptr()->toFloat(fDeltaTime);
	//std::cout << "Updated EntityManager: " << dt << std::endl;
}
Here's a first display of using the Singleton pattern. As I listed it as a prerequisite for this HOWTO, I won't show any example code for the Singleton pattern.

The most interesting part for this manager is the createEntity which takes a name as input. Note how we use the function newEntityID() from GameManager to give the entity a unique id, quite identical to how ComponentContainer holds the newComponentID(). This brings one obvious improvement of our ComponentContainer to mind. We could make it a factory instead of a container...


Let's move on to the ActionManager now. The Action Manager serves as a center point for all sigslot messages, and I must admit, this might be one of the messier classes of my code, so if someone on this forum as any ideas for improving the code and making it easier to read, I'd be very interested in that.

In this HOWTO, there are two maps in use, one that holds on to all core actions, and one who holds on to all ingame actions. The only core action in this code is the OnUpdate between managers and entities, while there are more diverse examples of using the ingame actions.

The difference between a core and an ingame action, is that the ingame action stores an entityID as a first key, and then stores another map as second that holds the name of the action (or the event ID) and then the sigslot signal as second. The core action only has action name as key and stores sigslot signals as second.

The sigslot signal I decided to use is the sigslot::signal2, which allows me to have two parameters inside the signal. This conveniently gives me one spot for data and one spot for passing the id of the entity who invoked the action. This is convenient since I can tell an entity who is attacking it, etc.

Code: Select all

typedef sigslot::signal2<void*, const size_t> actionSignal;
Note that I use a void* as data type, this is so that I can send any type of data. Doing so is considered unsafe of course, because the receiving end has no way of checking which type it needs to cast the void pointer into. Thus it's very important to use good names for the parameter in the functors subscribing to the signal. Like void* fDeltaTime would indicate that this parameter should be cast into a float, or void* caDialog would indicate that this parameter should be cast into a character array.

I decided to add a bunch of conversion functions inside the ActionManager as well, that convert the void* into the type you want, like float toFloat(void* v), etc

Let's take a look at the code:

ActionManager.h

Code: Select all

#ifndef ACTIONMANAGER_H
#define ACTIONMANAGER_H

#include "../Singleton.h"
#include "GameManager.h"
#include <sigslot/sigslot.h>
#include <map>
#include "../Containers/AnimationSequenceContainer.h"

typedef sigslot::signal2<void*, const size_t> actionSignal;

class ActionManager : public Singleton<ActionManager>
{
	friend class Singleton<ActionManager>;
public:
	void emitCoreAction(const char* action, void* ptr, const size_t senderID = 0);
	void emitIOAction(const size_t entityID, const char* action, void* ptr, const size_t senderID = 0);
	void emitIngameAction(const size_t entityID, const char* action, void* ptr, const size_t senderID = 0);

	actionSignal* getCoreAction(const char* action);
	actionSignal* getIOAction(const size_t entityID, const char* action);
	actionSignal* getIngameAction(const size_t entityID, const char* action);

	const bool toBool(void* v)
	{
		return (reinterpret_cast<const bool*>(v))[0];
	}

	const char toChar(void* v)
	{
		return reinterpret_cast<const char*>(v)[0];
	}

	const char* toCharArray(void* v)
	{
		return reinterpret_cast<const char*>(v);
	}

	const short toShort(void* v)
	{
		return reinterpret_cast<const short*>(v)[0];
	}

	const short* toShortArray(void* v)
	{
		return reinterpret_cast<const short*>(v);
	}

	const int toInt(void* v)
	{
		return (reinterpret_cast<const int*>(v))[0];
	}

	const unsigned int toUInt(void *v)
	{
		return (reinterpret_cast<const unsigned int*>(v))[0];
	}

	const float toFloat(void* v)
	{
		return (reinterpret_cast<const float*>(v))[0];
	}

	const double toDouble(void* v)
	{
		return (reinterpret_cast<const double*>(v))[0];
	}

	const Containers::AnimationSequence* toAnimationSequence(void* v)
	{
		return reinterpret_cast<const Containers::AnimationSequence*>(v);
	}

	const core::vector3df to3DVec(void* v)
	{
		return (reinterpret_cast<const core::vector3df*>(v))[0];
	}

	const core::matrix4 to4Matrix(void* v)
	{
		return (reinterpret_cast<const core::matrix4*>(v))[0];
	}

protected:
	ActionManager() {}
	virtual ~ActionManager() {}

private:
	std::map<const char*, actionSignal> _coreAction;
	std::map<const size_t, std::map<const char*, actionSignal>> _ioAction;
	std::map<const size_t, std::map<const char*, actionSignal>> _ingameAction;
};

#endif
ActionManager.cpp

Code: Select all

#include "ActionManager.h"
#include <iostream>

void ActionManager::emitCoreAction(const char* action, void* ptr, const size_t senderID)
{
	if(_coreAction.find(action) != _coreAction.end())
		_coreAction.find(action)->second.emit(ptr, senderID);
	else
		std::cout << action << " does not exist in _coreAction" << std::endl;
}
void ActionManager::emitIngameAction(const size_t entityID, const char* action, void* ptr, const size_t senderID)
{
	if(_ingameAction.find(entityID) != _ingameAction.end())
	{
		std::map<const char*, actionSignal>* actionMap = &_ingameAction.find(entityID)->second;
		if(actionMap->find(action) != actionMap->end())
			actionMap->find(action)->second.emit(ptr, senderID);
		else
		{
			//std::cout << action << " does not exist in _ingameAction" << std::endl;
		}
	}
	else
		std::cout << entityID << " does not exist in _ingameAction" << std::endl;
}
void ActionManager::emitIOAction(const size_t entityID, const char* action, void* ptr, const size_t senderID)
{
	if(_ioAction.find(entityID) != _ioAction.end())
	{
		std::map<const char*, actionSignal>* actionMap = &_ioAction.find(entityID)->second;
		if(actionMap->find(action) != actionMap->end())
			actionMap->find(action)->second.emit(ptr, senderID);
		else
			std::cout << action << " does not exist in _ioAction" << std::endl;
	}
	else
		std::cout << entityID << " does not exist in _ioAction" << std::endl;
}

actionSignal* ActionManager::getCoreAction(const char* action)
{
	if(_coreAction.find(action) != _coreAction.end())
		return &_coreAction.find(action)->second;
	else
	{
		actionSignal sig;
		_coreAction[action] = sig;
		return &_coreAction.find(action)->second;
	}
}
actionSignal* ActionManager::getIOAction(const size_t entityID, const char* action)
{
	if(_ioAction.find(entityID) != _ioAction.end())
	{
		std::map<const char*, actionSignal> &actionMap = _ioAction.find(entityID)->second;
		if(actionMap.find(action) != actionMap.end())
			return &_ioAction.find(entityID)->second.find(action)->second;
		else
		{
			actionSignal sig;
			actionMap[action] = sig;
			return &_ioAction.find(entityID)->second.find(action)->second;
		}
	}
	else
	{
		std::map<const char*, actionSignal> actionMap;
		actionSignal sig;
		actionMap[action] = sig;
		_ioAction[entityID] = actionMap;
		return &_ioAction.find(entityID)->second.find(action)->second;
	}
}
actionSignal* ActionManager::getIngameAction(const size_t entityID, const char* action)
{
	if(_ingameAction.find(entityID) != _ingameAction.end())
	{
		std::map<const char*, actionSignal> &actionMap = _ingameAction.find(entityID)->second;
		if(actionMap.find(action) != actionMap.end())
			return &_ingameAction.find(entityID)->second.find(action)->second;
		else
		{
			actionSignal sig;
			actionMap[action] = sig;
			return &_ingameAction.find(entityID)->second.find(action)->second;
		}
	}
	else
	{
		std::map<const char*, actionSignal> actionMap;
		actionSignal sig;
		actionMap[action] = sig;
		_ingameAction[entityID] = actionMap;
		return &_ingameAction.find(entityID)->second.find(action)->second;
	}
}
Note how I have the function get() instead of connectToAction or whatever. This is because I wanted the ability to connect and disconnect to an event, and didn't really care for dividing this up into two different functions. This does give me access to the entire signal class of the returning signal though, which probably can be used in ways not intended, but as long as the programmer has a clear idea for how to use it, the program should be ok. The Emit functions will fire the event.


Now, let's move on to the GameManager. This is the heart of our game's framework. I use this manager to initialize resource and to update and render every frame. I haven't added a shutdown() function to this manager, even though it would probably be convenient to have it.

GameManager.h

Code: Select all

#ifndef GAMEMANAGER_H
#define GAMEMANAGER_H

#include <Irrlicht/irrlicht.h>
using namespace irr;

#include "../Singleton.h"

class GameManager : public Singleton<GameManager>
{
	friend class Singleton<GameManager>;
public:
	void run();
	IrrlichtDevice* device;
	video::IVideoDriver* driver;
	scene::ISceneManager* smgr;
	gui::IGUIEnvironment* gui;
	const size_t newEntityID() { return _entityIDs++; }
	const size_t lastEntityID() const { return _entityIDs; }
	const size_t getPlayerID() const { return 9999999; }
	
protected:
	GameManager();
	virtual ~GameManager() {}

	void _init();

private:
	size_t _entityIDs;
};

#endif
GameManager.cpp

Code: Select all

#include "GameManager.h"
#include "StateManager.h"
#include "ActionManager.h"
#include "inputManager.h"
#include <iostream>

GameManager::GameManager()
{
	_entityIDs = 100;
}

void GameManager::run()
{
	_init();
	u32 time = 0;
	u32 lastTime = 0;
	float deltaTime = 0.0f;
	float fps = 0.0f;

	while(device->run())
	{
		driver->beginScene(true, true, video::SColor(255, 10, 10, 10));
			fps = (float)driver->getFPS();
			
			time = device->getTimer()->getTime();
			deltaTime = (float)time - (float)lastTime;
			lastTime = time;
			deltaTime *= 0.01f;

			if(fps > 0.0f && deltaTime > 0.0f)
			{
				ActionManager::ptr()->emitCoreAction("OnUpdate", &deltaTime);
			}
			smgr->drawAll();
		driver->endScene();
	}

	device->drop();
}

void GameManager::_init()
{
	device = createDevice( video::EDT_DIRECT3D9, core::dimension2d<s32>(1028, 960), 32, false, false, false, InputManager::ptr());

	driver = device->getVideoDriver();
	smgr = device->getSceneManager();
	gui = device->getGUIEnvironment();

	if(!device->getFileSystem()->addZipFileArchive("../../../Data/Models/IrrMesh.zip"))
		std::cout << "Failed to load zip archive!" << std::endl;
	else
		std::cout << "Loaded zip archive!" << std::endl;
	if(!device->getFileSystem()->addZipFileArchive("../../../Data/Textures/IrrTex.zip"))
		std::cout << "Failed to load zip archive!" << std::endl;
	else
		std::cout << "Loaded zip archive!" << std::endl;

	StateManager::ptr()->init();
}
There's a ton of other examples of how the GameManager works on this forum, so I'm not going to explain this manager much. Just note how we use the ActionManager to update subscribers, instead of using the traversal approach commonly used. This allows me to disconnect entities that I don't want to update, and connect entities that I'd like to be updated. There can be several motivations for doing any of the two. An example can be an entity that died, which no longer need to be updated. Why not disconnect it and save one update call per frame?


Just for those who's not following on the GameManager, here's my main.cpp:

Code: Select all

#include "Managers/GameManager.h"

void main()
{
	GameManager::ptr()->run();
}

We've now gone over the managers I thought was important for this HOWTO. We now have to foundation we need to start taking advantage of component based entities and observer communication. Let's give a couple of examples of components:

The Health Component:

HealthComponent.h

Code: Select all

#ifndef HEALTH_H
#define HEALTH_H

#include "Component.h"
class Entity;

namespace Components
{
	class Health : public Component
	{
	public:
		Health();
		Health(const size_t entityID);
		virtual ~Health();

		virtual const char* getName() const;
		bool isInitialized() const;
		void init(const size_t id, float startHealth, float maxHealth);

		//Useful for such things as level increase, etc
		void setMaxHealth(float maxHealth);
	
	protected:
		//Actions
		void onUpdate(void* fDeltaTime, const size_t senderID);
		void onDamage(void* fDamage, const size_t attackerID);
		void onRegenerate(void* fAmount, const size_t healerID);
		void onDead(void* cVoid, const size_t killerID);
		void onDying(void* cvoid, const size_t killerID);
		//void onWounded(void* cvoid, const size_t attackerID);

		bool _isInitialized;
		float _health;
		float _lastHealth;
		float _maxHealth;
		Entity* _owner;
	};
}

#endif
HealthComponent.cpp

Code: Select all

#include "HealthComponent.h"
#include "../Managers/ActionManager.h"
#include "../Managers/EntityManager.h"
#include "../Entities/Entity.h"
#include <iostream>
using namespace Components;

Health::Health()
: Component(NULL)
{
}

Health::Health(const size_t entityID)
: Component(entityID)
{
	_name = "HP";
	_isInitialized = false;
	_maxHealth = -1;
	_health = _maxHealth;
	_lastHealth = _health;
	_owner = EntityManager::ptr()->getEntity(_entityID);

	//Actions
	//ActionManager::ptr()->getIngameAction(_entityID, "OnUpdate")->connect(this, &Health::onUpdate);
	ActionManager::ptr()->getIngameAction(_entityID, "OnDamage")->connect(this, &Health::onDamage);
	ActionManager::ptr()->getIngameAction(_entityID, "OnRegenerate")->connect(this, &Health::onRegenerate);
	ActionManager::ptr()->getIngameAction(_entityID, "OnDying")->connect(this, &Health::onDying);
	ActionManager::ptr()->getIngameAction(_entityID, "OnDead")->connect(this, &Health::onDead);
}

Health::~Health()
{
	this->disconnect_all();
}

const char* Health::getName() const 
{ 
	return _name; 
}

bool Health::isInitialized() const 
{ 
	return _isInitialized; 
}

void Health::init(const unsigned int id, float startHealth, float maxHealth)
{
	_compID = id;
	_health = startHealth;
	_lastHealth = _health;
	_maxHealth = maxHealth;
	_isInitialized = true;
}

//Useful for such things as level increase, etc
void Health::setMaxHealth(float maxHealth)
{
	if(_isInitialized)
		_maxHealth = maxHealth;
}

void Health::onUpdate(void* fDeltaTime, const size_t senderID)
{
}

void Health::onDamage(void* fDamage, const size_t attackerID)
{
	float damage = ActionManager::ptr()->toFloat(fDamage);
	std::cout << _owner->getName() << " took " << damage << " damage" << std::endl;
}

void Health::onRegenerate(void* fAmount, const size_t healerID)
{
	float amount = ActionManager::ptr()->toFloat(fAmount);
	std::cout << _owner->getName() << " was healed for " << amount << " hp" << std::endl;
}

void Health::onDead(void* cVoid, const size_t killerID)
{
	if(!_owner->isAlive())
	{
		ActionManager::ptr()->getCoreAction("OnUpdate")->disconnect(_owner);
	}
}

void Health::onDying(void* cVoid, const size_t killerID)
{
	if(_owner->isAlive())
	{
		_owner->setAlive(false);
		std::cout << _name << " was killed by " << EntityManager::ptr()->getEntity(killerID)->getName() << std::endl;
		ActionManager::ptr()->emitIngameAction(_entityID, "OnChangeAnimation", "ASEQ_DIE1", _entityID);
		ActionManager::ptr()->getIngameAction(_entityID, "OnDying")->disconnect(_owner);
	}
}
This component monitors an entity's health and handle different actions/events related to health.


The Animated Node component:

AnimatedNodeComponent.h

Code: Select all

#ifndef ANIMATEDNODECOMPONENT_H
#define ANIMATEDNODECOMPONENT_H

#include "Component.h"
#include "../Managers/GameManager.h"
#include "../Containers/AnimationSequenceContainer.h"
#include <map>

class Entity;

namespace Components
{
	class AnimatedNode : public Component
	{
	public:
		AnimatedNode();
		AnimatedNode(const size_t entityID);
		virtual ~AnimatedNode();

		virtual const char* getName() const;
		bool isInitialized() const;
		void init(const unsigned int id, const char* name, scene::ISceneNode* parent, core::vector3df position, const c8* mesh, const c8* texture, f32 size);

		scene::IAnimatedMeshSceneNode* get() const;
		const char* getCurrentAnimation() const;
		const char* getLastAnimation() const;
		void setCurrentAnimation(char* currentAnim);
		bool animationChanged() const;
		void addAnimationSequence(Containers::AnimationSequence* seq);

	protected:
		//Actions
		void onUpdate(void* fDeltaTime, const size_t senderID);
		void onChangeAnimation(void* caCurrentAnim, const size_t senderID);
		void onRotateNode(void* fY, const size_t senderID);
		void onMoveForward(void* fSpeed, const size_t senderID);

		bool _isInitialized;
		scene::IAnimatedMeshSceneNode* _anode;
		char* _currentAnim;
		char* _lastAnim;
		bool _isCurrentAnimLooping;
		Entity* _owner;

		std::map<const char*, Containers::AnimationSequence*> _seqMap;
	};
}

#endif
AnimatedNodeComponent.cpp

Code: Select all

#include "AnimatedNodeComponent.h"
#include "../Managers/ActionManager.h"
#include "../Managers/EntityManager.h"
#include "../Entities/Entity.h"
#include <iostream>

using namespace Components;

AnimatedNode::AnimatedNode()
: Component(NULL)
{
}

AnimatedNode::AnimatedNode(const size_t entityID)
: Component(entityID)
{
	_name = "ANODE";
	_isInitialized = false;
	_anode = NULL;
	_currentAnim = "ASEQ_IDLE";
	_lastAnim = " ";
	_owner = EntityManager::ptr()->getEntity(_entityID);

	//Actions
	ActionManager::ptr()->getIngameAction(_entityID, "OnUpdate")->connect(this, &AnimatedNode::onUpdate);
	ActionManager::ptr()->getIngameAction(_entityID, "OnChangeAnimation")->connect(this, &AnimatedNode::onChangeAnimation);
	ActionManager::ptr()->getIngameAction(_entityID, "OnRotateNode")->connect(this, &AnimatedNode::onRotateNode);
	ActionManager::ptr()->getIngameAction(_entityID, "OnMoveForward")->connect(this, &AnimatedNode::onMoveForward);
}

AnimatedNode::~AnimatedNode()
{
	this->disconnect_all();
}

const char* AnimatedNode::getName() const 
{ 
	return _name; 
}

bool AnimatedNode::isInitialized() const 
{ 
	return _isInitialized; 
}

void AnimatedNode::init(const unsigned int id, const char* name, scene::ISceneNode* parent, core::vector3df position, const c8* mesh, const c8* texture, f32 size)
{
	_compID = id;
	_anode = GameManager::ptr()->smgr->addAnimatedMeshSceneNode(GameManager::ptr()->smgr->getMesh(mesh), parent, id);
	_anode->setName((const c8*)name);
	if(texture)
		_anode->setMaterialTexture(0, GameManager::ptr()->driver->getTexture(texture));
	_anode->setPosition(position);
	_anode->addShadowVolumeSceneNode();
	_anode->setScale(core::vector3df(size,size,size));
	_anode->setMaterialFlag(video::EMF_LIGHTING, false);
	_anode->setMaterialFlag(video::EMF_NORMALIZE_NORMALS, true);

	ActionManager::ptr()->emitIngameAction(_entityID, "OnRotateCamera", (void*)(&_anode->getAbsoluteTransformation()), _entityID);
	//ActionManager::ptr()->emitIngameAction(_entityID, "OnTargetMoved", (void*)(&_anode->getPosition()), _entityID);
	
	_isInitialized = true;
}

scene::IAnimatedMeshSceneNode* AnimatedNode::get() const 
{ 
	return _anode; 
}

const char* AnimatedNode::getCurrentAnimation() const 
{ 
	return _currentAnim; 
}

const char* AnimatedNode::getLastAnimation() const 
{ 
	return _lastAnim; 
}

bool AnimatedNode::animationChanged() const 
{ 
	return _lastAnim != _currentAnim; 
}

void AnimatedNode::onUpdate(void* fDeltaTime, const size_t senderID)
{
	if(!_isInitialized)
		return;

	if(!_isCurrentAnimLooping)
	{
		//If animation is done playing
		s32 currFrame = (s32)_anode->getFrameNr();
		s32 endFrame = _anode->getEndFrame()-1;
		if(currFrame >= endFrame)
		{
			if(animationChanged())
			{
				_anode->setTransitionTime(0.3f);
			}
			else
			{
				_anode->setFrameLoop(endFrame-2, endFrame-2);
				if(!_owner->isAlive())
				{
					ActionManager::ptr()->emitIngameAction(_entityID, "OnDead", " ", _entityID);
				}
				return; //if we're here, we're not going to animate our node at all
			}
		}
		else
		{
			_anode->animateJoints();
			_lastAnim = _currentAnim;
		}
	}
	else
	{
		if(animationChanged())
		{
			_anode->setTransitionTime(0.3f);
		}
		_anode->animateJoints();
	}
}

void AnimatedNode::onChangeAnimation(void* caCurrentAnim, const size_t senderID) 
{ 
	const char* currentAnim = ActionManager::ptr()->toCharArray(caCurrentAnim);
	if(_currentAnim != currentAnim)
	{
		_lastAnim = _currentAnim;
		_currentAnim = const_cast<char*>(currentAnim);

		if(_seqMap.find(_currentAnim) != _seqMap.end())
		{
			const Containers::AnimationSequence* seq = _seqMap.find(_currentAnim)->second;
			_anode->setFrameLoop(seq->startFrame, seq->endFrame);
			_isCurrentAnimLooping = seq->looping;
		}
	}

	std::cout << EntityManager::ptr()->getEntity(_entityID)->getName() << " changed animation from " << _lastAnim << " to " << _currentAnim << std::endl;
}

void AnimatedNode::onRotateNode(void* fY, const size_t senderID)
{
	float y = ActionManager::ptr()->toFloat(fY);
	_anode->setRotation(core::vector3df(_anode->getRotation().X, y*3, _anode->getRotation().Z));

	core::matrix4 m;
	m.setRotationDegrees(_anode->getRotation());
	m.setTranslation(_anode->getPosition());
	m.setScale(_anode->getScale());
	ActionManager::ptr()->emitIngameAction(_entityID, "OnRotateCamera", (void*)(&m), _entityID);
	//ActionManager::ptr()->emitIngameAction(_entityID, "OnTargetMoved", (void*)(&_anode->getPosition()), _entityID);
}

void AnimatedNode::onMoveForward(void* fSpeed, const size_t senderID)
{
	core::vector3df facing = core::vector3df(sin(_anode->getRotation().Y*core::DEGTORAD), 0, cos(_anode->getRotation().Y*core::DEGTORAD));
	facing *= 1.01f;
	_anode->setPosition((_anode->getPosition()+facing));
	core::matrix4 m;
	m.setRotationDegrees(_anode->getRotation());
	m.setTranslation(_anode->getPosition());
	m.setScale(_anode->getScale());
	ActionManager::ptr()->emitIngameAction(_entityID, "OnRotateCamera", (void*)(&m), _entityID);
}

void AnimatedNode::addAnimationSequence(Containers::AnimationSequence* seq)
{
	_seqMap[seq->name] = seq;
}
The Animated Node Component simple stores the AnimatedMeshSceneNode for us and provides actions for communicating with it.


At the end of this HOWTO, I'd like to display some simple examples for how to initialize an entity using the described approach:

Code: Select all

        Entity* ent = EntityManager::ptr()->createEntity("Chest");
	
	Components::Health* comp = new Components::Health(ent->getID());
	comp->init(ent->newComponentID(), 100.0f, 100.0f);
	ent->addComponent("HP", comp);

	Entity* ent2 = EntityManager::ptr()->createEntity("Dwarf");

	Components::CameraController* comp5 = new Components::CameraController(ent2->getID());
	comp5->init(ent2->newComponentID());
	ent2->addComponent("CAMERACONTROLLER", comp5);
	
	Components::AnimatedNode* comp3 = new Components::AnimatedNode(ent2->getID());
	comp3->init(ent2->newComponentID(), ent2->getName(), 0, core::vector3df(), "dwarf2.x", 0, 2.5f);
	comp3->addAnimationSequence(new Containers::AnimationSequence("ASEQ_DIE1", 212, 227, false));
	comp3->addAnimationSequence(new Containers::AnimationSequence("ASEQ_IDLE1", 292, 325, true));
	ent2->addComponent("ANODE", comp3);
	ActionManager::ptr()->emitIngameAction(ent2->getID(), "OnChangeAnimation", "ASEQ_IDLE1", ent2->getID());
	
	Components::Health* comp2 = new Components::Health(ent2->getID());
	comp2->init(ent2->newComponentID(), 100.0f, 100.0f);
	ent2->addComponent("HP", comp2);

	Components::InputReceiver* comp4 = new Components::InputReceiver(ent2->getID());
	comp4->init(ent2->newComponentID());
	ent2->addComponent("INPUTRECEIVER", comp4);
If we look at the pattern of how we're adding components to the entity, it's obvious how easy it is to make a class, XMLManager, who handles loading different entities from xml.

Now, if the XMLManager would handle adding components to entities and giving entities names, while the entityManager gives the entities unique ids, imagine, with a little bit of expanding on my example, how simple it really will be to add a new entity into your scene. One line of code would be enough:

Code: Select all

EntityManager::getEntity("Barrel")->spawnCopy(position, rotation, size);
I hope someone found this helpful and informative! Note that you can find the complete sourcecode over at my sourceforge project called Ravine's CRPG. I expect that there were some writing mistakes in this that I didn't catch with the word checker, please forgive me. I'm also very keen on hearing about programmers' opinion of this approach.

Happy programming!
Last edited by Trefall on Mon Oct 26, 2009 11:38 am, edited 2 times in total.
Halifax
Posts: 1424
Joined: Sun Apr 29, 2007 10:40 pm
Location: $9D95

Post by Halifax »

I think it is pretty cool that someone actually wrote this out for use in Irrlicht. Now beginner programmers can look at this when they need an example of how entities work, and are implemented. Great job, I must say.
TheQuestion = 2B || !2B
Saturn
Posts: 418
Joined: Mon Sep 25, 2006 5:58 pm

Post by Saturn »

This looks indeed useful on first sight, though I haven't yet given it a deeper look.
You get extra credits for use of composition rather than inheritence. As you said in your post deep inheritance is painful to maintain.
No IDyingDwardByHavingBeenShootByAnArrowInTheChestAnimatedMeshSceneNode

A few suggestions:

Use more typedefs. Writing std::map<const char*, Entity*>::const_iterator all the time is annoying and changing from map to hash_map had then to be done in many places instead of a single one.

Consider using a proper string class like std::string instead of char*. char* is annoying to use, error prone, and so 80ish. ;)

A return type of const <value_type> is meaningless, it is the same as just <value_type> so I'd refrain from using const bool, const size_t as return type. Same goes for value type function params in member function declarations, They can stay in the member function definition as they are indeed helpful there to catch accidental write access, but in the declaration they have no use at all other than to make the declaration harder to read.

I don't quite understand why actionSignals are returned as pointers by ActionManager. You handle them internally with copy semantics but use reference semantics in actionManager interface? Why not make registration and changing of them explicit by set member functions? This seems like a much more clean solution to me.

You often call std::map<>::find multiple times in the same function when you could have used an explicit iterator and with it call find only once. find is not a cheap function and there is an easy way to prevent costs for calling it multiple times. You really should consider it.

Why do all Manager classes befriend their Singleton<> super class? Actually A Singleton template can be implemented without this strange befriending. Is there a reason? Personally I'm not a friend of friend.
sudi
Posts: 1686
Joined: Fri Aug 26, 2005 8:38 pm

Post by sudi »

Good job on this article.
I added a really similar component system to my gameengine but the components where depending on each other bc i couldn't think of a good way how they could communicate. After this tut i will add some similar action system to my engine. thanks
We're programmers. Programmers are, in their hearts, architects, and the first thing they want to do when they get to a site is to bulldoze the place flat and build something grand. We're not excited by renovation:tinkering,improving,planting flower beds.
AO
Posts: 1
Joined: Mon Apr 21, 2008 7:27 pm

Post by AO »

Very nice article, good job!

I think it's fine passing around strings as constant char*, but if you want to store strings in class objects then use std string or similar.

The entity map uses a const char* to store the name which i think is fine. The entity name passed in is static so there's no need to store it in a string object which would double the amount of memory used.

I'd pass around const references as function parameters as often as possible to help performance.
Trefall
Posts: 45
Joined: Tue Dec 05, 2006 8:49 pm

Post by Trefall »

Thank you all for the replies!

Saturn & AO:
I'll go through my code again and clean up on the points you both raised and update the OP. I didn't add set member functions because I didn't want to write connect and disconnect member functions, a lazy and unsafe approach for sure. I'll change that now that you mentioned it. I'll change my habbit and start to use more typedefs too, since I clearly see the point you've raised on making the code cleaner (which is also what I strive for). I'll also be sure to not call find on std::map multiple times. My singleton class is oooold, I'll be sure to update it as well, as you clearly point out, there's no point for it being a friend. For strings I'll change up like you both mentioned. I'll change const char* member variables to core::string, but will probably continue to pass const char* as parameter to functions. I'll be sure to pass const references as well from now on.

Sudi:
I'm glad to hear that you found some inspiration in my approach! From reading on your homepage, I see that you're going for a multiplayer game. If you have a good solution for using the observer pattern efficiently over a client-server model, that's something I'd be very interested in hearing about. Maybe a solution would be to have the ActionManager server side or something...

Another thing to be aware about is that which observer library you're using will also affect how you can use the Actions. Sigslot, that I use, sort in FIFO, so I have to be aware of which order I initialize my classes and be sure that I initialize in the intended order, if there's an Action that depends on a certain order of execution (I try to stear away from this as much as possible though). Boost.Signals supports priority I know, but I prefer the one header file of sigslot over the entire boost lib, but I guess that's all preference.

Halifax:
I think it's important for beginners to realize that this is just one way of approaching the problem out of many. If found this approach to be logical and clean, and a way I'd like to code entities into my game, but that might not be the case with everyone else. Thank you for your comments though, I appreciate it ;)
sudi
Posts: 1686
Joined: Fri Aug 26, 2005 8:38 pm

Post by sudi »

Trefall wrote:Thank you all for the replies!


Sudi:
I'm glad to hear that you found some inspiration in my approach! From reading on your homepage, I see that you're going for a multiplayer game. If you have a good solution for using the observer pattern efficiently over a client-server model, that's something I'd be very interested in hearing about. Maybe a solution would be to have the ActionManager server side or something...
Well i'm writing my own actionmanager without something like sigslot. But problem with the network. right now its worked out with a sperate manager which tells the components to send updates. And i guess i will keep it that way. But the way the components talk to each other will be changed to ur methodebc mine is just not really good.
We're programmers. Programmers are, in their hearts, architects, and the first thing they want to do when they get to a site is to bulldoze the place flat and build something grand. We're not excited by renovation:tinkering,improving,planting flower beds.
sudi
Posts: 1686
Joined: Fri Aug 26, 2005 8:38 pm

Post by sudi »

Hey Trefall any idea how to get sigslot running in mingw32?
We're programmers. Programmers are, in their hearts, architects, and the first thing they want to do when they get to a site is to bulldoze the place flat and build something grand. We're not excited by renovation:tinkering,improving,planting flower beds.
Trefall
Posts: 45
Joined: Tue Dec 05, 2006 8:49 pm

Post by Trefall »

Interesting. So in concept what you're doing is using the action system for component communication, while using a different approach for network communication between entities?

Or did you mean that you're doing a hierarchial approach to updating the components, while being interested in using the action system for communication? In my setup, being told to update is a part of the action communication. I'm just interested in hearing about how other people solve this.

I have only tried building sigslot in vc++. Have you tried running sigslot in singlethreaded mode?
sudi
Posts: 1686
Joined: Fri Aug 26, 2005 8:38 pm

Post by sudi »

My idea with the action system is to exchange data between components. stuff like update, input and network is taken care by my gamemanager. Right now i acces the components by type-casting which is kinda bad. Anyways i would love to use sigslot but simply doesn't compile and i don't get why.
We're programmers. Programmers are, in their hearts, architects, and the first thing they want to do when they get to a site is to bulldoze the place flat and build something grand. We're not excited by renovation:tinkering,improving,planting flower beds.
Trefall
Posts: 45
Joined: Tue Dec 05, 2006 8:49 pm

Post by Trefall »

On the Sigslot homepage, the author writes:
The signal/slot library is ISO C++ compliant (at least where possible) and will work on pretty much anything. All you need is a reasonable C++ compiler that supports templates. You don't need partial template specialisation support, so VC6 and VC.NET are both fine.
What is preventing you from compiling?

There are other options for observer libraries, though I prefer the one header dependency of sigslot (I listed some in the OP).

So how component-based are your entities? In my approach I wanted my entity class to only be a component container with an id attached, and I almost made it. I decided that having a bool check that states wheather the entity is alive or not makes sense, since my components depends on the entity class anyway (access to it's owner), I didn't see it breaking the component based approach, also it doesn't block any data-driven functionality.

In my approach, I use components for receiving data as well, like, the InputReceiver component, which handles input events from the InputManager, or AIReceiver component (that will later handle AI events) or in the same line of design I'd have a NetReceiver component or something like that to receive events across the net.

Still I could handle message distribution from the net in my NetManager or in the GameManager, where I'd send update events from the game manager every frame or every scheduled frame. Now I would only send net events to any entity that subscribes to receive updates from the net. If an entity dies, I could just disconnect it from the event, and it would no longer receive events.

That's the approach I'm striving for with my framework anyway.

In your approach, are you handling logic and network updates with traversals, and pointer to player entity for updating player for input? Is your Player entity inheriting from entity?
sudi
Posts: 1686
Joined: Fri Aug 26, 2005 8:38 pm

Post by sudi »

na i also use only components. I even have taken it so far that the engine user has no acces to the entityclass at all. So in general there is no entity class. Only in the background i use a class called entity to hold the components together. When the user wants to create a entity he calls
IComponentManager::createEntity(). That function returns a new entity id. Now u can create components and add them to the entity with IComponentManager::addComponent(CompEntity, IComponent);
Now the components can register themselve to stuff like update, input and network.

PS:
The problem with sigslot is that i only have to include the header without even using it and it throughs errors. I tried the fix from the bugtracker. It worked for the error but now the "same" error arised in the other classes but the fix doesn't work there. to bad

EDIT: fixed it.
We're programmers. Programmers are, in their hearts, architects, and the first thing they want to do when they get to a site is to bulldoze the place flat and build something grand. We're not excited by renovation:tinkering,improving,planting flower beds.
Trefall
Posts: 45
Joined: Tue Dec 05, 2006 8:49 pm

Post by Trefall »

Ah, that sounds like a nice setup you've got there. I also hide the actual entity creation from the programmer using a factory pattern approach, but instead of returning an id, I return a pointer, and then the adding of components is done on that entity pointer. Your method is safer for sure, maybe I should return a reference instead...

Have you any plans for making entity/component handling data-driven? In my setup the goal is to define entities in an xml type setup sheet (an xml file defines the components that each entity is made of), and then I plan on integrating LUA to handle the functional scripting of the entities. I hope to get to the point where I can define new components entierly in LUA scripts, but I'll have to see how far I can take it.

If I manage this, I should be able to use the framework source code as a base, and then just script on top of that to make the games I want without touching the built core. If not, at least I want to script the functionality of the action events in LUA.

Hmm, I like your approach for input events, where you let outsiders subscribe to input events. The way I do it, I have an InputReceiver component that I can attach to my player entity for example. And in the InputReceiver I emit action events based on which input is received and handle the priority. Like, if the left mouse button is pressed, I'll give the gui priority to check if the mouseclick interacted with any gui buttons, and if it didn't I'll pass it on to the 3d world to do a raycast and see if I targeted something. So as long as you can manage the priority order, I'd really like to think more about subscribing to input events, instead of emitting from them.

On sigslot, I'm glad you got it working with mingw32! I hope you'll share how your experience in using the system goes.
sudi
Posts: 1686
Joined: Fri Aug 26, 2005 8:38 pm

Post by sudi »

My final plan about this system is the same as yours. I actually already made a template components which is loaded with script functions. So a completly scriptet component.
In terms of input i used my aproch bc for example my camera needs mouse input for rotating and another component for trigering the attack for example.
And in terms of refenrece would be easy if you return a const ref. i just went with the id sheme bc it completly takes away the entity id from the programmer. that way its easier to only implement the stuff using components. otherwise he might get the brilliant idea to inherit from the entity class and adds some more function which don't belong there.
We're programmers. Programmers are, in their hearts, architects, and the first thing they want to do when they get to a site is to bulldoze the place flat and build something grand. We're not excited by renovation:tinkering,improving,planting flower beds.
Trefall
Posts: 45
Joined: Tue Dec 05, 2006 8:49 pm

Post by Trefall »

Yes, I clearly see your motivation for hiding the entity class entierly. Maybe I'll give it the same approach, to make it more safe for other programmers who doesn't know how the framework is ment to be used. Then again, if the goal is to make games entierly scripted/xml'ed on this framework, then the interface that an end-user programmer will work with won't touch the entity class at all anyway. It's an approach with truly exciting possibilities though, if now I could only get enough time to actualy make good progress on this :P
Post Reply