Outboard component-based entity system architecture

Started by
105 comments, last by qingrui 16 years, 4 months ago
I've recently been making an entity system. Initially, I was planning it around a component-based model:

class Component {
public:
    virtual void OnUpdate() = 0;
    ...
};

class Entity {
    ...
    std::vector<shared_ptr<Component> > components;
    ...
}


In this model, the Entity aggregates a list of heterogeneous components, which modify the behavior of the object without having to complicate the Entity class. The factory in charge of creating Entities is responsible for populating them with the appropriate Components. There have always been a few things I didn't like about the component-based model. * Components have difficulty finding other components in the same Entity. Given an AnimationComponent which requires a PathfindingComponent to use, often it's necessary to manually iterate through the list of Components, dynamic_casting each one until you get a hit, or else to complicate the factory with details of what component needs to know about what, or the Entity with specific typed Component members for direct access. * Components have difficulty finding the same components in other Entities, and their associated game-wide subsystems. The PathfindingComponent needs to know at least about other Entities' PathfindingComponents, or (more likely) a GameWidePathfindingSystem. The former can require a lot of iteration and dynamic_casting; the latter requires either some pretty serious arrow chains, or singletons/globals. The basic problem, I think, is that an Entity has a big ol' list of components and no clue what to do with them. I've shown an Update event up there, but even that's a stretch; I've seen a lot of components with empty OnUpdate() methods. You can, of course, add OnDamage and OnNewPath and OnEntityScratchedLeftEar as necessary, but that makes the component-entity response matrix sparser and sparser. You can abandon that approach and have entities hook into the events they care about directly, but then what's the point of the observer pattern there at all? While designing my graphics system (this time through, enforcing a really strong logic/graphics separation), I fell into this alternate arrangement.

class FooComponent
{
    FooComponent(shared_ptr<Entity> entity);
    ...
};

class FooSubsystem
{
    void OnEntityAdded(shared_ptr<Entity> entity)
    {
        components[entity] = SomeComponent::Create(entity);
    }
    void OnEntityRemoved(shared_ptr<Entity> entity);

private:
    hash_map<shared_ptr<Entity>, shared_ptr<SomeComponent> > components;
};

class World
{
    Event<shared_ptr<Entity> > EntityAdded;
    Event<shared_ptr<Entity> > EntityRemoved;
};


I realize that's a bit skeletal, but what's going on there is that the subsystem hooks into events which fire when entities are added to or removed from the world, automatically creating corresponding components for itself and holding onto them. The same components as before are being created; but rather than being stored by the Entity in heterogeneous lists, they're stored by their parent subsystem in homogeneous lists. The thing I like most about this is that there's no need for the components to swim back to their subsystem. In fact, they generally don't need to know about the subsystem at all. More subtly, in the case where one component type (BarComponent, say) depends on another (FooComponent), the BarSubsystem can listen to the FooSubsystem for add/remove events instead of the World, automatically matched up to the component it cares about. In the case of the entity/graphics divide, doing something like this is just common sense. I've been using it for a lot more, though; pretty much all of the behavior of my Entity, which by now is a pretty teeny class. I'm sure others have explored this organization before. What say you all? [Edited by - Sneftel on September 10, 2007 1:12:22 PM]
Advertisement
I decided to go down a very similar path with the component-based entity design. In theory it sounds like a fantastic idea with the entities really just being containers for a certain grouping of components. It really seems to promote the idea of modularity and seems like it would adapt very well to data-driven designs. When I first thought about this design it made a lot of sense and it just seemed natural to me that an entity really is just a collection of components and it does nothing more than hold on to them. I also liked the component-based design because it seemed a good solution for the deep entity hierarchy problem. After saying all of this though, it is just like any other design decision and it comes with its pros/cons.

Once I started implementing I ran into the same questions you are asking. For my design each component knows its entity parent and each entity has a HasComponent type function for querying on a specific component type which returns either a shared pointer to the component or an empty (NULL) shared pointer. The entities store their components in a map or hash map type structure for quick lookup. To get component specific information I am forced to cast the returned pointer to the type I requested and I basically implemented a checked_cast type function which will do the more costly dynamic_cast in debug mode and just a static_cast in release mode.

Currently for game events I have an event dispatcher so entities can interact together and with the overall game world. The decision I made for now, which might get changed later, is to have each component within the entity register callbacks with the event dispatcher for the events it should handle for their parent entity. Again the same problem arises, a specific event object is created and passed through the callback function pointer to the component and the component function must cast through the checked_cast implemented earlier to the specific event it was expecting to extract the information it needs to do its task.

As a side note I used the boost::function and boost::bind for the callback implementation in the event dispatcher and just the usual register/unregister for messages. I have been following the fast delegates thread here and have looked over the library and it looks like a nice solution that I might try to integrate into my existing code.

This is my initial coding effort with a few minor refactorings and I am still not sure if this is a good design. It is certainly not fully featured and I have already experienced some initial aches and pains especially when event handling requires information from a lot of components of multiple entities. I think one of the most important design decisions for a successful component-based entity is choosing a good set of components so they are not too coupled.

I am very interested in this topic and would love to hear how others' feel about it and are dealing with the problems they run into.
Bump. Other points of view?

[Edited by - Sneftel on September 10, 2007 10:13:06 AM]
A lot quieter then I was expecting for this thread. I was hoping to see a lot of comments and arguments for/against this type of design. Also would like to hear some people's experiences that are further along in their implementation than I am.
Quote:Original post by vtchill
A lot quieter then I was expecting for this thread. I was hoping to see a lot of comments and arguments for/against this type of design. Also would like to hear some people's experiences that are further along in their implementation than I am.

