💾 Archived View for jojolepro.com › blog › 2021-01-13_planck_ecs › index.gmi captured on 2023-05-24 at 17:38:07. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
https://github.com/jojolepro/planck_ecs
| "Perfection is achieved, not when there is nothing more to add, |
| but when there is nothing left to take away." |
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.
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: