3
\$\begingroup\$

I built a standard C++ entity/component system, a bit like the one from this question). Components can access other components through their associated Entity. This is usually done by getting some kind of reference/pointer once, during component initialization. For example, a Jetpack component can access a RigidBody component, and use it to apply force to the entity. Components can be removed, during a scene, but most often, they don't. Subsystems, like Renderer, PhysicsEngine, and InputManager each listen for specific types of components, and processes them during each frame.


How should these components access each other? I see several solutions, but no good ones.


These are the potential solutions:

  • Raw pointers: Fast, but violates the idea of RAII. How do I check, if a component is deallocated? It feels like an unsafe path.
  • shared_ptr: This seems to work rather safely. I can live with the fact that it is a little slow, due to reference counting; the pointers are usually only setup during initialization, anyway. What about cyclic references? If two components reference each other, they'll never be deallocated. This can be remedied by manually clearing the shared_ptrs in some kind of Component::terminate(), that is called when the component is removed from the scene. The idea of "cleaning up", manually, seems to defeat the purpose of using smart pointers in the first place. Also, making sure terminate() is called at the right time adds complexity, as I cannot use the destructor - it's never called, due to the cyclic refs.
  • shared_ptr and weak_ptr: Internally, in the engine, components are stored as shared_ptrs; but the outside (i.e. components) can only get weak_ptrs. The downside, here, is that all components have to call weak_ptr::lock() each time they want to access another component, which could be several times each frame. This is also true for components that use components that are known to exist throughout the entire scene, and thus don't need an existence check. Contrary to the shared_ptr solution, this performance penalty is incurred every frame, instead of only at initialization. Maybe it is worth it overall, but it just seems clumsy, and potentially very slow.
  • Avoiding direct access: By adding a level of indirection between components, via a message system of some sort, the problem of cyclic dependencies may never emerge in the first place. I haven't experimented with this, as it seems to be overly complicated. Could this be overly complicated?
  • Avoiding cyclic deps: I'm not quite sure how do it, but maybe you could just forbid cyclic shared_ptrs altogether, in some way.

Can the downside of these solutions be remedied, or are there better ways to handle cyclical dependancies in an entity/component system?

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Messaging is a common approach, and there are a number of questions on the site about it if you want to do some more research. \$\endgroup\$
    – House
    Nov 14, 2013 at 16:40
  • \$\begingroup\$ (zeroing) weak references are the way to go, unfortunately you're using C++ which makes this a much less natural thing to use compared to Objective-C. Also, cyclic dependencies almost always point to a design flaw in your components. Use delegation to avoid such issues, that way one component can make calls to methods on an unknown object IF that object (the other component) simply implements the delegate protocol/interface. \$\endgroup\$
    – CodeSmile
    Nov 14, 2013 at 17:43
  • 1
    \$\begingroup\$ Also you can make things easier through requirements. You can have a RigidBody anywhere at anytime without any other component. But you can't have a Jetpack without a RigidBody. You can make it illegal to add a Jetpack component to an entity that doesn't have a RigidBody, that makes raw pointers still reasonably safe. Also a common principle is not to add/remove components at runtime, but rather enable/disable them - that way you don't need to deal with broken pointers nearly as much. \$\endgroup\$
    – CodeSmile
    Nov 14, 2013 at 17:46

2 Answers 2

4
\$\begingroup\$

Raw pointers: Fast but violates the idea of RAII. How to check if a component is deallocated? It feels like a scary unsafe path

Raw pointers are a perfectly valid choice. All that using a smart pointer would do is move all of the ownership and dependency logic out of the component code into member variables, convoluting your ownership structure significantly and adding some minor performance overhead. There's little reason to add this complexity and bad design just to protect a programmer from a rare, easily diagnosed bug.

Raw pointers from component A to component B are perfectly valid so long as you know that component B lives past the use of said pointers. You can achieve this in a number of ways. One good way is to simply make your component system express dependencies explicitly. If the object code knows that component A depends on component B then it can disallow the removal of B until A is removed. Destruction of objects should simply not use the pointers so that destruction order is unimportant (which is easy and logical to achieve; you really don't need other components during destruction in any sane component).

If you are worried about components having optional dependencies just be sure to have an OnAddedComponent and OnRemovedComponent method or your components so they are notified when other components on the same object come or go. You can hook up or null out component pointers here.

Your components should not have too many dependencies to worry about, either. There's a few places dependencies make sense. For the other cases, either decouple components entirely, merge components that depend on each other, or use messages to make dependencies weak (messages allow all kinds of other benefits like observability to outside systems, too).

\$\endgroup\$
0
\$\begingroup\$

One way is to store actions in the data. This might only be a theoretical idiom which can be combined with messaging to be more practical in some situations. As a rule of thumb, you might use messaging when just the occurrence of an event matters, and data members every once there is information attached to an event.

In your example, add a member to the rigid body type holding the onetime acceleration. The physics component would add this vector to the body's acceleration and set it back to zero afterwards, on every update.

rigidbody->speed += rigidbody->acceleration;
rigidbody->acceleration = vec3(0, 0, 0);

Other components, like your jetpack component could then add some amount of acceleration to a rigid body.

rigidbody->acceleration += vec3(12.3, 4.5, -0.3);

This approach though requires all your data to be global or at least accessible from within all components. You could then write components that are both, completely stateless and independent of each other, which might be valuable.

You might also want to read Component based game engine and dependencies which has already been asked here.

\$\endgroup\$

You must log in to answer this question.

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