Skip to main content
GameDev.net gamedev.net
🔒 Locked

Component Based Game Architectures (examples?)(more info?)

Started by Nohup Apr 22, 2007 at 6:04 PM 16 replies 11.7k views
Original Post
Nohup
Nohup
I recently read through 2 articles in GPG5 & 6 that explain the theory, and implementation of a component based game architecture... (quick explaination follows) -------- 100% composition based game objects, no inheritance tree. (an elf "has a" health-component, a location component, a magic component, etc...) Very loose coupling between game objects/components. The only member function that gets called regularly is "Component::ReceiveMessage(msg)" -------- I am completely in LOVE with this architecture. It simplifies SOOO many issues. Inter-object interfaces are simple now, and problems with inheritance trees just vanish. I've proposed this architecture for a couple projects at work now, and everyone who sees it seems to agree that it's great for what we do. Albiet there's some slight performance loss which is a tradeoff for the huge amount of flexibility. ANYWAY... On with the question. I'm looking for more info on/examples of this architecture. Particularly, I'd like an optimal way to handle the message passing between objects (message types or an enum+data). As well as some examples of how people chose to break their game objects up into components. I've waded through 200+ google results without finding much. Anyone out there seen a good component based system that worked well? Or just feel like commenting on the architecture? Post away.
Barius
Barius
Sorry, I don't have any more information about this architecture - every article I've seen about this topic on the internet has been extremely vague - but why is the following

Quote:

The only member function that gets called regularly is "Component::ReceiveMessage(msg)"
--------


supposed to be a good thing? Do you really need to have the same super-narrow interface for each component, and doesn't it make debugging harder?





Nohup
Nohup

Perhaps the "100%" is overstating things a bit... The components will undoubtably have more member functions that deal with creation, initialization, deletion, book-keeping, and possibly some specific functionalities. But most of the day-to-day operations of the components will consist of receiving messages, processing them, and sending out more messages.

I never thought of the message passing portion of the architecture as a "super-narrow interface", if anything, the limitless number of message types, makes for rather open-ended communication.

The message based interface seems to me, to be a very worhwhile abstraction. Every component can send data to every other component, without even knowing the other component's type, or how/if it will respond to the data. The components' functionality is encapsulated behind a single member function that says "if you do something important, let everyone know, and we'll do, whatever it is we do".

The downside to this, (and every) abstraction, is of course, efficiency.
Every message passed between components results in a virtual function call, and a switch statement for the receiver to decide what to do with the message. Not an odious amount of processing, but more than a simple member function call.

As for debugging... well, I think that's a wash. When something goes wrong, it may be harder to narrow down who caused what to happen. All you'll really see in a debugger is "Object X's Y component got a Z message, and did something stupid". Not a lot of tools to trace back to find out who caused the problem.

But at least with a message passing system, you can build some FANTASTIC logging tools into the code. You can dump a file full of who-sent-what-message-to-who. Or, you can create debugging components to listen to messages and report what's going on behind the scenes. More importantly, you can inject messages into the system at any time, and make sure things respond appropriately.

Well, this response went a lot longer than I meant for it to, but, I could talk about this stuff all day.
ToohrVyk
ToohrVyk
I am somewhat dubitative about this design. It seems to fall into the category of "I don't know what I'll need yet" systems, which is a very good thing to have—early commitment to a fixed specification is dangerous. However, it aims at being a permanent system, and achieve flexibility by being overly abstract, which means that it will not take advantage of any new information that may appear during the design—it was designed to be independent of such information from the start.

In short, a component-based system forces you to remain most of the time at a fixed abstraction granularity (all objects are Components) where more precise granularity levels may be more relevant (an object communicates with its renderable avatar, for instance) and would be statically available to any other design. I would rather keep the variable abstraction granularity, even if it requires frequent rewrites of the system, than commit from the beginning to a component-level granularity.

As for the implementation, most object-oriented languages already implement message-passing in a very quick and efficient way: methods. I would make all components inherit from a base Component class, which would have public member functions for every single possible message. These functions would perform some bookkeeping (such as logging) and then call protected virtual implementation functions that would execute different actions on a per-object basis. Last but not least, you could always implement a Message class which would call the relevant member function as a side-effect when prompted to, for handling storing of messages if it proved really necessary. The resulting system is as fast as any dynamic message-based system can be, because it uses the virtual table as a faster switch, and besides, it's both easily extended and type-safe.
James Sutherland
James Sutherland
I co-designed/implemented the component system that's described in one of the GPG articles (I forget which volume, but it's the article by Bjarne Rene), and the system really did work very well for us on the whole. Though it did change somewhat from what's described in the article. Nothing fundamental, though.

