Need some advice on a Component-Based Object System using the Observer Pattern

Started by
12 comments, last by Sean_Seanston 13 years, 4 months ago
I have a college assignment where I have to take a design pattern and develop it into a set of classes in a small application.
So I figured the Observer pattern would be as good as any and a component-based object system seemed to be an interesting way to apply it while hopefully learning something useful.

My idea is to have a simple console application where I've created 2 objects using components and have them display messages when they change position, collide with each other and then lose health.

So currently I have an Entity class that can add and remove Components, a Subject base class that can add/remove Observers, an Observer base class that does basically nothing right now and several kinds of Components such as HealthComponent.

The idea is to derive specific Observers from Observer such as HealthObserver or PositionObserver to handle corresponding events. Then they would have functions such as notifyPositionChanged() etc.

I'm just wondering about how to do a few things:

1. How should collisions work? Assuming we have 2 objects both with a collisionComponent, how might we deal with it?

positionComponent could notify collisionComponent (which would inherit from positionObserver) about a change in position but then how do we find other objects that might have collided?

Iterating through a list of objects and checking their collisionComponent for their hitboxes seems to go completely against the point of the Observer pattern.
Do we have to notify every other collisionComponent that exists in all other objects? That seems like a mess.

We can't assume an object has a collisionComponent either (even though they will for the purposes of the example) so Entity->getComponent() is as much as the Entity itself can have to do with it I think.

Being the Observer pattern, I guess the collisionComponent needs to be informed of a position change, decide whether or not there was a collision and if there was, inform the healthComponent and whatever else needs to be done.

I think it's really just a good way of detecting the collision I have trouble with. An ugly way of accessing the collisionComponent data of all Entities that might have collided would sort of go against the point of the assignment which is to show good implementation of a design pattern.

2. Somewhat relating to that, how should I handle telling an Entity to move? An Entity won't necessarily have a component allowing movement so building an integral move() function into Entity is out so is there a better option than just Entity->getPositionComponent()->move()? That seems to be awfully anti-encapsulation to go so deep into an object like that. I can't see any other way right now myself though.

Obviously it's just a small proof-of-concept application as an example of a certain kind of design so it doesn't have to be perfect and anything massively complicated is out really I think, but can anyone think of a reasonably neat solution?
Advertisement
Looking at problems like these in more general terms usually helps to make your code and solution simpler.

You want to use the observer pattern to tell all listeners when some piece of data change, right? You mentioned the position data and health data. You also mentioned some typical events, like a collision event, that could occur.

For what you want to achieve, you could have the following components and classes:
- Health
- Physical
- Collidable (+ CollisionManager)

and
- Entity

You could now declare an Entity which has health, and is physical and collidable.

Health has some data, like HP and DEAD.
Physical has some data, like POSITION.
Collidable has some data, like AABB.

You want Collidable to get notified when Physical changes position, and you want Health to get notified when Collidable detects a collision.

The simplest way to implement this would be through events. Since you want to use the observer pattern, you could do something like this:

Observers:
class IPosObserver{public:  virtual void invoke(const Vec3 &pos) = 0;};class ICollisionObserver{public:  virtual void invoke(const Collision &collision) = 0;};class IDeadObserver{public:  virtual void invoke(const bool &dead) = 0;};


Components:
class Physical : public IComponent{public:  Physical() {}  void setPos(float x, float y, float z) {    pos = Vec3(x,y,z);    for(int i = 0; i < pos_observer.size(); i++)      pos_observer.invoke(pos);  }  const Vec3 &getPos() const { return pos; }  void addPosObserver(const IPosObserver &observer) { pos_observer.add(observer); }private:  Vec3 pos;  std::vector<IPosObserver> pos_observer;};class Collidable : public IComponent, public IPosObserver{public:  Collidable(CollosionManager *collisionMgr, const Mat3x2 &aabb)   : collisionMgr(collisionMgr), aabb(aabb) {}  virtual void invoke(const Vec3 &pos) {    Collision *collision = collisionMgr->testCollision(pos, aabb);    if(collision)      for(int i = 0; i < collision_observer.size(); i++)        collision_observer.invoke(*collision);  }  void addCollisionObserver(const ICollisionObserver &observer) {    collision_observer.add(observer);  }private:  Mat3x2 aabb;  std::vector<ICollisionObserver> collision_observer;};class Health : public ICollisionObserver{public:  Health(int hp) : hp(hp), dead(false) {}  virtual void invoke(const Collision &collision) {    int dmg = collision.impact_force;    hp -= dmg;    if(hp <= 0) {      dead = true;      for(int i = 0; i < dead_observer.size(); i++)        dead_observer.invoke(true);    }  }    void addDeadObserver(const IDeadObserver &observer) {    dead_observer.add(observer);  }private:  int hp;  bool dead;  std::vector<IDeadObserver> dead_observer;};