I think it could be a busier thread. I for one am struggling to get my head round it, maybe another/more thorough explanation is needed to get people going?

Perhaps its me, I seem to be having a bad day reading code [dead], but I'm having trouble just understanding the very top of Sneftel's snippet:
Quote:
class FooComponent{    SomeComponent(shared_ptr<Entity> entity);    ...};

I can't decide what that is, or meant to be, I'm verging on sayings it looks like a typo so FooComponent should read SomeComponent, and then that makes it a constructor?
well maybe a quick overview to the ideas behind the component-based system would be helpful. Here are a couple of links explaining how others envision it. Maybe these will get the creative juices flowing.

This first link is a simple walk-through of a component-based hierarchy showing some of the advantages it could potentially have.
link 1

This link is just a broad overview comparing the traditional deep entity hierarchy against a component based one.
link 2
Quote:Original post by dmatter
Perhaps its me, I seem to be having a bad day reading code [dead], but I'm having trouble just understanding the very top of Sneftel's snippet:
Quote:
class FooComponent{    SomeComponent(shared_ptr<Entity> entity);    ...};

I can't decide what that is, or meant to be, I'm verging on sayings it looks like a typo so FooComponent should read SomeComponent, and then that makes it a constructor?

Whoops. Fixed.
The system I'm trying to design:

Essentially an entity is a container for components whose responsibility is to route events between components. The idea is that a component is a chunk of logic with a set of output events and a series of input events to which it responds.

Components then don't care explicitly about other components, they just use the more generic events which other components create and pass across the Entity bus. You could, therefore, throw any component into any entity and gain some/all of it's functionality (depending on what other components are also present).

One downside here, of course, is the extra overhead of event passing and catching. But I think the upside, is modularity and (potentially) threadability. w/r/t the latter, right now I have each entity guaranteed to have it's components all processed in the same thread, but going forward I may toy around with using components as the logical unit of threading. [The bigger idea of the project is a job-based multi-threaded, multi-core design]

The other downside has been actually trying to code the Components. I'm so used to the paradigm of m_pathfinder->GetNextSubGoal(), that trying to abstracting that information flow through event messaging has been difficult to orchestrate. Some of those problems have resulted from poor breakdown of tasks into components, but some (i hope) is just the challenge of a paradigm shift.

-me
@vtchill:
Thanks for the links, I should have been clearer, I do understand what a component based system is when I said about finding it hard to understand I was referring to Sneftel's specific implementation.

@Sneftel:
Ok, that makes things easier (although I had assumed it was a mistake) [smile].

Is the hash-map really a hash-multi-map?
I ask because with your initial component based model an entity could be composed out of many components, with your new arrangement it's not clear how an entity can be composed from more than one.

Quote:The thing I like most about this is that there's no need for the components to swim back to their subsystem. In fact, they generally don't need to know about the subsystem at all. More subtly, in the case where one component type (BarComponent, say) depends on another (FooComponent), the BarSubsystem can listen to the FooSubsystem for add/remove events instead of the World, automatically matched up to the component it cares about.

What mechanism do subsystems use to listen in on each other?

What could be really helpful is to see in comparison a deeper explanation of how this new system fixes the two major problems you identified with the first model.


Edit: *Smack forehead* - It's just clicked how an entity is composed from multiple components [lol]
Quote:Original post by dmatter
@Sneftel:
Ok, that makes things easier (although I had assumed it was a mistake) [smile].

Is the hash-map really a hash-multi-map?
I ask because with your initial component based model an entity could be composed out of many components, with your new arrangement it's not clear how an entity can be composed from more than one.

No, it's really a hashmap, because although a given Entity may be associated with many Component-like things, it's generally associated with only one FooComponent. For BarComponents, you'd have a BarSubsystem to hold those for each Entity. (I should note that in my actual codebase none of these things are called "subsystems"; it's just a name which makes sense here.)

Quote:What mechanism do subsystems use to listen in on each other?

It varies by the exact relationship. One example: My pathfinding subsystem makes frequent nearest-neighbor or all-in-range queries to the spatial culling subsystem, specifying a point in space and getting back a list of entities near that point. The PathfindingSubsystem knows about the SpatialCullingSubsystem, but it doesn't know about SpatialCullingComponents. In other circumstances the opposite is true; a PhysicsComponent might know about the AnimationComponent to feed in blended dynamics, but wouldn't know about the AnimationSubsystem. Those take care of the first bullet point I mentioned.

Quote:What could be really helpful is to see in comparison a deeper explanation of how this new system fixes the two major problems you identified with the first model.


Quote:What could be really helpful is to see in comparison a deeper explanation of how this new system fixes the two major problems you identified with the first model.

The spatial culling system is a good example. You can think of a SpatialCullingComponent as an Entity's "assigned representative" for the spatial culling subsystem. When the entity is added to the world, rather than some factory system giving it the component, the SpatialCullingSubsystem says "hmm, a new Entity. I'll have to assign it a representative." That representative is then responsible for reporting positional changes back to the subsystem so that the quadtree can be changed. The Entity has no idea that anything is happening, other than a few listeners being added to its events. Since the Subsystem, not the factory, is in charge of the Component's creation, the Component knows about both the Entity and (if necessary) the Subsystem at the moment of creation, and the Subsystem knows about the Component. That takes care of the second bullet point.

As a side note, notice that there's no base Component class. I suppose there's some common functionality that could be factored out to a superclass, but the different types of components are never used interchangeably. The same is true of Subsystem.

This topic is closed to new replies.

Advertisement