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

Started by
16 comments, last by Antheus 16 years, 11 months ago
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.
Advertisement
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?






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.
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.
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).
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.

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.
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.

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.
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.

This topic is closed to new replies.

Advertisement