Usage:
Entity entity;Physical *physical = new Physical();Collidable *collidable = new Collidable(collisionMgr, aabb);Health *health = new Health(100);physical->addPosObserver(*collidable);collidable->addCollisionObserver(*health);health->addDeadObserver(*entityMgr);entity.addComponent(physical);entity.addComponent(collidable);entity.addComponent(health);//Quick flow test:physical->setPos(10, 0, 0);


Obviously this is a pretty naive approach. You could do it much more general and a lot more useable, but you'd most likely have to start using template classes/functions. I'm using components by letting the entity hold a list of components and properties. Components then add the properties they want to their entity and keep a reference to those properties. The components can also register listeners to the properties, so that when a property change, they will receive a callback. Thus I have the observer pattern integrated into my template-based property class that gets invoked every time the property's data change: Property.h

Best of luck with this though :) A cool little project!

[Edited by - Trefall on December 3, 2010 7:41:14 PM]
I forgot to answer your question 1) and 2) :P

1) Collision works by checking the boundary of two entities, and see if they overlap. There are multiple strategies to this. If you're in 2D coordinates, the simplest is either a circle or an AABB (AxisAlignedBoundingBox). If you're in 3D, the simplest is probably an AABB. Do a quick google on AABB vs AABB, and you'll discover it's just a simple if check. Here's point vs aabb:

bool pointInAABB(const Vec3 &point){        return ((point.x >= aabb.min.x && point.x <= aabb.max.x) &&                        (point.y >= aabb.min.y && point.y <= aabb.max.y) &&                        (point.z >= aabb.min.z && point.z <= aabb.max.z));}


It's a good idea to let a collisionMgr handle collision detection instead of a component, because collision detection happens between multiple entities, and the collidable component only knows about it's owner, the entity it's added to. The collidable should probably add it's owner to the collision manager however...

2) Most general way to add the ability to invoke functionality within a component of an entity, but without having knowledge of which components are actually within the entity, I'd suggest using a simple Event framework.

Event<Vec3> e("Move", Vec3(10, 0, 0));entity.sendEvent(e);void Entity::sendEvent(const IEvent &e) {  for all components:    component->OnEvent(e);}void Physical::OnEvent(const IEvent &e) {  if(e.type == "Move") {    const Event<Vec3> &vec3e = static_cast<const Event<Vec3>&>(e);    pos = vec3e.getArg0();  }}


//Super simple event framework:
class IEvent{public:  IEvent(const std::string &type) : type(type) {}  const std::string type;};template<typedef T>class Event : public IEvent{public:  Event(const std::string &type, const T &arg0) : IEvent(type) {    arg.push_back(arg0);   }  const T &getArg0() { return arg[0]; }private:  std::vector<T> arg;};
I'm using a similar solution to Trefall, with a C++ delegate event system.

Thanks Trefall, that was a lot of help and cleared up some things I wasn't sure about.

I've been implementing some of that and seeing how it works and I think I mostly know how to proceed now.

There's just one thing I'm sure about with collision detection though:

I see that the Collidable component itself checks for collisions by calling the Collision Manager's function and then notifies its observers if there was a collision.

If it does collide with another Entity, then what happens to the other Entity? Should collisionMgr->testCollision() invoke the other Entity's collision handling as well? How then might it do that?

I think I'm clear on everything else but I've found collision detection with components and observers quite confusing given the inherent object interaction involved.
I'm glad I could be of some help, and especially on this topic!

You're entirely correct about your observation. The simple approach I wrote wouldn't really take care of the other object in the collision.

What I would really do, is this:

class Collidable : public IComponent, public IPosObserver{public:  Collidable(const Entity &owner, CollosionManager *collisionMgr, const Mat3x2 &aabb)   : owner(owner), collisionMgr(collisionMgr), aabb(aabb)   {     collisionMgr->registerEntity(owner, aabb);  }  virtual void invoke(const Vec3 &pos) {     collisionMgr->testCollision(owner, pos);  }  void addCollisionObserver(const ICollisionObserver &observer) {    collision_observer.add(observer);  }  void OnEvent(const IEvent &e) {    if(e.type == "Collision") {      const Event<Collision> &collisionEvent = static_cast<const Event<Collision>&>(e);      const Collision &collision = collisionEvent .getArg0();      for(int i = 0; i < collision_observer.size(); i++)        collision_observer.invoke(collision);    }  }private:  Mat3x2 aabb;  std::vector<ICollisionObserver> collision_observer;  const Entity &owner;};


So, the Collidable component now also stores a reference to the entity that owns it. If an entity then gets a collidable component added to itself, the Collidable component will register the entity, and it's aabb, with the CollisionMgr. Then, when the invoke of IPosObserver is triggered, the component tells the CollisionMgr to test collision of the entity at this new position with any other entity registered with the collision manager (optimizations can be made here internally in the CollisionMgr, like using a Quad/Octree or any other form of spatial partitioning scheme to lessen the search-field).

When the collision manager finds that the Test Collision call results in a collision, it packs both the colliding bodies into the Collision object, along with other pieces of info that might be the result of the collision, and pass it along with an event to both entities involved. Each entity's internal component list decides how and if this collision event is handled...
Cool, I think I've solved the problem good enough for my purposes anyway. Thanks.

One more thing... what happens with memory allocation with components in this situation?

I have some code like this:
Entity* playerEntity = new Entity( "Player1" );playerEntity.addComponent( new HealthComponent( 100 ) );


If I do:
delete playerEntity;

I still need to call delete on the pointer to the HealthComponent, right?

So can I just iterate through the map and call delete on the pointers in the destructor of the Entity class, and have everything deleted when I call delete on an Entity*?

Like this I mean:

Entity::~Entity(){	for( std::map< std::string, Component* >::iterator it = compMap.begin(); it != compMap.end(); it++ )	{		delete (*it).second;	}}


EDIT: Oh and is there any way I could make this function to return components automatically do a dynamic_cast to the proper type? I saw something like that on another site but I don't understand how it works.

Here's my simple function to return a Component*:
Component* Entity::getComponent( std::string compName ){	return compMap[compName];}


Here's the site with the function:
http://www.purplepwny.com/blog/?p=215

It's a template function but I don't understand how it knows what to cast it to.

[Edited by - Sean_Seanston on December 6, 2010 1:02:10 PM]
Yes, you have to go over your list of components and delete them in the entity destructor, like you did.

Templates are fairly simple. You can either have templated classes or templated functions. Note that templated functions in C++ can't exist in the .cpp file, they have to be inline functions in the header.

class Entity{  template<class T> T *getComponent(const std::string &type);};template<class T>inline T *Entity::getComponent(const std::string &type){  std::map< std::string, Component* >::iterator it = compMap.find(type);  if(it != compMap.end())  {     Component *icomp = it->second;  //Dynamic cast is quite expensive, so only do this in debug mode...#ifdef _DEBUG     T *comp = dynamic_cast<T*>(icomp);     if(comp == NULL)       return NULL;     return comp;#else     T *comp = static_cast<T*>(icomp);     return comp;#endif  }  return NULL;}


Usage:
Health *health = entity->getComponent<Health>("Health");


Note that in my own component implementation, I haven't yet come about a situation where I need this functionality...
Quote:Original post by Trefall
Note that in my own component implementation, I haven't yet come about a situation where I need this functionality...


Is that because you never need to access components directly since it's all done indirectly through the Entity?

I'd probably try something like that too if I had the time or if I was implementing components in an actual application... I think I'll just use this rough approach for now, the assignment should be complex enough already.

I'll have to template my component getter though, that should be impressive ^_^. Thanks for that.
Quote:Original post by Sean_Seanston
Is that because you never need to access components directly since it's all done indirectly through the Entity?


Yes. With my own approach, if I ever come about a situation where a component need to know of the existence of another component, or, god forbid, access another component, I've done something very wrong with how I've parted logic up into the logical modules.

Components are meant to be modules of logic that add a piece of behavior to their entity. If you come in the situation where you have to have this component and those other three components to add that component, you're in trouble, because you're bound to make a mistake, or end up with large spreadsheets that you have to follow carefully every time you want to add another component to your entity, or build a new entity from scratch. You really don't want to end up in that situation when it can be prevented :)

Quote:Original post by Sean_Seanston
I think I'll just use this rough approach for now, the assignment should be complex enough already.


Yeah, that's what I had in mind also when presenting the approach I gave you in my first post. I'd just link you to my thorough approach on my journal if you had more time ;) That said, if you ever find the time to implement components into a proper application, I'd highly recommend to search through the web and review as many approaches as you can, then mix them together into the component system that fits yourself and your brain the best.

And yeah, using C++ template is a bit scary to start with, but opens up a whole new world of flexible solutions! Best of luck!

This topic is closed to new replies.

Advertisement