I think, in the real world, you're going to find you'll want more than just message passing between the components. I can see how you'd like the idea conceptually, but I imagine it'll be annoying in practice. What we had was, effectively, a QueryInterface on the component database with indivudual components implementing named interfaces. So if you had a couple of components that were fairly tightly coupled and need access to each other regularly, they could get pointers to each other and just call methods directly rather than going via the message passing mechanism.

The message passing itself was very simple. Messages resolved straight away. They weren't queued up or anything like that. So debugging isn't a problem, because you have a call stack. I did put some additional debugging support in for use by designers, however, which let them set breakpoints on game logic-related messages (ones they were setting up in the editor) and single step through the game so they could see exactly what's going on in the system.

Component-based architectures do require a fair bit of thought and initial design work, but I'm fairly convinced we made the right decision. Towards the end of the project, adding new features and object types was incredibly quick and easy. The system was fairly data driven, so often feature requests could be sorted without even touching code (just plugging an existing component type onto an object template and setting a few default values).
oliii
oliii
Makes it more sensible for distributed systems and networking. But that's at a higher level than just components usually.
Everything is better with Metal.
Anna
Anna
These links might be helpful:


A Data-Driven Game Object System (slides)

Introduction to Dungeon Siege Architecture

Building Object Systems - Features, Tradeoffs, and Pitfalls (.ppt)

Refactoring Game Entities with Components


Searching for information on data driven systems might get you some useful results as well.

I'm in the middle of implementing a system like this but my progress is slow; I don't have any experience implementing such complex systems as this one.
Nohup
Nohup
Thanks for the links Anna, I'd not seen a couple of those yet. Some good information there.


It was a nice surprise to read James Sutherland's response. I wasn't expecting to hear from anyone so close to the issue. The system presented in GPG5 is a great piece of software engineering. It's provided a lot of inspiration to me, and some other guys here at work who admire good software.

If I could pose a couple further questions about the GPG5 architecture, there's a few things that have always been a bit of a mystery.


1) The fact that a game object can only have one component that provides a given interface. (i.e, you can only have ONE component that provides the IHealth interface) This seems to be a speed/functionality tradeoff, but has this decision ever been a limiting issue during design or development?

2) Do you have any advice for implementing a composite, or compound component? To work around the limitation in question 1?

3) Header files... One of the nice things about this architecture is the loose coupling. The only place where this isn't the case is the CComponentManager. It requires the inclusion of every component's header file. That could get to be quite a large list of includes. Has this ever been a problem? Any way around it?

4) Any advice for handling the "cached" component pointers that will be held in various components? Some clean way to insure that deleted components are never accessed? A shared pointer, or something equivalent?

5) Do you have any idea how well this architecture scales up to very large numbers of game objects and components? At what point does the overhead start to become a problem?


Thank you for your time on this board. It's always great to hear from the professionals in the field.

James Sutherland
James Sutherland
I can't honestly remember a case where the single instance of an interface per object actually presented a problem. Though we were writing a shooter game, not something as complex as an RPG or anything like that. Although we had quite a lot of component types, the actual objects tended to be quite simple on the whole.

The component manager include mess was quite annoying, though it's hard to really see an easy way round it. After the project was over, as I was rewriting chunks of the component system, I ended up using macros to autogenerate a fair chunk of the messy registration stuff. It's not ideal, but at least it kept it looking neat.

As an aside, one really useful improvement we made after the article was done was to get rid of QueryInterface, and replace it with a static method on each component that looked something like:

class CMyComponent : public CComponent{   ...   static CMyComponent *Get(CHash objectName); // Return the component of this class from the given object};


This meant that all the calls that looked like this:

CMyComponent *pComp=static_cast<CMyComponent*>(GetComponentManager().QueryInterface("myobject", IID_MYCOMPONENT));


Turned into:

CMyComponent *pComp=CMyComponent::Get("myobject");


Which, if nothing else, looks a lot nicer. Again, this ended up being wrapped in a macro, so components themselves didn't need to remember to implement the function. They just had something like DECLARE_COMPONENT(ComponentName) at the top of their class which generated all the common stuff that all components need to have.

The cached pointers weren't too much of a problem for us. The only handling we had was that you could register pointers with the component manager, and it would NULL them out for you when the component they pointed to got deleted. This seemed to catch any potential problems easily enough. We didn't allow the components attached to an object to change during its lifetime, so the only place it was normally a problem was the odd order-of-deletion type of bug. As, generally speaking, components only tended to cache off pointers to other bits of the same object.

I'd have to ask Bjarne for numbers, because I don't have them to hand, but running on the PS2 we were getting away with a few thousand components at any given time without it having a major effect on CPU time. Honestly, the main CPU cost was spawning of new objects rather than ticking existing ones. Given more time, that would have been fixable but as it was, there was too much parsing and processing going on when components were created. Ideally it should just be a mem copy of pre-initialised data, and a few pointer fixups.

I think the main performance problem (especially on previous gen systems) is that the system as presented in the article is really cache unfriendly. I have a vague idea that this could be improved by clustering all the data needed for updating components of the same type together, and making sure you run all their updates in a single pass, so you should be making good use of both the data and instruction caches. But I've not thought it through enough to work out how practical it is.

I had a load of notes on how I was going to rework the system for our next game, plus the original system had changed a bit between the article being written and the game shipping. With Circle going bust a few weeks ago, I'm not sure how much of this stuff I still have left, but I can try to dig out what I can and post anything that might be of interest.
JasonBlochowiak
JasonBlochowiak
This smells more than a bit of golden hammer.

Components are a useful tool, but relying on them entirely seems like overkill, especially for base items (like location) that almost all of your entities are going to have seems silly.

Inheritance, when used well, is a A Good Thing, and not some inherently evil thing to avoid.
James Sutherland
James Sutherland
Oh, absolutely. We certainly used plenty of inheritance in our component-based system. Similar components inherited interfaces and behaviour from each other quite a lot. What we didn't have was a single big inheritance tree that every type of entity was part of. Essentially, you have a number of smaller, flatter trees from which you pick components to plug together, rather than a single big tree.

I'm not interested in advocacy, and I certainly wouldn't suggest that this is The One True Way for building your game entities. All I really can say is that it worked pretty well for us.
Dancin_Fool
Dancin_Fool
Yeah I love the component based system. Though I definitely agree that a system should not be completely component based. Sometimes theres no need for it for very simple things where speed is very important.

It is definitely a useful tool though and very powerful when used in the right places.
SanityAssassin
SanityAssassin
There was a very, very, very lengthy discussion on this several months back in the sweng-gamedev mailing list. The question on this list first originated with Jamie Fristrom on Oct. 16, 2006. Go to this link:

clicky

click the October 2006 link, and look at the "strategies / patterns for composition" thread. Lots of info and thoughts on the subject.

JasonBlochowiak
JasonBlochowiak
Quote:
Original post by James Sutherland
Oh, absolutely. We certainly used plenty of inheritance in our component-based system. Similar components inherited interfaces and behaviour from each other quite a lot. What we didn't have was a single big inheritance tree that every type of entity was part of. Essentially, you have a number of smaller, flatter trees from which you pick components to plug together, rather than a single big tree.

I'm not interested in advocacy, and I certainly wouldn't suggest that this is The One True Way for building your game entities. All I really can say is that it worked pretty well for us.


Interesting. I'm actually using a single inheritance tree for every entity, but using non-generic components to build the behavior of each of those entities.
Xai
Xai
The fundamental idea I want to reinforce is that a single open ended "RecieveMessage" type system is great for anything which needs such power, but noticably interfere with / abandons all static / compile time advantages developed over the past 40 years.

In fact, it is fundamentally saying that you want a smalltalk / ruby like design (pure message passing, with runtime exceptions anytime a message can't be understood by the receiver) rather than any compile time phase at all.

Now, on a different front, the BeOS (Be Operating System) used such an interface for the BLooper and BWindow classes (just like the WndProc in windows) but did so in a lighter weight, more flexible way than windows, and added the rule that all loopers and windows run their own thread. So the BeOS got the benifit of multi-threading desyncronization across this overly powerfull interface. When something like this:

SendMessage(new ShootMessage(23, ..., ..., ...));

is called for everything instead of:

Shoot(23, ..., ..., ...);

your just adding indirection into the core chain of handling. And it assumes that somehow everyone who wants to "hear" the fact that a shot was fired was able to "plug in" to the stream of messages it was sent out on.

This really isn't much different than just changing the registering for specific events from specific objects like .NET does, to registering for general event streams from objects ... for very doubtfull gains in most uses.

There are however cases where it can really work great. I personally use message streams taylored to specific message classes often (I have a stream of program events that a logger or error display can tap into. I have a stream of gui events that the sound systems can tap into to cause common (non-positional) sounds to be played, etc). But you always have to balanced strongly typed specifically targetted and compile time checked against loosly coupled, broadly sent, runtime handled. Each has their repercussions for everything.
LoneDwarf
LoneDwarf
I am also using a component based object system and I must say I am liking it for the most part but there are a couple of negative points that could be raised. First my objects are actually a bag of components, so the query interface (QI) is part of the object. I think some others are using an object ID and tables of components like a database might use.

I would first say that the statement "When the only tool you have is a hammer, everything looks like a nail” can be a good description of a component system. It's very easy to get a feature request for the game and then run off trying to figure out how to write a component that fits the feature. Thinking of a component system as just a way to implement QI and maybe data layout, is what I have fallen into and I think it has saved me from writing ridiculous components.

I have also wondered about the problem of implementing more than one of the same interface within an object. I agree that it's very wise to worry about it when it becomes a requirement. If I get cornered into it, I suspect I will write an interface that handles the problem. You really need to take this attitude or nothing will get done. An example of a mistake I made along these lines is parenting. I figured it would be a good idea to add a parent child relationship to my base object. I did this before it became a requirement and I paid for. It was more code to manage and just cause problems and didn't really solve anything. In the end I ripped it out and won't put it back in unless there is no other way around it.

I don't think a component system will cure any inheritance problems, I think it just minimizes it somewhat or maybe only hides it. You still will likely have abstract interfaces for stuff like 3d models, cameras, etc.

I must say I am hating the pattern of writing very generic code like IObject that has stuff like onUpdate, onRender, etc. This is pretty much what a component system is. It leaves you with one very pretty piece of code that loops through all the objects and lets them update. Thrashing through cache, touching objects asking them if they want to update. Then you spend the rest of the code trying to figure out what each IObject really is. Sorry for the rant :(

Keeping in mind that I am liking my component system but if I was doing a smaller project , there is no way I would use one. You pay for it in the debugging and code management. Using a simple class hierarchy with interfaces works more than fine if you don't have heavy requirements. It's very easy to fall into over engineering code.


Cowboy Coder
Cowboy Coder
Quote:
Original post by Tesshu
Keeping in mind that I am liking my component system but if I was doing a smaller project , there is no way I would use one. You pay for it in the debugging and code management. Using a simple class hierarchy with interfaces works more than fine if you don't have heavy requirements. It's very easy to fall into over engineering code.


I agree to some extent, I only came to use components after the existing system became too unwieldy. I would certainly do it again if I was to do any game with a large enough variety of objects.

It's not all about aggregation vs inheritance, you also have to think about how message passing and data binding fits into the whole model. You also need to be prepared to make some comprises from a "pure" model, for speed consideration.
Antheus
Antheus
One way to handle compile time checks is by passing concrete message classes, rather than base class.

handle(WindowMessage &msg);handle(ButtonMessage &msg);handle(XMessage &msg);// even catch alltemplate < class Message >handle( Message &msg );


This has its own problems, especially overhead when messages need to be stored, but it does offer decent compile time safety, and will produce nicely inline code in many cases, if messages aren't dynamically allocated, then the overhead is severly reduced as well, sometimes entire message class won't be even instantiated.

The main drawback it has is that it can make dynamic subscriptions to handlers difficult or even impossible.

This aproach is somewhere in the middle between generic messages and signal/slot aproach. This type of messaging can also be implemented with any type-safe signal/slot aproach, rather than binding parameters directly, you assume all slots take exactly one parameter.

One advantage this does offer is much lesser overhead for dispatching a message with many parameters to many consumers. Straight signal/slot that uses templates to call callback functions needs to pass all parameters to each callback called. With many parameters, this can slow things down (but doesn't need to). Here, there's always one single reference that gets passed around, and callbacks only access what they need to. And since it's mostly compile time, it can get optimized better.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.