The ObjectShell fully realized
Back when I was creating the multiplayer pacman, as well as the Braid clone, I first went into the world of component based entity systems. The idea behind this system is basically that any object is not made out of a single class. Rather it's several classes working together to represent a single object.
For the bachelor thesis, we implemented this system to it's full extend. We were able to produce new objects at runtime, which presents a huge advantage. Instead of having to code every single little tiny object, we simply link it's components together.
To do this, we basically need two classes: Component and Entity. I'll introduce you to the Entity first
class Entity: public EventListener { friend class Engine; public: Entity(); Entity(std::string name, std::string meshName, Ogre::SceneNode* parentNode); virtual ~Entity(); void addComponent(BaseComponent* component); void registerListener(const Event::EventType& type, EventListener* listener); virtual void receiveEvent(Event* event); virtual void setParentNode(Ogre::SceneNode* newParent); Ogre::SceneNode* getNode() const; void setOgreEntity(Ogre::Entity* ogreEntity); Ogre::Entity* getOgreEntity() const; void setGraphicComponent(GraphicComponent* graphicComponent); GraphicComponent* getGraphicComponent() const; void setName(std::string name); std::string getName() const; static Entity* create(std::string name, Ogre::SceneNode* parentNode) { return new Entity(); } void setNode(Ogre::SceneNode* node) { this->node = node; } void removeComponent(BaseComponent* component); private: GraphicComponent* graphicComponent; Ogre::SceneNode* node; Ogre::Entity* ogreEntity; typedef std::vector<BaseComponent*> ComponentVector; typedef std::vector<EventListener*> EventListenerVector; ComponentVector components; std::map<Event::EventType, EventListenerVector> eventListeners; std::string name; };
Now, this class is basically only a container for components, with some added functionality for Ogre3D (this was done to ensure that any entity will be in Ogre3Ds scenegraph).
The class also handles events that get either passed to the Entity or its components.
The next thing is of course the component itself. Watch the almighty BaseComponent class:
class BaseComponent: public EventListener { protected: Entity* parent; public: virtual ~BaseComponent(){ parent = NULL; } virtual void setParent(Entity* parent) { this->parent = parent; } virtual Entity* getParent() { return parent; } virtual void receiveEvent(Event* event) { } };
Yeah, it's really not much to look at. But this shows the power of this system. I can add ANY component to an Entity. The component only has to register itself to the events it's interested in. With that communication system, any component can talk to another, without the two knowing about the others implementation. This is the real strenght of the system.
Let's see a little example here. Obviously in a 3D system, objects move around quite a lot. We also want 3D sound, so the position of an audio emmiter has to be updated as well. So, as soon as the object is translated, the corresponding event will be sent. This is what happens afterwards:
void AudioComponent::receiveEvent(Event* e) { if(e->getType() == Event::TRANSLATE) { std::map<std::string, Audio*>::iterator it = audioList.begin(); Ogre::Vector3 position = getParent()->getNode()->_getDerivedPosition(); for(;it!= audioList.end(); it++) { it->second->getAudio()->SetPosition(position.x, position.y, position.z); } } BaseComponent::receiveEvent(e); }
As you can see, as soon as the audio component knows it has to update its position, all it has to do is to get the (wolrd)position of its parent entity and apply that to each audio it's currently playing. No other component has to be able to do that, so this code is neatly separated from the other systems. To demonstrate, let's see the same method for the GhostObjects
void GhostComponent::receiveEvent(Event* e) { if(e->getType() == Event::TRANSLATE) { btTransform transform; transform.setIdentity(); Ogre::Vector3 position = parent->getNode()->_getDerivedPosition(); Ogre::Quaternion orientation = parent->getNode()->_getDerivedOrientation(); transform.setOrigin(BtOgre::Convert::toBullet(position)); transform.setRotation(BtOgre::Convert::toBullet(orientation)); ghostObject->setWorldTransform(transform); } }
Same method, but what happens is entierly different. It will also update the position of the involved pyhsics objects. But again, this is encapsulated here.
One can imagine a ScriptComponent, where the component loads a script (Lua, Javascript, Phython,...) and executes that once it gets an event. This would make the engine very easy to work with, but for now it's a bit of a stretch.
As you may have seen, this system allows to easily extend the engine, while at the same time remain very flexible. You don't need a huge monolithic inheritance structure, but rather small encapsulated classes that each do one specific job. I'll admit that it could lead to an overly huge structure overall (like having thousends of classes), but I think the flexibility of this system makes it worthwhile.
Next I'm going to talk about where these components come from and how they are managed in the engine.











