Replies: 2 comments 8 replies
-
That's a very interesting concept. I would like to understand more how this archetypal data storage would affect single component processors - e.g. transform updates. Wouldn't there be a higher cost per processor if one needs to perform cross archetypal queries? N.b. I'm very much for Stride 5 breaking backwards compatibility - see latest discussion in the plugin RFC. |
Beta Was this translation helpful? Give feedback.
-
Here's a list of benchmarks for popular C# ECS libraries: The ones using archetypal ECS reach some quite impressive numbers. |
Beta Was this translation helpful? Give feedback.
-
Related to #1568
This is a little write up to present the ECS implementation i made recently and i think could hugely benefit Stride, it would also help make rendering in parallel simpler. I hope to get some of your ideas and criticism from it.
Also you should read this as "Maybe an idea if we want to make a Stride 5 version"
Premise
ECS design
In Bungie's multithreaded renderer, to make synchronization easier between game logic and rendering, they decided to copy carefully chosen component data to a separate storage in order to perform rendering work. This puts the copying/extracting phase as a hot path for optimization but allows for the renderer to work on its own thread(s) while the game logic works on the others.
Going from this idea, i started thinking of designing an ECS system but there were short comings.
.NET and allocation
In .NET, what impacts performance is data throughput :
Considering ValueTypes :
In the newer .NET versions we are able to use ByRef value types, avoiding unnecessary copies, which removes part of the copy issues.
The new design
Having that in mind, I prototyped an ECS system with a archetypal storage similar to what legion, bevy-ecs and flecs proposed. So i followed some design principles
Entities are just indices
Components are structs, in arrays/lists they are stored contiguously.
Systems/Processors should be able to process one or multiple entities, depending one or multiple component types.
Storage
If we want to iterate over entities that have multiple components we have to define a kind of storage that avoids fragmentation.
Given this set of entities with their associated components ( where A, B, C are types)
We need to find a way to allocate arrays and make sure those arrays do not have empty values/padding. For that we group entities based on their archetypes. An archetype is defined by the types of components an entity has. In our example, we would separate entities like so :
Once processors iterate over entities, they will do so with a constraint on the types present. A processor that deals with component
A
andB
, will interrogate archetypes containing those types (where the archetype types is a supersets of queried types).We can iterate over a subset of entities by just doing a superset/subset comparison.
There's a big draw back from this way of storing data, adding or removing a component to an entity forces us to move all the components from an archetype to another.
One way FLECS managed this is to create a graph of archetypes where edges link an archetype and all the direct supersets of it ( A * B - A * B * C, A*B - A * B * Z, etc).
Systems/Processors for game logic
This one was weird to think about. I wanted to keep the action of attaching scripts to entities as it feels like it's the best way to add logic to a game for a simple game developer. But with the storage defined above, it was a complex task so i had to get to a simpler API to build.
My initial idea was to use generics to simplify the creation of systems. A user should be able to define a function and use it as a game system. Separating rendering and game logic for better parallelism allows us to keep systems focused on game logic.
This time, inspired from bevy i wanted to have game logic created as simply as :
C#/.NET is not as performant or usable as rust in certain areas especially with tuples and structs but the biggest shortcoming was C#'s generics, which is fairly limited.
So i came up with a kind of systems/processors where users could design their own queries with functions + generics and the library would feed the function with the necessary data. The code looks like this
Of course this is still a work in progress, i'm in the process of adding async systems and sub-worlds. But so far this API makes the game logic allocation-free and avoids as much computation as possible.
Rendering in Parallel
As you saw, in this ECS implementation, we create a
World
(equivalent to aSceneInstance
), we add Entities with any structs and add processors to create some game logic.For us to render in parallel, we would have another
World
object containing its own storage and its own rendering processors. Both worlds would run in parallel.There would be a point where both the game logic
World
and the renderWorld
would need to be synchronized (typically after each frame) to allow us to extract/copy data from the game logicWorld
and copy it to the renderWorld
. Once it's done, they both can run in parallel until the next step of synchronization.Considerations
Query api
The Query/Processor API is just one way to query the data over the storage. Since the storage is just a way to store data contiguously, there can be many ways to query over it, maybe even designing a script system, mix and match other many different type of query systems.
Serialization
Since the storage is made up of structs and structs can contain object references, there's a need to make sure each reference object is not duplicated during serialization. (Something Stride might already be doing)
Bundles/Plug-ins
There's also the bundle approach I have found interesting in Bevy. Basically bundling component types and processors into a single class/struct implementing an interface. Still WIP in my library but very possible.
Beta Was this translation helpful? Give feedback.
All reactions