Getting Started with ECS using Planck ECS ================================================================================ Hello hello! This tutorial is aiming people having between zero and moderate experience with an ECS. Let's jump right in! Creating our test project -------------------------------------------------------------------------------- Let's start a project so you can follow along. #``` cargo new --bin start_with_ecs cd start_with_ecs #``` Great! Let's add our dependency in Cargo.toml: #``` [dependencies] planck_ecs = "1.2.0" #``` Now, we are ready to start talking about the ECS! ECS? -------------------------------------------------------------------------------- Entity-Component-System. It is a way of organising data that is well suited for game projects and simulations. In this tutorial, we are not going to make parallels with how Object Oriented programming works. We will just talk about the concepts one by one. Entity -------------------------------------------------------------------------------- An entity is a "thing" on which you can attach "properties". For example, what we call an "Apple" is an entity. You are an entity. Every object is an entity. Simply put: Entity is the fancy name we give to "something". Component -------------------------------------------------------------------------------- Components are the properties that make entities what they are. For example, an "Apple" have these properties: - Red Color - Sweet Taste - Round Shape Components are used to describe what the entity is by attaching data (meaning) to it. System -------------------------------------------------------------------------------- Systems are simply functions. They run code. That's it! Is this all there is? -------------------------------------------------------------------------------- No, there are a couple extra concepts introduced by Planck ECS. Don't be afraid, they are simple! World -------------------------------------------------------------------------------- World is a structure which contains everything that the Systems should have access to. Resource -------------------------------------------------------------------------------- Anything contained inside of the World is called a "Resource". It is just a convenient name. Dispatcher -------------------------------------------------------------------------------- Dispatchers are used to execute systems (which are just functions!) using data that is inside of the World. They are optional, but very convenient! Ready? Set? -------------------------------------------------------------------------------- Go! Open up src/main.rs We will first import Planck ECS' features into our game. Add the following line at the start of the file: #``` use planck_ecs::*; #``` Next, we will need something to contain the data we create. Let's create a new World inside of the main function: #``` fn main() { let mut world = World::default(); } #``` Our First Resource -------------------------------------------------------------------------------- Let's insert our first Resource into the world. We will use the Entities resource, which is a structure that holds a list of entities that exist in the World. #``` fn main() { let mut world = World::default(); world.initialize::(); } #``` The initialize function will insert Entities inside of the World. Initialize works for all structures that have a default value (implements the Default trait). In case you ever need to insert a structure which does not implement Default, you can always use Option, since Option's default is always Option::None. Our First Entity -------------------------------------------------------------------------------- Let's now use our Entities Resource to create our first entity: #``` fn main() { let mut world = World::default(); world.initialize::(); let entity1 = world.get_mut::().unwrap().create(); } #``` Here, get_mut gives us access to the Resource in World. Since World doesn't contain every single possible Resource, it returns an Option which we unwrap. Finally, we call the create() function on Entities, which creates a new Entity and gives it to us. Since it happens often that we initialize something and then access it right away, there is a convenient function that does both steps at the same time: world.get_mut_or_default. Let's rewrite our code using it: #``` fn main() { let mut world = World::default(); let entity1 = world.get_mut_or_default::().create(); } #``` Our first Component -------------------------------------------------------------------------------- Now, let's add some meaning to this Entity. We will attach a color to it. First, we will create the Color component. This can be anything: a struct, an enum, you choose. Here, let's use an enum. Add the following before or after the main function: #``` #[derive(Debug, PartialEq)] enum Color { Red, Blue, Green, } fn main() { [...] #``` And now, we can tell the World that we want to use this enum as a Component by initializing a Components Resource and specifying that we want the Color enum. #``` fn main() { let mut world = World::default(); world.initialize::>(); let entity1 = world.get_mut_or_default::().create(); } #``` The Components Resource is simply used to contain components of type T in a way that is easy to use and also is quick to access. Finally, let's attach our Component to the Entity! #``` fn main() { let mut world = World::default(); world.initialize::>(); let entity1 = world.get_mut_or_default::().create(); world.get_mut::>().unwrap().insert(entity1, Color::Red); } #``` Here we use the insert(entity, component) method available on the Components resource. There are two simplifications that we can do to our code. First is to tell rust to guess that we want the Color component by using _. #``` fn main() { let mut world = World::default(); world.initialize::>(); let entity1 = world.get_mut_or_default::().create(); world.get_mut::>().unwrap().insert(entity1, Color::Red); } #``` Rust is smart enough to understand from our usage of Color::Red on the right side that what we want is Components. Now, this code looks strangely like the one we saw when creating the Entity previously. Let's apply the same trick: #``` fn main() { let mut world = World::default(); let entity1 = world.get_mut_or_default::().create(); world.get_mut_or_default::>().insert(entity1, Color::Red); } #``` Yep! We can use get_mut_or_default here too! Let's summarize. Now, we have an Entity stored in the Entities resource. We also have a Color::Red component that is stored in the Components resource and that is "assigned" to our Entity. Using Systems -------------------------------------------------------------------------------- Remember the core concepts? Entity, Component and System. Well, now we are at the System part. As we saw previously, Systems are just functions that use data from the World. Let's write a System that changes the color of the apple to blue. #``` fn change_color_system(colors: &mut Components) -> SystemResult { Ok(()) } fn main() { [...] #``` We have some things to explain here. The parameters of Systems use references (that's the & and &mut symbols). A special rule for Systems is that all parameters using &mut must be placed after all parameters using &. For example, the following is invalid: #``` fn system(a: &mut A, b: &B, c: &mut C) -> SystemResult {...} #``` It should instead be written as: #``` fn system(b: &B, a: &mut A, c: &mut C) -> SystemResult {...} #``` Now, you might wonder what that SystemResult thing is. This is a feature that is unique to Planck ECS: Error handling. In most implementations of Entity Component Systems, it is assumed that Systems cannot fail, which is quite naive. Here, returning SystemResult allows the user to return either Ok(()), which means no error occured, or Err(SomeErrorType), which indicates that the System failed to execute. For this tutorial, we will always use Ok. Now, let's change the color of that apple! #``` fn change_color_system(colors: &mut Components) -> SystemResult { for color in join!(&mut colors) { *color = Color::Blue; } Ok(()) } #``` The join!() macro returns an iterator over the Components we reference. Here, we tell the join!() macro to give us all Color Components mutably, so that we can modify them. Then, we change the color. Running the System -------------------------------------------------------------------------------- To run a System, we use: #``` fn main () { [...] change_color_system.system().run(&mut world).unwrap(); } #``` This will automatically get the required Components from World and call the function change_color_system. Now, what happens if the System uses a Resource which is not in World? It will crash! Let's fix this up: #``` fn main() { [...] let mut system = change_color_system.system(); system.initialize(&mut world); system.run(&mut world).unwrap(); } #``` When calling the initialize function on a System, it will use world.initialize() for every Resource that is in the System's parameters. Convenient, right? Dispatcher -------------------------------------------------------------------------------- Remember Dispatchers? We saw that they are used to execute multiple Systems. Well they do more than that, they also call system.initialize for you! They are also very convenient, because without them you would need three lines for each System! Here's how we use a Dispatcher: #``` fn main() { let mut world = World::default(); let mut dispatcher = DispatcherBuilder::default() .add(change_color_system) .build(&mut world); let entity1 = world.get_mut_or_default::().create(); world.get_mut_or_default::>().insert(entity1, Color::Red); dispatcher.run_seq(&mut world).unwrap(); } #``` The Dispatcher creation is pretty much self explanatory. Simply chain .add(system).add(system2).add(system3) to add Systems, then complete the Dispatcher using the build(&mut world) function. It is when calling build(&mut world) that the initialize function will be called on each System. Finally, we run the Systems using dispatcher.run_seq(&mut world). run_seq means "Run Sequentially", which will execute the Systems one after the other. Planck ECS also has a feature where you can execute multiple Systems at the same time. We will not use it in this tutorial. Multiple Components -------------------------------------------------------------------------------- Now, let's come back to our game! Let's imagine that we had another Entity that also had a Color Component, but that we don't want to change its Color. #``` fn main() { let mut world = World::default(); let apple = world.get_mut_or_default::().create(); let orange = world.get_mut_or_default::().create(); world.get_mut_or_default::>().insert(apple, Color::Red); world.get_mut_or_default::>().insert(orange, Color::Red); } #``` With our current System, both entities will have their colors changed to blue. We need a way to add some more meaning (data) to distinguish between the two entities. There are a couple ways to do this. - We can add an "Apple" Component. - We can add a "ColorChanging" Component. Generally speaking, the second approach is preferred, because it encourages reusing Systems for more than one type of Entity. Let's create this Component: #``` struct ColorChanging; #``` and add it to our apple Entity: #``` let apple = world.get_mut_or_default::().create(); let orange = world.get_mut_or_default::().create(); world.get_mut_or_default::>().insert(apple, Color::Red); world.get_mut_or_default::>().insert(apple, ColorChanging); //here world.get_mut_or_default::>().insert(orange, Color::Red); #``` Finally, let's make it so our System only runs on Entities that have a ColorChanging Component: #``` fn change_color_system( changings: &Components, colors: &mut Components) -> SystemResult { for (_changing, color) in join!(&changings && &mut colors) { *color.unwrap() = Color::Blue; } Ok(()) } #``` First, we add the Components parameter. Then, we modify the parameters of join!() macro. Here, we tell it to give us the Component pairs only for Entities having ColorChanging AND Color. We also specify that we want ColorChanging immutably (we are not going to modify it) and Color mutably (we do change the color). At the left side of the for loop, we ignore the ColorChanging Component by adding a _ in front of the variable name. We do this because we used the ColorChanging Component as a filter for join, but we don't actually plan on using it for anything else. Finally, we added .unwrap() after the color variable. We need this when joining over multiple Components at the same time. This is because the join macro also supports || OR operations. For example, if we used: #``` join!(&mut colors || &changings) #``` we would have pairs of Components that look like this: #``` (&mut Option, &Option) #``` Because we use &&, we can assume that both Options have a value (Some). But if we use ||, then we cannot assume this, as one of the Entities has a Color but no ColorChanging Component. Making sure everything works -------------------------------------------------------------------------------- It is always a good practice to verify that our Systems are working fine. Let's do this quickly in the main function: #``` [...] dispatcher.run_seq(&mut world).unwrap(); assert_eq!(*world.get::>().unwrap().get(apple).unwrap(), Color::Blue); assert_eq!(*world.get::>().unwrap().get(orange).unwrap(), Color::Red); } #``` Removing a Component -------------------------------------------------------------------------------- Simple! Use: #``` world.get_mut::>().unwrap().remove(apple); #``` Of course, this works inside of Systems too, since you have Components as a parameter! Removing an Entity -------------------------------------------------------------------------------- Also simple! Use: #``` world.get_mut::().unwrap().kill(apple); #``` However, it is important to know that removing an Entity using kill(entity) will NOT delete its Components that are in the Components Resources. This can be done automatically by using the world.maintain() function: #``` world.maintain(); #``` It is recommended to run world.maintain() after using dispatcher.run_seq(&mut world), as this ensures everything is cleaned up! Conclusion -------------------------------------------------------------------------------- I hope this was instructive! It might seem like there are lots of things to learn, but also consider that you now know almost every single function of Planck ECS! If you liked this tutorial or the work I do (I wrote Planck ECS, by the way!), consider donating on Patreon. https://patreon.com/jojolepro It is your donations that enable me to continue creating Rust libraries and learning material like this one! Full Code -------------------------------------------------------------------------------- Here is the full code created in this tutorial: #``` use planck_ecs::*; #[derive(Debug, PartialEq)] enum Color { Red, Blue, Green, } struct ColorChanging; fn change_color_system( changings: &Components, colors: &mut Components, ) -> SystemResult { for (_changing, color) in join!(&changings && &mut colors) { *color.unwrap() = Color::Blue; } Ok(()) } fn main() { let mut world = World::default(); let mut dispatcher = DispatcherBuilder::default() .add(change_color_system) .build(&mut world); let apple = world.get_mut_or_default::().create(); let orange = world.get_mut_or_default::().create(); world .get_mut_or_default::>() .insert(apple, Color::Red); world .get_mut_or_default::>() .insert(apple, ColorChanging); world .get_mut_or_default::>() .insert(orange, Color::Red); dispatcher.run_seq(&mut world).unwrap(); assert_eq!( *world .get::>() .unwrap() .get(apple) .unwrap(), Color::Blue ); assert_eq!( *world .get::>() .unwrap() .get(orange) .unwrap(), Color::Red ); world.get_mut::>().unwrap().remove(apple); world.get_mut::().unwrap().kill(apple); world.maintain(); } #``` Patreon rocket goes brrrrrrrrrrrr . .. =... https://patreon.com/jojolepro =... .. .