Plank ECS: A Minimalistic Yet Performant Entity-Component-System Library

|       "Perfection is achieved, not when there is nothing more to add,        |
|                but when there is nothing left to take away."                 |
|                       - Antoine de Saint-Exupery, 1900                       |

If you already know what an ECS is, jump to the
"Comparison With Other ECS" section.

The title is a lie. In reality, this is an Entity-Component-Resource-System

First, let's start at the beginning.
What is an ECS you ask?
An ECS is a way to organise data and modify this data.

Why not just use regular object oriented code?
For three reasons:
1) Using an ECS is often faster.
2) It uses parallelism to complete the data modifications much faster.
3) It looks much cleaner.


Now, let's cover the basics.

The Basics
We have four main elements.

- Entity: A "thing" that exists in the world. It may be a game character, 
a map, a button, anything! By itself, an Entity is a thing with no attributes
at all. Literally, it is just a thing that exists and is nothing.

- Component: An attribute added to an Entity. This is what defines what the
"thing" really is.

- Resource: Some data that is not attached to an Entity but also exists.
For example, time is a resource of our world, but isn't specific to any
Entity existing in the world (if we pretend that general relativity isn't a
thing, that is..)

- System: An operation that transforms Entities, Components and Resources.

Making It All Come Together
Here's a quick example of how it looks conceptually:

Entity 1:
- Name("Button")
- OnClick(Event::ButtonClicked)
- HoverAnimation("assets/button/on_hover.png")
- Position(5, 8)
- Size(10, 2)
- Render(Square, White)

As you see, the entity is a thing where we "attach" components that
specify what it is.
We read this as: "Entity 1 is a thing with a name 'Button', that creates
an event when clicked, that is animated when hovered, has a physical
position and size and is rendered as a white square."

- Time(current_time)

- if current_time > 5 seconds, then move all entities' Position left by 3 units.

We have a simple system that conditionally modifies the Position component
of all entities having one.

Extra Elements
To make this all work together, we need some more concepts.

First, the World.
A World is extremely simplistic: It holds all the entities, components and
Actually, that's how we used to do it. See, Plank ECS follows the minimalist
mindset. Our World stores only Resources, and everything else has been made a
Resource. Let's see how that works.

For Entity, we store them in an Entities Resource. Simply a list of existing
entities, with some extra operations to create and kill entities.

For Component, we store them in a Components<T> Resource. Similar to Entities,
it is a list. The main difference is that you access components using an Entity.

A good way to think of it, even though it is not implemented this way,
is as the following:
Entities: List(Entity)
Components<T>: HashMap(Entity, T)

Now, we have a way to contain entities, components and resources: the world.
What are we forgetting? Ah yes, the systems!
Where are they stored?
How do we execute them?
How do they get access to the data in World?

Systems are stored in a Dispatcher. Dispatchers are built from a list of Systems
and are used to execute Systems either in sequence or in parallel.
The Dispatcher will fetch resources from the World automatically and execute
the System in a way that guarantees there will not be any conflicts while
accessing resources.

To do this, Systems need to be built in a way that corresponds to what the
Dispatcher can handle.

Constraints On Systems
These are the constraints that specify how systems may be built:

1) Systems must take only references as arguments.

2) All mutable references must be after all immutable references.
For example: fn my_system(first: &u32, second: &u64, third: &mut u16)
This constraint is attributable to the way traits are implemented for generic
types in rust. Removing this constraint would make the build time factorial,
which would effectively never complete.

3) Systems must return a SystemResult. This is to gracefully handle and
recover from errors in systems.

4) System arguments must implement Default. If they don't, then you need to use
&Option<WhatYouWant> instead of directly using &WhatYouWant.
This constraint exists so that resources may be automatically created for you,
as well as enforcing that any resource that might not exist is actually handled
by the system without any issue.

How It Actually Looks
Importing the library:
use plank_ecs::*;

Creating an entity:
let mut entities = Entities::default();
let entity1 = entities.create();
let entity2 = entities.create();

Creating components:
struct A;
let mut components = Components::default();
components.insert(entity1, A);

Creating a world:
let mut world = World::default();

Creating a system:
fn my_system(value: &Components<A>) -> SystemResult {

Creating a system as a closure:
let my_system = |value: &Components<A>| Ok(());

Creating and using a dispatcher:
let dispatcher = DispatcherBuilder::default()
    .build(&mut world);
// Run without parallelism.
dispatcher.run_seq(&mut world).expect("Error in a system!");
// Run in parallel.
dispatcher.run_par(&mut world).expect("Error in a system!");

// Does some cleanup related to deleted entities.

Joining Components
The last part of the puzzle: How to write the example system from earlier
that modifies the position using the time?

For this, we need to introduce joining. Joining starts with us specifying
multiple Component types and bitwise conditions. Don't be afraid, this is
simple. Here is an example:
join!(&positions_components && &size_components)

This will create an iterator going through all entities that have both a
Position component AND a Size component. If you use &mut instead of &, then you
will get a mutable reference to the component in question.

The join macro supports the following operators: && || !
Those work as you would expect, with the caveat that operators are strictly read
from left to right.
For example,
join!(&a && &mut b || !&c)
creates an iterator where we only components of entities having the
following are included: they have (an A AND a B) OR do not have a C.
The reference to B will be mutable.

Finally, when joining, what you get is actually:
(&Option<A>, &mut Option<B>, &Option<C>)

The options are always present when joining over multiple components.

fn position_update_if_time(time: &Time, sizes: &Components<Size>, 
    positions: &mut Components<Position>) -> SystemResult {
    if time.current_time >= 5 {
        // Iterate over entities having both position and size, but updates
        // only the position component.
        for (pos, _) in join!(&mut positions && &size) {
            pos.as_mut().unwrap().x -= 3;

Comparison With Other ECS
Let's have a quick and informal comparison with other Rust ECS libraries.

First, performance: According to the last time we ran benchmarks, we were the
fastest library when iterating over a single component.
For other benchmarks, including multiple component joining, entity creation and
deletion and component insertion, we ranked on average second, behind legion,
but sometimes being faster on some benchmarks.

Code Size: The complete code size of Plank ECS, including tests and benchmarks, 
is under 1500 lines.
For comparison, Bevy ECS has 5400 lines of code, Specs has 6800, legion has 13000, shipyard has

SystemResult: As far as we know, we are the only ECS where systems return
errors gracefully in this way.

Macros: System declaration, in most current ECS, either require a
macro-by-example or a procedural macro to be concise. Here, you declare
systems in a way identical to regular functions.

Tests: We have high standards for tests. Since our code size is small,
all features and all non-trivial public functions are tested.
We also benchmarked all performance-sensitive code.

Safety: We use unsafe code only as an absolute last resort.
This shows in the numbers. Here's the count of unsafe code snippets found in
popular ECS libraries:
- Specs: 150
- Bevy ECS: 157
- Legion: 264
- Shipyard: 312
- Plank ECS: 4

The numbers speak for themselves.

Because of the quality and the time spent on this library, it was decided
that the AGPL license would be more adequate for it. We want the code to remain
open source and encourage contributions to come back.

As we know some people want to make use of our product in commercial software,
we offer a paid commercial license as an alternative way to contribute back.

In conclusion, Plank ECS is not an innovative piece of software. It does
the same thing that the community has been doing for years. It just does it in
a better and more safe way.
Part of the Focks Team project.