#ecs #component #systems #system #entity #parameters #thread

thinkofname/think_ecs

Source code of the game Univercity: https://store.steampowered.com/app/808160/UniverCity/

1 unstable release

0.1.0 Jan 26, 2020

#762 in Concurrency

157 stars & 8 watchers

GPL-3.0-or-later

97KB
2.5K SLoC

A multi-threaded entity component system without locks on components

Features

  • Fast, aims to have low overhead
  • Threaded, systems will automatically be threaded where possible
  • Simple, nothing complex in terms of api usage

How it works

Systems declare the components they wish to access by using parameters. A Read<T> parameter declares that the system only wishes to access the component as read-only, whilst Write<T> declares that the system may mutate the component (add/remove/edit).

Internally a scheduler will execute all systems using a set number of threads only allowing systems that can run safely in parallel to run at any given time. The rules for this system are simple: A component may have any number of readers at a given time as long as it has no writers. Only a single system may write to a component and no readers are allow whilst a system holds write access to a component.

Usage

Firstly all structs that you wish to use as components must implement the Component trait. This can be done with the component! macro. (See component!'s documentation for the different types). The component must then be registered with the Container.

struct Position {
    x: i32,
    y: i32,
}
component!(Position => Vec);
let mut c = Container::new();
c.register_component::<Position>();

Now the container can be used to create entities, add components and access them.

let entity = c.new_entity();
c.add_component(entity, Position { x: 5, y: 10});
// Mutable access to the component
{
    let pos = c.get_component_mut::<Position>(entity).unwrap();
    pos.x += 4;
}
// Immutable access to the component
assert_eq!(c.get_component::<Position>(entity), Some(&Position { x: 9, y: 10}));

Entities are generally processed via systems. Systems are just functions. You can register a list of functions to be run via the Systems type. When run Systems will automatically decide when to run a system based on its parameters and what other systems are currently running. The order that systems are run in is not defined.

The functions take at least one parameter, a EntityManager reference. This provides a interface to create and iterate over all entities in the system. Other parameters must either be a Read<T> or Write<T> reference where T is a component type. Read provides immutable access to a component and Write provides mutable access (adding/removing the component as well). Both the Read and Write types provide a mask method which can be used with EntityManager's iter_mask method to iterator over a subset of entities. Masks can be chained as followed to iterator over the intersection of multiple types pos.mask().and(vel).

let mut sys = Systems::new();
closure_system!(fn example(em: EntityManager, mut pos: Write<Position>) {
    let mask = pos.mask();
    for e in em.iter_mask(&mask) {
        let pos = pos.get_component_mut(e).unwrap();
        pos.y -= 3;
    }
});
sys.add(example);
sys.run(&mut c);

Quirks/Issues

  • Removing an entity whilst in a system wont actually remove it until after all systems have finished executing.

Dependencies

~1.5MB
~25K SLoC