💾 Archived View for jojolepro.com › blog › 2021-01-13_planck_ecs › index.gmi captured on 2023-06-16 at 16:13:57. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-01-29)

-=-=-=-=-=-=-

Jojolepro

Blog

Projects

Music

Quotes

GitHub

Archives

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

https://github.com/jojolepro/planck_ecs

https://docs.rs/planck_ecs

#```

| "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
library.

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.

Good!

Now, let's cover the basics.

### The Basics
We have four main elements.


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.


"thing" really is.


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..)



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

Entity 1:


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."

Resources:


System:


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
resources.
Actually, that's how we used to do it. See, Planck 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 planck_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 {

Ok(())

}


Creating a system as a closure:

let my_system = |value: &Components<A>| Ok(());


Creating and using a dispatcher:

let dispatcher = DispatcherBuilder::default()

.add(my_system)

.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.

world.maintain();


### 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.

Together:

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;

}

}

Ok(())

}


### 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.

=>https://github.com/jojolepro/ecs_bench_suite/tree/planck

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

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:


The numbers speak for themselves.

### Licensing
Published under CC0, Planck ECS is under the public domain and available for
free!

### Conclusion
In conclusion, Planck 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.

If you like this library, please consider donating on patreon:
=>https://patreon.com/jojolepro

=>https://github.com/jojolepro/planck_ecs
=>https://docs.rs/planck_ecs

(CC0) Joël Lupien 2020-2022
=>/blog/2021-01-13_planck_ecs/index.txt View page source