Advertisement

Where should shared resources live in an ECS engine?

Started by July 18, 2021 07:56 PM
9 comments, last by Shaarigan 3 years, 3 months ago

I'm working on my C-language hobby ECS engine and am curious what other people have done with regards to “shared resources” in their ECS-like engines.

For example, let's say I'm using SDL as a 2D renderer. This renderer may be used by multiple systems. One system may be responsible for drawing isometric blocks, another system may be responsible for drawing menu overlays, etc. Essentially, each system must have a handle to the renderer such that it can draw to the screen. There are two solutions I see:

  1. Define all resources to a “global-like” scope. These resources can then be fed as inputs into the system update routines. Pros of this method is that each system can share a single resource, and each system only contains behaviors and no data (i.e. the system doesn't have a renderer data member).
  2. Redefine my systems such that there is a single render system that contain subroutines for each render type. This system would then encapsulate all rendering capability, thus there would be no need for resource sharing. For example, I would have a render system that would first draw the world, then second draw menu overlays, etc. etc. I don't know if I like this method though. I could see the “render" system grow to become a very complicated beast. Further, the system would then contain data (not just behaviors) which would make the system stateful. This isn't inherently bad, but it might set a bad precedent for designing systems in the future.

This is a 100% subjective question, but I'm curious how you guys have handled this.

ECS is great for game logic, but not great for low-level libraries.

Like, imagine if there was a “component” that managed all memory allocation, and every other component needed to have an instance of this component before they could in turn allocate memory. It would be terrible to use and give you no real benefits. (Not to mention the delicious problem of bootstrapping an instance of that component to begin with …)

Libraries don't go away. You can use a library for math, and a library for memory, and a library for file I/O. You can also use a library for rendering – BUT, I would suggest that you put your “sprite” or “mesh” or whatever your basic renderable concept is, into the interface layer for entities, and not call the renderer directly from any components. There are two ways of going here – either you have a StaticMeshComponent that calls the rendering library, and another SkinnedMeshComponent that also calls the rendering library, or you have a lower-level library-like construct for static meshes and skinned meshes, and then make ECS be purely the configuration data on top of that ("what meshes to draw.") Both work.

And, yes, a rendering system is a very complex beast, no matter what. At a minimum, you need “passes” for your rendering – perhaps, “far geometry," “skybox (z tested against far geometry)," “far blended geometry,” “near geometry," “near blended geometry,” and “UI overlays” would be a sufficient number of passes, but if you end up doing G buffer deferred renderers, you would do something totally different. Thus, you'll probably want multiple layers in your design. A the ECS layer, you have “mesh asset X with parameters Y and Z,” and then at the renderer layer that turns into “binned into passes 1, 2, and 8," and that, in turn, turns into specific setups and render queues in your low-level device pump.

enum Bool { True, False, FileNotFound };
Advertisement

100% agreeing with hplus.

I also use ECS as the glue between the low-level engine and the gameplay layer. For example, since I'm working mainly in 2D, I have a “Sprite” component which is configured (texture, color); and internally the “system” uses this configuration to feed my Sprite Batcher, which internally supplies the required setup for the renderer.

@hplus0603

Thank you for the insight! This seems to be the common issue I'm running into: what should be handled by ECS, and what shouldn't?

Regarding your comments on libraries, yes, that makes sense. However, I'm still questioning where the “statefulness” of these libraries should reside? Let's say I'm using a 3rd physics engine that maintains its own state (e.g. X number of objects active, characteristics of each object, etc.). Where should this statefulness live within my ECS? Should it be encapsulated by a system? Or should there be a dedicated “resource” pool that houses all of this information? I still believe this “statefulness” belongs within my ECS model because this data is directly involved with my system implementations.

I'm currently leaning towards a resource pool. This will allow systems to contain no state, which sets a good precedence.

My thought process is this: If I require statefulness that isn't associated with a specific entity within my game, it should not be considered a component. Components make up entities, thus anything that won't be associated with a single entity should not be a component. Instead, this statefulness should be considered a resource.

However, there is still a relationship between components and resources. Both of these objects will contain stateful information that will be read / written by my ECS systems. Thus they should be encapsulated by my ECS “world”.

The basic principle of ECS (real ECS, not the Unity one) is that everything is data. Your object is just an ID which components point to and components are just data/properties that object is made of. A system is just a batch processor which enables logic to happen based on objects with certain properties.

This said, your physics example may be solved (depending on the physics engine) by simply having a component which is related to an object identifier in the physics engine itself. Note that you/your ECS engine handles physics objects, not vice versa.

In case of the renderer, you should take the update loop into account. The update loop controls everything else and so also how and when visual objects are rendered. The order depends on your game, a simple game with 3D objects and a scene may first collect a list of mesh components and have them processed by the mesh render system and after that collect all UI components and have them processed by the UI render system.

It feels to me that people are mostly thinking too complicated about ECS and oversee the pure data aspect

@Shaarigan Thank you for the response.

I think I understand the theory of ECS: Components hold data, systems process data, and entities “tie together” components.

However, this is a very simple definition and leaves much up to interpretation. What happens if the “data” does not belong to a single entity? If I have a renderer resource, there's no reason that data should be placed into a component. Since it doesn't belong to a single entity.

Advertisement

You may be trying to adhere to the common dictionary definition of ECS too closely to be practical (components are data and systems are code that act on data). ECS is just a framework for structuring your code to act on data in a data-first way, but that doesn't mean you can't have data that lives outside of components.

Your example of having a 3rd-party physics library wrapped by your custom physics systems could be structured in a variety of ways:

  1. Let your physics system have a bit of data and own the 3rd-party physics world since the reality is that what it wants to do is loop over physics components and process them in some form, probably with access to the physics world.
  2. Give your ECS world the concept of shared resources and let it own your physics world - your physics system can then access this resource via the world when it wants it.
  3. Just keep it simple and have a completely separate concept of engine libraries (physics world, renderer, etc.) that are accessible to all of your engine code (not just your ECS code).

It really just boils down to what works for your game / framework.

Thomas Izzo said:
What happens if the “data” does not belong to a single entity?

Again, depends. If the data is something referenced by the entity, then I don't see any reason to not put it into a component. This requires the data to be at least somehow non-unique, for example if youre in a tree like structure where nodes are entities pointing to other nodes, 1 > N relation.

If you have true unique static data, you might either want to have it in the classic way stored in a static variable somewhere in your code or use a static/singleton entity. The entity might be useful if you have static data which is composed on demand, for example a Player entity which has optional name, gender and a premium code property.

Like @valakor said, you can't use ECS for everything and don't get confused about data used by a system and data provided by an entity. A system may require static data like a reference to the underlaying renderer in order to function properly doesn't mean that an entity needs to provide that data if it isn't required for the entity to be properly used! Systems still require a cetrtain set of components and this is how it is determined if an entity can be processed by a certain system. For example if you have the mesh component and the UI component, this doesn't mean that they have to provide a static reference to the renderer used to put them on screen, it just means that there are 2 different systems which work on the mesh component, providing mesh rendering into the 3D world and UI component, providing 2D rendering on top of the render buffer.

You should think of entities as dynamic objects who's properties are defined at runtime and use compile time known behavior based on their properties.

An example: I wrote a data driven build tool for our Game Engine SDK project, which can be found on GitHub for reference. Since we removed the need to maintain a complicated project definition and configuration file in favor for a more flexible approach, our tool needs to now the sub projects aka build modules in order to for example compile the code properly or create IDE files to provide access to an editor. So what we have is somehow related to ECS; an actor pipeline. The main difference between that pipeline and an entity component system is the system part. Instead of working on a batch of componentns every frame, the pipeline works on single instances pushed to it. Except for that difference, we also have entities aka templates with components aka properties.

Our tool first takes a collection of paths entered via command line and creates an entity for each of those paths. Each entity gets a module component and is then pushed to the pipeline. Each module will be handled by an actor depending on the components attached to it. There is for example an actor to determine if a module has C# or C++ files included and creates the corresponding SharpProject or CppProject property. Those components are then also puhsed to the pipeline and handled from another actor which determines the relationship between those projects. Another actor then may call the compiler for all components in the right order or the IDE project generator is triggered.

We have static data too, for example the SDK root directory, an overall config (which defines important paths to the compiler among others) or cached .NET Framework Assemblies which have to be referenc resolved as well. This data is held at points in our code which are static too in order to enable actors to use that data. We also have non-unique data, the NPM package config which is unique per module for example

Shaarigan said:

Systems still require a cetrtain set of components and this is how it is determined if an entity can be processed by a certain system. For example if you have the mesh component and the UI component, this doesn't mean that they have to provide a static reference to the renderer used to put them on screen, it just means that there are 2 different systems which work on the mesh component, providing mesh rendering into the 3D world and UI component, providing 2D rendering on top of the render buffer.

Piggybacking off Shaarigan's use case, I would go as far as saying those two systems should accept the Renderer as a constructor argument as well. This defines an implied coupling with the renderer, a dependency graph in code, and the argument list can be used to determine if or when the system might be doing too much or that code requires a refactor if the argument list grows beyond a certain number of arguments.

Another reason I tend to frown upon Global access in general is you never know when that access is going to impact the introduction of a new concept. If you haven't seen it, I recommend watching Tom Ford's GDC talk about Overwatch where the use of global accessors in the ECS system caused some problems when needing to introduce Kill Cam support to the game client.

In a best case scenario, you don't have an object instance of a system nor a constructor to call since systems determine what data they need and the data doesn't define the system. That said, it is very likely that a system is called statically in an update and so is a pure static class/a collection of pure static functions if the language supports that

This topic is closed to new replies.

Advertisement