So I’ve been working on this thing called Entropy Engine – which carries a lot of accumulated ideas and such from previous projects such as Sauce, and various attempts to build a metaverse platform over the past 15 years or so.
As of late, I’ve been dusting a lot of it off. There’s a lot of questions such as “what even is the metaverse”, “why make yet another platform”, and “is this even a platform” that I’m not gonna get into with this post. That’s probably one big mega post or something that future me can worry about.
Instead, I’m gonna talk about how I’ve designed Entropy’s ECS.
What is an ECS?
A miserable little pile of entities and components!
In case you’re not familiar, let’s recap what an ECS is.
ECS stands for Entity Component System. Typically you’re gonna focus on the E and C here, and Systems are not the same as the System in ECS.
The relationship looks kinda like this:
--- config: theme: 'dark' --- graph TD; System-->Entity; System-->Component; Entity-->Component;
The basic idea is that the system tracks entities and components, and the relationship between them. An ECS will “link” a component to an entity. This linkage is pretty strong – a component can’t exist without an entity, but an entity can exist on its own. And the system will usually just act as a means to manage the two – providing storage for them, the ability to pull up groups of them, and so on.
There’s a few different types of ECSes as well:
- Object-Oriented
- For those that are familiar with Unity or Unreal, this is gonna be the most familiar (pre-DOTS in Unity that is).
- An entity (GameObject in Unity, Actor in Unreal) can have many different components
- Generally gives you a lot of flexibility, are easy to build, but can perform worse than other options
- Data-Oriented
- Think Unity DOTS, EnTT (what I use internally), or flecs
- Trades memory and cache coherency for flexibility in most cases
- Very high performance thanks to its cache and memory coherency
- Very compact in memory as a result
- Can be more cumbersome to work with
- Archetype
- Unity DOTS also falls into this category (an ECS can combine multiple attributes!), along with flecs
- Focus on applying operations to groups of components that represent “archetypes”
- Sparse Set
- Uses sparse set relationships to track the relationship between entities and components
- Lots of contiguous arrays to reduce memory fragmentation
- Can be cumbersome to work with – especially if you’re coming from an Object-Oriented ECS
- Very fast to iterate over components
- Hybrids
- As implied on the tin, takes bits and pieces from other ECS types
- Entropy’s falls into this category
I should note that it’s very difficult to come to a “one size fits all” ECS – as with anything in software engineering you’re always going to make some kind of trade off.
What is Entropy’s ECS?
Entropy’s ECS is a hybrid of sorts. Under the hood it’s actually using EnTT for its performance characteristics. But its differences are in its interface.
Entropy’s ECS has multiple high level concepts that are designed to make it easier to interact with. It all starts with a World.
--- config: theme: 'dark' --- graph TD; IWorld-->Entity; IWorld-->Component; Entity-->Component; IWorld-->ISystem; ISystem-->Component;
To break this down:
- IWorld has entities, systems, and components
- Entities have components
- Systems consume components and “do things” with components – usually with a view that specifies what combination of components it needs
You can kind of think of it as model view controller with extra steps.
There’s a few extra things as well:
- Worlds have a lot of responsibility
- Responsible for executing update, render, and simulation cycles on systems
- Internally, they actually handle setting up components on entities
- They “own” systems, meaning you’re not actually setting anything up – you’re asking the world to
- Systems are where the magic happens
- Update and fixedUpdate
- Can modify state on components
- Can create and modify entities
- More or less run independent of each other – update and fixedUpdate run in separate jobs
- Thread safety is not guaranteed – BYOTS
- Render
- Can only read state on components – never modify them
- Effectively always runs in a separate thread
- This is where you should be emitting various draw commands
- Can track and modify internal render state
- Systems can “opt in” to being concurrent
- Synchronous systems: get executed in a specified order by the world
- Asynchronous systems: get added to a special “work group” and executed after the synchronous systems
- Slight misnomer: Async systems are waited on until they complete their execution cycle.
- They’re async in that they won’t wait on each other for execution, but whatever cycle is being executed will wait until all systems have finished executing.
- Update and fixedUpdate
- Entities are what you’re gonna interact with to add and remove components
- Supports multiple components of the same type on an entity – sort of
- Uses “shadow entities” internally for cases where you need multiples of the same component on a given entity
- This is actually kind of messy and super not ideal. EnTT supports multiple components per entity, but you end up creating multiple storage pools along the way that can lead to loads of fragmentation if left unchecked
- There doesn’t seem to be a “great” way to handle this in sparse set ECSes – you’re gonna sacrifice performance in one way or another. So I decided to sacrifice it with extra state tracking rather than compromising memory contiguousness.
- Will probably have this disabled for my projects, and have it be enabled at compile time for things that actually need it (for example, virtual world platforms looking for a new renderer)
- Passes through component addition to the world, but gives you a familiar API to work with
- addComponent, removeComponent, etc. are all there.
- Only exist to contain components as one would expect. Just adds some API sugar to make it easier to work with.
- Supports multiple components of the same type on an entity – sort of
A note about concurrency
Entropy’s ECS does not guarantee thread safety for a component or system. That’s for you to figure out. Build your systems in particular with this in mind – self certifying a system as supporting concurrency is not a silver performance bullet. It does, however, have thread safety baked in for:
- Adding and removing components
- Adding and removing systems
- Adding and removing entities
Anything else is outside of the scope of Entropy. Like any other engine or ECS, I can’t really tell you how to handle concurrency for your specific application. I can only tell you how specific bits and pieces work within my systems. I may add some helpers and such to make this easier on the components themselves – for example, shared_mutex all the things along with more concurrent containers (concurrent map anyone?), but it’s not really a priority right now, and really these are things that someone can use already if they need to build more thread safe code.
Why use EnTT?
As I mentioned, Entropy uses EnTT under the hood. But why? Implementing ECSes should be easy right?
Really it comes down to that most valuable asset of all: time.
EnTT generally ticked most of my boxes for an ECS:
- Extremely fast for lots of entities and components
- Very low memory fragmentation which makes cache hits very high and misses very low
- Generally easy to wrap and work with if you understand its limits
With this in mind, the other features I cared about could basically just exist as a wrapper or function as high level abstractions around EnTT. Sure – I put a lot of time into creating passthroughs and such that all lead to EnTT at the end of the day. But all in all I felt it was worth it to hit the feature and performance balance that I care about.
Future Work
Right now I have a need to more explicitly schedule how groups of systems execute – so I’ll probably be adding that next. Particularly rendering focused systems that may need to execute multiple times per frame – such as to support multiple windows looking into the scene.
I’m also not 100% satisfied with all of the concurrency mechanisms I’m using today – so I may try to have more focused concurrency machinery in place just for the ECS. Right now it’s all running on a pretty generic unprioritized thread pool with some extra bits (such as work groups). Seems to work fine for now, but definitely an area of improvement (task graphs anyone?).
Closing Thoughts
I hope you found this informative. I’m sure that there’ll be someone somewhere who thinks this is all some batshit implementation or something – but generally it works for me and right now that’s what’s important to me. I’m hoping I can actually have this out in the open in the not too distant future for people to mess and tinker with along with the rest of Entropy Engine. Stay tuned!
Leave a Reply