Is the Component Object Model Really Worth it?

Started by
21 comments, last by Codeka 15 years, 3 months ago
The idea that an object is no more than the sum of its parts has gotten a lot of interest from programmers. But is this design system really worth implementing? There's a reason I posted this under Game Programming, and not under General Programming. In games, objects have a tendency to be less abstract than its other programming counterparts. Objects are solid things, for the most part, like "MainCharacter", as opposed to, say, "Client", in business software. This means that in a game, the components of an entity in the COM model are less likely to be as loosely coupled as they are theoretically supposed to be. For example, lets say that our object "MainCharacter" has a "Stats" component and a "Render" component. A monster has recently attacked our MainCharacter, so the latter's hit points have dropped. The StatsComponent has received the Damage message from the monster, and has dropped its Hit Points stat. Here's where the trouble begins. The RenderComponent is supposed to render MainCharacter with an amount of blood on him that according to the amount of damage done to him. Now what do we do? There are two ways to solve this problem: 1) Have the RenderComponent query the MainCharacter for a StatsComponent and get the hit points from it. This would introduce a tight coupling between our components that the component object model was supposed to avoid in the first place. Doing this defeats the purpose of COM. 2)Adding a hit points member to the RenderComponent that also gets affected by the Damage message sent by the monster's attack. The problem here would be the use of duplicate code. Every time you use duplicate code, someone throws a shoe at the President. If the flexibility of the behavior of a component object system is what you are after, you may want to take a look at the Strategy design pattern (http://en.wikipedia.org/wiki/Strategy_pattern) rather than build a whole object from scratch. So you can’t really use run-time flexibility as a reason to use COM. So, is the component object model really worth it? Feedback much appreciated.
Advertisement
Quote:Original post by Jetjayjay

2)Adding a hit points member to the RenderComponent that also gets affected by the Damage message sent by the monster's attack.
The problem here would be the use of duplicate code. Every time you use duplicate code, someone throws a shoe at the President.


Code? Or Data?

The solution to above is to let individual component subscribe to individual data in other components.

If renderer component needs health value, it subscribes to object's stats' hitpoints value.

And yes, data gets copied and replicated a lot, but since this type of systems are mostly concurrent these days, that is the preferred method anyway. You're stuck with buffered updates anyway (read/write modes).

See the Intel's Smoke demo, which achieves high degree of concurrency by using per-member updates.

Simplest way to achieve that is, if component X needs to be notified of Y::p property changing, it subscribes to that by providing local copy on 'p'. The system, when needed, copies value from Y::p to X::p. When, at some later point X needs updating, it has what system deems most recent value of ::p.
Don't you think that adding a subscription system to the COM would make a messy system even messier?

Using the component-object model will probably end up, in the long run, being less efficient (there are a lot of dynamic_casts or other comparison functions that will be needed to implement COM) and less readable than a straightforward inheritance/polymorphism based class heirarchy. Introducing a subscription scheme to COM will cause very tight coupling between components and will require a lot of semantic programming that the system was meant to avoid in the first place. As I have already mentioned, the Strategy design pattern can achieve the same flexibility of behavior that COM can, without the need to build every object from scratch.
A bit of a ramble, but here are my thoughts. There is a definite risk that going with some kind of composition instead of classic inheritance is full of pitfalls. However, like any other engineering practice, if you craft your components with foresight the benefits outweigh the costs. When we wrote our component-based entity system, we chose to do it because it allowed us to get away from the crippling rigid class hierarchies that we had developed in the past.

Our first crack at implementing characters and objects had everything broken down into such fine-grained details that the overhead of fetching data from other components and managing state was high, albeit completely generic and decoupled. We had query messages that could ask any sibling component for a piece of data by name. We could expose interfaces from multiple components so that an 'IStats' component-interface could be implemented by different components, be them MonsterStats, CharacterStats or PropStats, and queried by interface name by client components. It was crazy, hard to manage, and hard to get right.

Then we stopped, and evaluated whether this loose coupling was ... too loose, and compared it with our original goals. One of our goals was to keep a larger portion of our code base project to project without having to rip a lot of game-specific logic out of the base classes of our hierarchies. A COM system solved this. Another goals was to allow for component-based updates, where the logic, animation, rendering, etc. could be done in separate threads/SPUs, again solved by the component approach. Additionally, we wanted to add scripted components that could work alongside C++ components for rapid prototyping as well as data-only components, quick reload and state management. Our component entity system was solving those but we had been to zealous in our attempts to decentralize our old class-hierarchies.

Ultimately, when we rewrote our components we found that our components had more logic and data in them than originally expected, but still allowed a lot of mixing and matching, and more importantly, augmentation with secondary kind of components that were more about logic, eye candy or the like. We ended up with some pretty full-feature components: model, physics, animation. Each of these had a lot going on inside them.

In the case of something like 'Stats', it would end up being broken up among the needs of the game - A lot of entities in our game had health and could be damaged, so we had a Health component instead of a monolithic Stats component that did a couple of things. I received Damage messages, responding with a 'Dying' message if health < 0. It also had a MaxHealth variable and a HealthRegenerationPerSecond variable, used for resetting objects or healing. The Render component would look for the Health component, a definite dependency, but it has to be done somewhere, but at least anything in the game could have health, and know how to respond to it.

At some point, we realized, we had to stop worrying about the the system we were writing, working in a 'must be generic' mindset, and get back to writing the game, a 'must get this game working and fun' mindset. What the component system allowed was localized dependencies between components instead of complex dependencies both against hierarchies as well as within hierarchies. Ultimately, this yielded a game-entity system where the cost of entry for new object types was very low, formalized and flexible.

