Will you have networking?
I understand that you are probably making a single player game engine. However, thinking about how to deal with the network very, very, very early in the desgin will push you a better architecture... even if your game will not be networked anyway.
Think of a server-client architecture. If you were to build that, you would want to be able to send information about the enetities from the server to the client. To do this, it is convinient that components have value semantics. And for that, it is conveninient that they do not have behaviour and do not depend on external systems (eg. canvas).
The server would have systems that handle the behaviour, and the client would deal with IO (keybindings, rendering, etc...), but they need to work on the same data.
Thus, you implement a "real ECS architecture" plus client-server synchronization of components and you are good to go (well, netcode gets more complicated than that, but that is a great start).
To answer you question: All of the above.
"using the universe as a global crap-bag" sound like you should use a Blackboard pattern. Your knowledge sources would be the system that handle the configuration files for keybinding and other settings. You can also have knowledge sources for information generated in game, or even for user input. All the knowledge sources post into the blackboard. In the pattern the control is supposed to call the knowledge sources. The game loop will do that.
"Using an DIC, or a service locator", you can have it takes information from the blackboard.
"Passing data in (...) constructor"
You can start your by adding the constructors and passing data down the hierarchy.
You then change the creation of components to a call ot
Universe.Create or something like that. Then universe looks at the blackboard, picks the data, passes it in the constructor... but there would still be things that needs to be passed from the parent object.
You can pass a resource locator as parameter in the constructor, so that components can take from it the data it wants, while the parant object can pass other data in other parameters of the contructor.
However, do not pass externals to components. Instead...
"switch to real ECS architecture". Did I say components above? Oh, no. That is how you create systems, not components. Your systems should create the entities and attach the components. Then your game loop calls the systems, the systems query the entities depending on the components, and they run some code.
Your knowledge sources are systems!
Let us pretend we do not want to use systems.
Currently, you would have entities that has
Transform, which I am guessing defines thier position. I suppose you would want to add colliders for physics. So, let us pretend you have those too...
Perhaps you want to add a physics component. Since you do not have systems. Perhaps the physics component has the colliders
Your physics simulation would update the transforms. No problem there. However, you also want to check for collisions. Hmm...
Somehow, you need the physics component to get a list of nearby objects to check collisions against. How?
Besides, that physics component would run per entity. If no information is shared among them, one entity A would find it collides with another entity B, and then entity B finds it collides with entity A. You find the collision twice.
Thus, you need to break encapsulation of the components. They need to know where each other is, and who collided with who. That is not a good design.
Let us pretend we do want to use systems.
The game loop calls the physics system. The physics system queries all the entities with physic components (remember that these physic components are only data). We want a spatial data structure, something where it is easy to get nearby objects.
Two ways to do this:
- The Entity Component System allows you to subscribe to changes in components. So your physics system get a notification when a collider is attached, for example. Then the physics system can add the component to the data structure.
- The physics system creates and handles all the data it needs, and the components are just references.
Then, the physics system can find candidate collision pairs, check for collisions, and do resolution,
User input and scripted movement.
Pretending there are no systems
Let us say we want to be able to move some objects with user input, or we want them to move in a predefined path, or be handled by AI (path finding et.al.).
However, the physics component wants to handle movement too. That means that these components need to know of each other, and thus would be coupled.
To solve it, you a component for the position, and now the other components only need to know about that one.
Well, if you have components that just store the position, and other components that handle input or do physics simulations, or whatever with that component... you have components and systems, except you are not calling them that.
Make it a system
A system takes the bindings from configuration files, or whatever. There is an input system that needs them. You can choose if you want it to post to a blackboard or you want to pass them directly.
Either way, the input manager system takes external input. It will query for the entities with the control components. Yes, more than one. That way you can handle couch multiplayer.
Now you have a few options:
- You are making a game: implement the input manager to directly move the objects.
- You are making an engine:
- Have the input manager update the control components. Then the game developer can choose to write an input system that takes the control components and moves the position components or whatver.
- Just write an input manager and let the game developer call it from their systems and do whatever they want.
I'm sure there are more ways to do this, these are just the ones that I have come up with.
Why entities as just an id
We can implement entities as dictionaries of components. And it works. In fact, somewhere in the ECS there is something equivalent to a dictionary of components per entity.
However, you want to do much more than store components in entities.
You want to query all the entities that have certain components. For example, you want a system that looks for entities that has a position and a model to render them, and another that looks for entities that has a position and a velocity to move them. notice these overlap.
Unless you are ok with each system iterating linearly over all the entities, then those queries means that you want to create some sort of indexes of entities. And you want to make sure that updating the dictionary of components updates the indexes. That means that the dictionay of components cannot be a plain old dictionary, it is a façade to update the internals of the ECS.
Alright, what else does the entity has? Nothing, really. The behaviour should be in the systems, and all the data in the components. If you really want to have multiple kinds of entities, you can add a
Tag component. Thus, you do not want entity to be a base class (client code could extend it and use it improperly). Also, composition over inheritance.
Now, the entity needs to point to the indexes to be able to update them, and the indexes point to the entity to be able to return it. We want to decouple that. In order to do so, we move all the logic that handle the indexes and the stored components to another class (called "registry", "scope", something like that). So that the entity only has an id. Now, there is no need for the index to point to the entity, it just stores id.
The design of the "registry" (by any other name) is that of data tables. You can think of the id as the primary key, have a column for each component type and a row for each entity. There are, of course, other optimizations form that idea.