9
\$\begingroup\$

I'm not sure how exactly objects do things to other objects in a component based design.

Say I have an Obj class. I do:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

How could I then have another object not only move the ball but have those physics applied. I'm not looking for implementation details but rather abstractly how objects communicate. In an entity based design, you might just have:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

Any article or explanation to get a better grasp on a component driven design and how to do basic things would be really helpful.

\$\endgroup\$

3 Answers 3

10
\$\begingroup\$

That is usually done using messages. You can find lots of details in other questions on this site, like here or there.

To answer your specific example, a way to go is to define a small Message class that your objects can process, e.g:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

This way you're not "polluting" you Obj class interface with component-related methods. Some components can choose to process the message, some might just ignore it.

You can start by calling this method directly from another object:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

In this case, obj2's Physics will pick the message, and do whatever processing it needs to do. When done, it will either:

  • Send a "SetPosition" message to self, that the Position component will pick;
  • Or directly access the Position component for modifications (quite wrong for a pure component-based design, as you can't assume every object has a Position component, but the Position component could be a requirement of Physics).

It's generally a good idea to delay the actual processing of the message to the next component's update. Processing it immediately could mean sending messages to other components of other objects, so sending just one message could quickly mean an inextricable spaghetti stack.

You'll probably have to go for a more advanced system later on on: asynchronous message queues, sending messages to group of objects, per-component registering/unregistering from messages etc.

The Message class can be a generic container for a simple string as shown above, but processing strings at runtime isn't really efficient. You can go for a container of generic values: strings, integers, floats... With a name or better yet, an ID, to distinguish different types of messages. Or you can also derive a base class to fit specific needs. In your case, you could imagine an EmitForceMessage that derives from Message and adds the desired force vector -but beware of the runtime cost of RTTI if you do so.

\$\endgroup\$
9
  • 3
    \$\begingroup\$ I wouldn't worry about the "non purity" of directly accessing components. Components are used to serve functional and design needs, not academia. You want to check that a component exists (eg, check return value is not null for the get component call). \$\endgroup\$ Aug 27, 2012 at 17:11
  • \$\begingroup\$ I have always thought of it as you last said, using RTTI but so many people have said so many bad things about RTTI \$\endgroup\$
    – jmasterx
    Aug 27, 2012 at 20:06
  • \$\begingroup\$ @SeanMiddleditch Sure, I would do it this way, just mentioning that to make it clear that you should always double-check what you're doing when accessing other components of the same entity. \$\endgroup\$ Aug 28, 2012 at 9:50
  • \$\begingroup\$ @Milo The compiler-implemented RTTI and its dynamic_cast can become a bottleneck, but I woudln't worry about that for now. You can still optimize this later on if it becomes an issue. CRC-based class identifiers work like a charm. \$\endgroup\$ Aug 28, 2012 at 9:53
  • \$\begingroup\$ ´template <typename T> uint32_t class_id() { static uint32_t v; return (uint32_t)&v; }´ - no RTTI needed. \$\endgroup\$
    – arul
    Aug 28, 2012 at 11:20
3
\$\begingroup\$

What I did to solve a problem similar to what you show, is to add some specific component handlers and add some kind of event resolution system.

So, in the case of your "Physics" object, when that is initialized it would add itself to a central manager of Physics objetcs. In the game loop, these kind of managers have their own update step, so when this PhysicsManager is updated it calculates all the physics interactions and adds them into an event queue.

After you produce all your events, you can resolve your event queue simply checking what happened and taking actions acordingly, in your case, there should be an event saying object A and B interacted somehow, so you call your emitForceOn method.

Pros of this method:

  • Conceptually, is really simple to follow.
  • Gives you room for specific optimizations like using quadtress or whatever you would need.
  • It ends up being really "plug and play". Objects with physics don't interact with non-physics objects because they don't exist for the manager.

Cons:

  • You end up with a lot of references moving around, so it can become a bit messy to correctly clean everything if you are not careful (from your component to the component owner, from the manager to the component, from the event to participants, etc).
  • You have to put special thought to the order in which you resolve everything. I guess it's not your case, but I faced more than one infinite loop where an event created another event and I was just adding it to the event queue directly.

I hope this helps.

PS: If someone has a cleaner/better way to solve this, i'd really like to hear it.

\$\endgroup\$
1
\$\begingroup\$
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

A few things to note on this design:

  • Name of the component is the first parameter - this is to avoid having too much code work on the message - we can't know what components any message might trigger - and we don't want all of them chewing a message with 90% failure rate that converts to a lot of unnecessary branches and strcmp's.
  • Name of the message is the second parameter.
  • The first dot (in #1 and #2) is not necessary, it's just to make reading easier (for people, not computers).
  • It's sscanf, iostream, you-name-it compatible. No syntactic sugar that does nothing to simplify the processing of the message.
  • One string parameter: passing the native types is not cheaper in terms of memory requirements because you have to support an unknown number of parameters of relatively unknown type.
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .