3
\$\begingroup\$

Lately I've been working on a small personal project which is basically an Entity Component System framework with autoupdated Systems.

While I have a pretty good idea on the way the framework should work, because of lack of experience I am having trouble with actually keeping everything decoupled.

Some details on the framework:

Each entity is defined by it's components.

Systems are responsible for the actually modifying the entities by changing their components.

In order to improve locality of reference, instead of keeping each component in the appropriate entity, all components are stored in homogenous vectors and each entity keeps a list of indices to each vector.

Since each System modifies specific components, it should keep a list only of the entities with the corresponding components. This 'registration' to the Systems happens whenever a component is added or removed from an entity.

How I dealt with all of this until now was to have a ComponentManager, an EntityManager and a SystemManager. These classes however have very tight coupling with each other. The EntitManager needs to have access to the ComponentManager in order to handle the size of the the index lists and the mapping of each component type to them. It also needs access to actually add the components in the appropriate vector. Another coupling is between the EntityManager and the SystemManager. Whenever an entity is created, it needs to be added to the list of the appropriate Systems.

A general event bus would appear to help but I am not sure how to implement it without making it global.

How do I improve this design by removing coupling while still maintaining the system's functionality?

\$\endgroup\$
2
\$\begingroup\$

Class coupling can occur when you have classes that work collaboratively to provide functionality that the classes alone couldn't otherwise.

One way to avoid coupling is to abstract the coupling away into a layer above the classes in question. For example, remember that the classes you are talking about are part of a larger system called the Game Object System. You could easily have a ObjectManager class that brings the classes you describe together which 1) exposes a public API and 2) takes on the burden of some of the coupling needs you want to avoid.

The first benefit allows you to freely alter the internal class interactions between EntityManager, ComponentManager, and SystemsManager without impacting the external code. The second allows this ObjectManager to implement methods such as ActivateEntity() that internally manages the interactions between classes like EntityManager and SystemsManager for a specific use case.

Another means to decouple the three classes is to leverage the observer pattern where appropriate. For example, an EntityManagerListener class could expose some callback methods that the SystemsManager implements by subclassing the listener. During startup, the SystemsManager registers as a listener to the EntityManager and now when the entity manager fires specific events, the SystemsManager and any other class of interest can be informed without any coupling. Similarly the ComponentManager could implement something similar to adjust it's internals based on things that happen with the EntityManager. None of this requires an event bus, just one class maintaining a vector of listeners and iterating them during specific cases where event notifications are to be done.

But if you find yourself where the inner workings of one class require a considerable amount of information from another class, then perhaps the relationships you've built are too granular and perhaps should be refactored into a single class.

\$\endgroup\$
1
\$\begingroup\$

Interfaces are probably the answer. Your ComponentManager, EntityManager, and SystemManager do not really need each other, they only need each other's types. But an object doesn't have to be a type definition. It can be merely an implementation of a type by fulfilling an interface. Your objects can serve their current roles, but they still can be removed and replaced at will. This can be done via injection, or any number of other constructor-type patterns you want to implement.

Sometimes systems need each other; it's unavoidable. Make each of these items an implementation of a type (by interface), rather than a definition of a type, and they will be as decoupled as is possible.

And if you desperately want to avoid globals, there is always the infamous Singleton (also a constructor pattern).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.