ECS
An entity-component-system with a custom scheduler. Written in Rust.
Overview
If you’re unfamiliar, an entity component system (ECS) is a way of architecting a game engine and representing game objects in a scene. My ECS is the foundation for all other systems in my hobby engine. Two standout features of my ECS are:
- A hybrid component storage system.
- A custom scheduler for automatic multithreading of systems.
Hybrid Storage
Traditionally, there are two ways of storing components in an ECS.
- Storage Based: Components of the same type are stored together in a data structure like a hashmap or array.
- Archetypal: Entities that have the same types of components store their components together in groups called archetypes.
Each approach has its pros and cons, but I wanted the ability to choose which type of approach I used for what components to get the best of both worlds. To do this, I split components into two types. “Components,” which use the archetypal approach, and “Tags,” which use a storage based approach. An example of a system using these two types of components is shown below.
impl PhysicsSystem {
fn run(
&mut self,
// Events can send information to systems. This one is a tuple containing delta time.
dt: PhysicsStep,
_: Commands,
// Queries are how systems define what resources they have access to. This one requests
// write access to `Velocity` and read access to `Mass`. An optional tag called `Disabled`
// is used to turn off physics for an entity.
queries: Queries<(Entity, (Write<Velocity>, Read<Mass>), Read<Disabled>)>,
_: Res<()>
) {
// Construct the query we desire
let query = queries.make::<(Entity, (Write<Velocity>, Read<Mass>), Read<Disabled>)>();
// Iterate and apply gravity
// NOTE: The first element of the tuple is the ID of the entity, which we don't need
for (_, (velocity, mass), disabled) in query {
// Ignore if disabled
if disabled.is_some() {
continue;
}
// Apply gravity
velocity.0 += self.gravity * dt.0;
// We live in an alternate universe where objects can only move as fast as their mass!
if velocity.0 > mass.0 {
velocity.0 = mass.0;
}
}
}
}
Scheduler
ECS scheduling algorithms typically work by identifying what sets of systems can run in parallel based on the component types they access. Most ECS schedulers use topological sorting to do this, but I identified a problem with that solution. If you greedily select systems to run from a given set of available systems, you can end up choosing a subset which does not maximize the number of systems running in parallel.
My solution to this problem combines the Bron–Kerbosch algorithm with caching to produce an efficient scheduler which constantly maximizes system throughput.
Room For Improvement
There are plenty of things I’d like to incorporate into my ECS to improve its usability. Some of those things include:
- A way to visualize the system schedule for debugging.
- Unifying tags and components to reduce complexity.
- Rayon support for queries.