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
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.
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_ptrsolution, 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?