[Done] The Sauce of
git clone
Log | Files | Refs | README | LICENSE

commit d021a09056ef37c36b48a8694fbc9d0317fb28f4
parent 65bb05173bb3a9ca15249605f3dece6ea1206d82
Author: Joël Lupien (Jojolepro) <>
Date:   Wed, 13 Jan 2021 12:52:37 -0500

add blog post on planks ecs.

Asrc/blog/2021-01-13_planks_ecs/index.txt | 279+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 279 insertions(+), 0 deletions(-)

diff --git a/src/blog/2021-01-13_planks_ecs/index.txt b/src/blog/2021-01-13_planks_ecs/index.txt @@ -0,0 +1,279 @@ +Planks 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. Actually, 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 modification much faster. +2) It looks much cleaner. + +Good! + +Now, let's cover the basics. + +The Basics +-------------------------------------------------------------------------------- +You 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, Planks 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 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 planks_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 by us specifying +multiple Component types and bitwise conditions. Don't be afraid, this is +simple. Here is a simple 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 will 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 +faster 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 Planks ECS, including tests and benchmarks, +is under 1500 lines. +For comparison, Specs has 6800 lines of code, legion has 13000, shipyard has +25000 and Bevy ECS has 5400. + +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 features. + +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 +- Planks ECS: 4 + +The numbers speak for themselves. + +Licensing +-------------------------------------------------------------------------------- +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. + +Conclusion +-------------------------------------------------------------------------------- +In conclusion, Planks 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. +