I know I really missed your points, but I think the proposed 'problem' is not so problematic. At some point in development, you need to stop building the system, and build the game. I think the Stats & Render problem is an example of it. At some point you just need to write in your Render component: if ( me.HasComponent( Health ) ) DrawBlood( me.GetComponent( Health ).GetHealth() );

Best of luck.
Quote:Using the component-object model will probably end up, in the long run, being less efficient


It worked for Dungeon Siege back in 2002. That document also makes for good read on practical issues encountered, as well as solutions to them.

Many others have used such systems with success.

But yes, developing fully run-time customizable, data-driven system will have higher resource requirements than hard-coding everything.

The difference is, with inheritance, all changes need to be made directly in code. With component based system, everything can (but doesn't need to) be done by content developers who will never touch actual code.


And I'm not really sure how COM is involved in all of that. It may be foundation of Windows, but it's not really such a great technology, and is primarily intended for IPC, not light-weight data representation.
Quote:Original post by Antheus
And I'm not really sure how COM is involved in all of that. It may be foundation of Windows, but it's not really such a great technology, and is primarily intended for IPC, not light-weight data representation.


Sorry for the mix-up with the Windows COM. What I was referring to was the concept of the Component Object Model as a system, not the Windows foudation stuff.

Antheus, so you mean that data in the object that is meant to be more frequently accessed by the rest of the objects would be kept in more goal-specific and focused components, and larger components, like the RenderComponent, would be larger and less accessible to the rest of the entities' components?

[Edited by - Jetjayjay on December 27, 2008 2:11:49 PM]
Yeah I really don't recommend referring to your compoent architecture as "COM", since you're likely to inspire nightmares and flashbacks in those of us that have had the "pleasure" of doing extensive work with COM. :P
Quote:Original post by Jetjayjay

Antheus, so you mean that data in the object that is meant to be more frequently accessed by the rest of the objects would be kept in more goal-specific and focused components, and larger components, like the RenderComponent, would be larger and less accessible to the rest of the entities' components?


You have multiple types of dependencies. For example, Render system needs to work after Physics system. In trivial design this means:
while (true} {  for (IPhysics p : physics) p.update(dt);  for (IRenderable r : renderables) r.render();};

This way you hard-code the dependencies on system level. But this only gets you so far, since relations are implied.

In your case, Human has both, Health (not like Rock or Mesh or Sound) and custom renderer. And relation between Health and HumanRenderer exists between those two on per-object basis.

When you create your human, you specify all that:
Entity * entity;entity.add(new Health(0,100));entity.add(new HumanRenderer());
Of course, human renderer just adds blood, so HumanRenderer<-BasicRenderer<-IRenderer.

Renderer is opaque, but HumanRenderer needs to know about health:
HumanRenderer {  HumanRenderer() {    // dynamic cast here    this->health = this->getOwher()->getComponent<Health>();    if (this->health == NULL) this.health = Health::getDefaultInstance();  };  virtual void render() {    ....    // read an int non-polymorphically    render_blood(this->health->current_value();  }  Health * health;  // Health can be just int or std::pair<int, int> or ....}


One dynamic cast per creation, and non-polymorphic access. Coupling between HumanRenderer and Health *interface* is strong, but their implementation coupling is trivial and minimal. Interface coupling needs to be strong, since they are very closely related.

But, human renderer is now capable of rendering anything and everything with Health by overlaying blood.

Of course, things get more complicated. What happens if Health goes away (many systems don't allow components to be removed), is there really need to redraw health? What if it's cached on texture and doesn't need redrawing, how exactly is getComponent<> implemented? And so on....


Eventually, you end up with fine-grained, meta-information rich object model which declares all of the above and more.


For concurrency, copying values if often desirable. For example - renderer and logic run in separate threads. Health of all objects will change rarely, typically it will be just 2 or 3 entities which will be affected.

Logic does its stuff, and 3 entities change health. They post HealthChanged(old,new) message.
At some later point, renderer is activated. Before running, it checks for all messages sent to it and finds 3 HealthChanged messages. It sends delivers these messages to its own IRenderer components which update their local copies. Finally, it redraws the screen.

So instead of locking or copying entire state, small, per-component portions of data are sent across threads which continue running regardless of each other.


But a lot depends on implementation itself, as well as design. One size doesn't fit all, and it's hard to predict where bottle-necks will lie. But considering the original dillema - polymorphism is not the only way to realize such a system. Much of functionality can be defined as interaction between two concrete non-polymorphic interfaces.
I don't think that it's worth it yet. Trying to implement a class as a series of components nicely is just too much of a pain in current languages.

I think that it might become viable when languages allow multiple inheritance a little less shoddily, or more easily allow properties to be passed about.
So Antheus, what you mean is that the dependencies between implementations can be reduced by offering some sort of null or default object if no such component is found in the entity?
That would probably solve many of the problems with all of the coupling between components, but I'm still not convinced that component-entity programming is worth the trouble you get in return. RTTI is pretty performance-intensive, and all of the dereferncing stuff is also fairly slow (not to mention almost completely unreadable). The semantic programming involved in a component object system defeats the purpose of the system itself, which was to loosen the coupling between components of an objects. In games, as I have mentioned above, objects are not instances as abstract as they are in other software, so you will be getting a lot of this kind of code:

int myFoo = this->parentEntity->GetComponent(FooComponent)->foo;
if (!myFoo)
{
myFoo = 5;
}

The thing is, that isn't even exaggerating the way it would work. You would see that all over your code. Not pretty, and has plenty of performance overhead.

By the way, I looked at the way Dungeon Siege implemented a component object system. The way the did it, though, isn't really a true composite pattern. What they had was a built-in template system, not generic, loosely coupled components in an object. It was really more of a "fill in the object data fields" type of functionality.

This topic is closed to new replies.

Advertisement