0
\$\begingroup\$

I am new to game development. Coming across the ECS (entity-component system) pattern I generally quite liked the idea. I have implemented the current version of my engine using the OOP style approach. In other words, Components are objects that have both state and behaviour and methods that can be polymorphic. My entity manager also means that when a SpriteRenderComponent has been added to an entity, entity.GetComponent<RenderComponent>() will return a reference to the SpriteRenderComponent. Of course, the problem with this is that components have to communicate with each other. E.g. the draw method will access the TransformComponent's transform data.

Now I have dug a bit deeper and found that most sources recommend the pure ECS approach where the entities are an id or a list of components, components are just structs with public data and the systems are where all the magic happens ie where all the functions are. Of course, components don't depend on each other and the same is ideally true for systems as well.

So now when I want to draw a sprite I create a SpriteRenderSystem which knows how to draw a sprite. The problem is that drawing concerns all kinds of drawable things e.g. to sort them depending on a zOrder and layer. This is what lead me to the idea of nodes as objects that are local to the system. So now the SpriteRenderSystem would add a 'SpriteRenderNode' to the RenderManager. The SpriteRenderNode could inherit from an abstract RenderNode and implement the virtual draw method. The SpriteComponent would only hold the data specific to the sprite such as colour. The vertex array and all the other stuff that is purely related to rendering is kept encapsulated in the derived RenderNodes.

Problem #1

In order to be drawn by the RenderManager, the SpriteRenderNode would need to know and therefore store its transform data. The only way to keep this data up to date would be to copy the entity's transform component's transform data to the SpriteRenderNode every frame. I am not so much concerned about performance here but about the practice of duplicating the data in the systems. It feels like this is a violation of general good programming practices though I can't see any immediate reasons why it may prove harmful.

Problem #2

Taking the data out of the components.

From what I understand, the data should live in the components. However, taking the above-presented idea a step further, there may be cases where storing data in components isn't necessary at all. Such an example may be an animation system which may simply store and update animation nodes for each entity and then change their components such as texture, position... The problem with this is that it would take away the ability to save the game by simply serialising all the components.

As you see I am quite confused about how a good pure ECS should look like. Any help would be much appreciated.

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

First of all, there is no such thing as a good pure ECS. Every ECS system is different, and all of them have their own advantages and disadvantages.

Problem #1

I'd go with the following setup. SpriteRenderSystem would have access to an array of SpriteComponents and TransformComponents. Internally, it would also have an array of SpriteRenderNodes with one to one mapping to SpriteComponents.

SpriteComponent would be a pure data structure, holding sprite's state information, such as sprite size, color, etc. Serializing all SpriteComponents should be enough to restore the state of all the sprites in the game.

Internal SpriteRenderNode would be another pure data structure, that would keep cached data required to do the actual rendering of sprites. It could hold things like transformation matrices, vertices and so on.

In update cycle SpriteRenderSystem would iterate over SpriteComponents. For each SpriteComponent it would take the data from it, then take the data from respective TransformComponent, combine it, and using combined info update SpriteRenderNode. After that, SpriteRenderSystem can sort SpriteRenderNodes by layers, maybe do some other processing, and finally loop over all of them and submit it to some graphics API (Your own abstraction, or directly to DirectX, OpenGL, etc). (Also note that after all SpriteRenderNodes are set up, you can do the further processing on another thread, and since all SpriteRenderNode data is internal to the system, it can do all sorts of optimizations, like have separate SpriteRenderNodes arrays for each layer, etc).

Problem #2

Basically I've answered this question with the above example. So in general, it helps to view your whole game in the ECS like this:

Game state:

  • System list
  • Entity list
  • Component list
  • Maybe some sort of per system configuration

Cached state:

  • Internal state in systems.

Serializing all data in the game state, should be enough to restore the game later. So each time you're dealing with new data, you just ask a question: can you recreate the game without this data? If the answer is no - it goes to a component; if the answer is yes - it goes to internal system state.

So for animations example, you might want to have AnimationComponent that would contain basic state for the animation, like which animation is playing, what frame it's at, etc. For something like skinned mesh animations AnimationSystem internally could have a respective AnimationComponentNode. On update, AnimationSystem would update each AnimationComponent and generate bone transforms into AnimationComponentNodes. Or if you're animating sprites, then AnimationSystem can just update SpriteComponent.

\$\endgroup\$
0
\$\begingroup\$

The differentiation you make whether logic should be in system or in a component sounds like you want to have an elegant system, where a single architecture can handle all problems. For me having systems is just pure necessity, because it is too slow to draw each sprite individually and you have to do stuff like sorting to get the desired result. But this is not true for all components. Game logic or scripts are just other components that do not need this optimization.

Problem #1

Each component should have a reference to the component it belongs to.

The transform system can then just loop over all nodes and make the update, the sprite renderer can also get the transform component very easily.

e.g. (in C#)

foreach (var sprite in sprites)
{
   var worldMatrix = sprite.Entity.Get<TransformComponent>()?.World ?? Matrix.Identity;

   Render(sprite, worldMatrix);
}

BTW: Some engines make the transform component required for all entities.

Problem #2

Depending on your serialization system you can annotate the data/properties/fields you want to persist.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .