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 theshared_ptr
s in some kind ofComponent::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 sureterminate()
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
andweak_ptr
: Internally, in the engine, components are stored asshared_ptr
s; but the outside (i.e. components) can only getweak_ptr
s. The downside, here, is that all components have to callweak_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 theshared_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_ptr
s 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?