I've had some time to spend on Citizen and as always these days it's gone straight into the Entity-Component system. I feel like it never gets to where I want it. The more I work on it the more I got left to do. I guess that is the typical story when you try to create something you've never done before, and I think that as long as I can afford the time it takes, this gives me more hard-boned experience than anything. It's just that I'm dying to get to the point where I can work on gameplay again. [sad]
Anyway since I don't have any pretty pictures to show you, and I want to give some substance beyond my confused rambling, I thought I'd post some code from the core of the entity system.
The problem:
As I've mentioned in earlier posts my implementation is based on a number of component managers containing components of their type. The entity object acts as a tag to associate with the components, and as an interface for the components to access entity-specific data. The managers do the real update work and they are completely isolated from each other. If they do need to access the state of the world they have a pointer to the entity manager object which is encapsulating the whole system, and which has a public interface for manipulating the entities in a controlled and safe way.
The problem I wanted to solve was to have a common foundation for the component managers. Up until now I had made a separate implementation for each manager, copy-pasting some common code (like adding and removing components) and modifying it slightly for each type. On the other hand there didn't seem to be much point in deriving from a basic component types since their constructors would have different signatures anyway. I ended up with the following template solution which turned out quite alright.
The code:
First there is the templated base class of the component managers.
ComponentManagerBase.hpp
class EntityManager;class EntityData;template < class ComponentType > class ComponentParameters;// --------------------------------------------------------------template < class ComponentType >class ComponentManagerBase {public: // -------------------------------------------------------------- ComponentManagerBase( EntityManager *entityManager ) : entityManager( entityManager ) {} // -------------------------------------------------------------- ~ComponentManagerBase( ) { if ( !components.empty( )) { Log log; log.printWarning( "~ComponentManagerBase: Some components had to be destroyed by manager destructor." ); } for ( EntityIdComponentMap::iterator it = components.begin( ); it != components.end( ); ++it ) { delete it->second; } } // -------------------------------------------------------------- void addComponent( uint entityId, EntityData *entityData, const ComponentParameters ¶ms ) { EntityIdComponentMap::iterator it = components.find( entityId ); if ( it != components.end( )) { throw Exception( "ComponentManagerBase->AddComponent: Entity already has given component type." ); } components[entityId] = new ComponentType( entityId, entityData, params ); } // -------------------------------------------------------------- bool hasComponent( uint entityId ) { EntityIdComponentMap::iterator it = components.find( entityId ); if ( it == components.end( )) { return false; } return true; } // -------------------------------------------------------------- void removeComponent( uint entityId ) { EntityIdComponentMap::iterator it = components.find( entityId ); if ( it == components.end( )) { throw Exception( "ComponentManagerBase->RemoveComponent: Entity has no component of this type." ); } delete it->second; components.erase( it ); } protected: EntityManager *entityManager; typedef std::map EntityIdComponentMap; EntityIdComponentMap components;};
The common trait among all the managers is that they have a container of components of their given type, here implemented as a std::map keyed with the numeric entity id the component belongs to. Furthermore they commonly need to provide a public interface to manipulate this collection, here the add-, remove- and hasComponent functions. They also make sure there is a valid pointer to the EntityManager object (which creates the managers) and that leftover components are destroyed properly when the manager dies.
The trick here is the forward declaration of the ComponentParameters class template, a so far undeclared template that holds the creation parameters for each component type. You can see that the addComponent function takes a reference to such an object, specialized on the manager's component type. Then it is simply passed along to the component's constructor so no specific details about it or what values the component wants are needed.
Then there is the minimalistic base class of the components.
ComponentBase.hpp
class EntityData;class EntityManager;// --------------------------------------------------------------class ComponentBase {public: ComponentBase( uint entityId, EntityData *entityData ) : entityId( entityId ), entityData( entityData ) {} uint getEntityId( ) { return entityId; }protected: uint entityId; EntityData *entityData;};
This doesn't do much more than manage some basic properties of all components, like a numeric id for the entity and the access into the entity-specific data.
Finally, here is Gun, a sample component manager and corresponding component built upon this foundation. I only post the declarations.
GunManager.hpp
class EntityManager;// --------------------------------------------------------------class GunManager : public ComponentManagerBase {public: GunManager( EntityManager *entityManager ) : ComponentManagerBase( entityManager ) {} void updateByFrame( ); void shoot( uint entityId );};
Gun.hpp
class EntityData;class Gun;// --------------------------------------------------------------template<>class ComponentParameters {public: ComponentParameters( uint cooldown, real exitSpeed ) : cooldown( cooldown ), exitSpeed( exitSpeed ) {} uint cooldown; real exitSpeed;};// --------------------------------------------------------------class Gun : public ComponentBase {public: Gun( uint entityId, EntityData *entityData, const ComponentParameters ¶ms ) : ComponentBase( entityId, entityData ), params( params ), currentCooldown( params.cooldown ) {} Vec2 getPos( ); real getAngle( ); real getSpeed( ) { return params.exitSpeed; } uint getCooldown( ) { return currentCooldown; } void decreaseCooldown( ); void resetCooldown( );private: ComponentParameters params; uint currentCooldown;};
Note that in order for this to work the GunManager must inherit ComponentManagerBase, obviously, and there must also be an explicit specialization of the ComponentParameters<> class template along with the declaration of Gun. This way only the Gun component and the party calling the GunManager's addComponent function need to know the inside of ComponentParameters. GunManager doesen't even see it and ComponentManagerBase only passes it on. [smile]
That's all there is to it. Adding component types now is easy following the two criteria above, and the common component creation and keeping code is reused independent of type. I hope you've found this post useful. If you have any questions, ideas or reservations against it, you know what to do.
I like the design, but I have a few questions, if you don't mind. Look out, ordered list!
Is ComponentType supposed to be ComponentBase in "ComponentManagerBase.hpp"? It seems that Gun inherits from ComponentBase, but ComponentParameter uses ComponentType as a parameter.
Are the Entity IDs declared in an enumeration?
It would be nice to see a small example of how EntityData instances are created. I know that's a lot to ask, but I like the design so far and I might be able to use something similiar in my project. I'm sorry if I missed something, or if you explained this stuff before.