1
\$\begingroup\$

In the game I have, I'm using an architecture close to what I've seen of Unity components :

Class diagram: Universe has many Entity, which has many Component

Basically, the universe implements the game loop, entities implement and update method, which always delegates to each entity's components.

Components are where the actual data and behaviors sit, quite like a MonoBehavior in Unity I guess.

And I use this architecture to model my whole game.

Class diagram: Player, Enemy and Bullet extend Entity, while Transform, MovementController and HealthBar extend Component

I use child classes of Entity as prefabs:

class Player extends Entity {
   constructor() {
      this.addComponent(new Transform())
      this.addComponent(new KeyboardControlledMovement(()
      this.addComponent(new PlayerCanvasRender())
      this.addComponent(new Gun())
   }
}

So, I get this kind of Universe - Entity - Component tree :

Object diagram: Player has Transform, KeyboardControlledMovement, HpBar, PlayerCanvasRender and Gun components, Gun creates Bullet entities, which have Transform, LinearMovement, BulletCanvasRender and InstantRammingDamage components

Question time!

Aside from the entities and the components, there exist Settings, such as ell the key bindings, the reference to the output canvas, the player buffs, ...

They all have to reach various components, and I don't know how to get them there.

Object diagram: KeyMappings settings are injected into KeyboardControlledMovement component, PlayerHpFactor data is injected into HpBar component, OutputCanvas reference is injected into PlayerCanvasRender and BulletCanvasRender components, PlayerDamageBooster data is injected into either — cannot chose — Gun component, Bullet entity, or InstantRammingDamage component


So far, I've thought of several possible solutions:

  • Passing data in parameter of the entities' and components' constructor would be a KISS way of cascading external values. It looks like — if it isn't actually — manual dependency injection. However, in order to — as an example — feed the BulletCanvasRender with a reference on the canvas it has to draw on, it has to be passed through the Gun component, which has nothing to do with rendering; I'm mainly afraid of code cluttering here.

  • Until now, I've been using the universe as a global crap-bag. This is great since it eliminates the boilerplate associated with manual dependency injection, but it makes the universe initialization function quite large — plus, I have to come up with names for all those new settings properties...

  • Another option would be to switch to real ECS architecture by completely separating the raw data, which would stay in the components, from the behavior, which would go into external systems, and make use of the external settings from only within the systems. I would still have to perform some kind of manual dependency injection, but it would be limited to a set of non-nested systems. I don't like this solution either since it would cause quite a big change in the code.

  • Using an DIC, or a service locator to create all the components — or at least those that require injected data.

\$\endgroup\$
  • \$\begingroup\$ IMO, the main problem here is you are trying to have engine do what scripts should be doing and mixing game logic with things like physics together. I am afraid you will need at least some degree of separation between low-level things like rendering and game logic like buffs. \$\endgroup\$ – wondra Jul 28 at 10:33
1
\$\begingroup\$

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).


Passing data

To answer you question: All of the above.

  1. "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.

  2. "Using an DIC, or a service locator", you can have it takes information from the blackboard.

  3. "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...

  4. "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!


Physics

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:

  1. 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.
  2. 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.

\$\endgroup\$
  • \$\begingroup\$ Thanks very much for your answer. I'm currently in the middle of making my own — and probably terrible — ECS architecture.I have the impression I gonna end up with a lot of systems, perhaps one per component, is it a flaw? I also have troubles with the colliders: a circle collider and a polygon collider are both colliders; though they clearly hold different data, thus the inheritance. I've got the impression that the collision system will be a big switch instanceof, or maybe I'm missing a point? \$\endgroup\$ – Odepax Aug 4 at 20:09
  • \$\begingroup\$ @Odepax a component type per system is not a flaw. In practice it is common to have multiple systems use the same component, or a system that uses multiple components, the ECS architecture will give you that flexibility when you need it. There is a chance your systems are too big, and you need to think about sub systems (composition of systems). \$\endgroup\$ – Theraot Aug 5 at 5:56
  • \$\begingroup\$ @Odepax I have seen collision handled like that, it works. For an alternative, use the collider type pairs to look up the collision method to handle them in a hashtable or similar. However, depending on the language you are using, that may or may not be a good idea. Edit: if the language does not have delegates or lambda types, you can use an interface an service classes. If it requires late binding or reflection, the switch is probably better. \$\endgroup\$ – Theraot Aug 5 at 6:04

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.