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. - 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." Resources: - Time(current_time) System: - 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 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: - Specs: 150 - Bevy ECS: 157 - Legion: 264 - Shipyard: 312 - Planck ECS: 4 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