|
| 1 | +--- |
| 2 | +title: How Bevy uses Rust traits for labelling |
| 3 | +categories: |
| 4 | +- rust |
| 5 | +- bevy |
| 6 | +--- |
| 7 | +Out of curiosity I've recently started following the development of [Bevy], |
| 8 | +a game engine written in [Rust]. |
| 9 | +Today I want to talk about how Bevy uses Rust traits to let users very conveniently label elements. |
| 10 | + |
| 11 | +**Note:** The implementation we arrive at is actually very generic |
| 12 | +-- you can easily apply it to any other Rust project. |
| 13 | + |
| 14 | + |
| 15 | +[Bevy]: https://bevyengine.org/ |
| 16 | +[Rust]: https://www.rust-lang.org/ |
| 17 | + |
| 18 | +## How to bevy |
| 19 | + |
| 20 | +Bevy really wants you to use its entity-component-system (ECS) architecture |
| 21 | +to structure your games. |
| 22 | +What is boils down to is writing functions ("systems") |
| 23 | +that use queries to fetch and update components and resources. |
| 24 | +You define what you app/game is by telling Bevy which systems exists |
| 25 | +and how they might be combined. |
| 26 | + |
| 27 | +**Aside:** These "systems functions" are super interesting in themselves: |
| 28 | +They are just regular Rust functions with specific parameters |
| 29 | +and through type-system magic (read: traits) Bevy knows how to call them. |
| 30 | + |
| 31 | +Here's a simple Bevy 0.6 app: |
| 32 | + |
| 33 | +```rust |
| 34 | +use bevy::prelude::*; |
| 35 | + |
| 36 | +fn main() { |
| 37 | + App::new() |
| 38 | + .add_plugins(DefaultPlugins) |
| 39 | + .add_system(clock) |
| 40 | + .run(); |
| 41 | +} |
| 42 | + |
| 43 | +fn clock(time: Res<Time>) { |
| 44 | + println!("Started {}s ago", time.seconds_since_startup()); |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +*Spoiler:* This will spam your terminal with how long the app has been running. |
| 49 | + |
| 50 | +## Defining system relationships using labels |
| 51 | + |
| 52 | +Bevy has a very neat scheduler |
| 53 | +that is able to run all systems that operate on disjoint data in parallel. |
| 54 | +If you want to specify that some systems have to run before others, |
| 55 | +you have to annotate this. |
| 56 | + |
| 57 | +Here's another, slightly more complex example. |
| 58 | +Not that to not be immediately presented with a wall of text, |
| 59 | +we have changed the `add_system` to `add_startup_system`. |
| 60 | +This means the system is only run once, at start-up. |
| 61 | + |
| 62 | +```rust |
| 63 | +use bevy::prelude::*; |
| 64 | + |
| 65 | +fn main() { |
| 66 | + App::new() |
| 67 | + .add_plugins(MinimalPlugins) |
| 68 | + .add_startup_system(setup_world.label("world")) |
| 69 | + .add_startup_system(spawn_player.before("world")) |
| 70 | + .run(); |
| 71 | +} |
| 72 | + |
| 73 | +fn setup_world() { |
| 74 | + println!("one") |
| 75 | +} |
| 76 | +fn spawn_player() { |
| 77 | + println!("two") |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +The idea is that we first setup the world with its map |
| 82 | +and then spawn the component(s) that represent our player. |
| 83 | +If you run this, you will see two lines "one", "two", in that order. |
| 84 | + |
| 85 | +The important two lines of code are the where we give our system the `label("world")`, |
| 86 | +and where the other system can refer to that label |
| 87 | +and declare it wants to run `after("world")`. |
| 88 | + |
| 89 | +**Aside:** How does this work internally? |
| 90 | +Well, long story short, that [`after`][`ParallelSystemDescriptorCoercion`] method turns your system function into a [`ParallelSystemDescriptor`] |
| 91 | +with metadata that the scheduler can pick up and build a graph from. |
| 92 | + |
| 93 | +Feel free to play with this! |
| 94 | +Change it to `before("world")`, |
| 95 | +change the order in which the systems are added, |
| 96 | +add more systems, etc. |
| 97 | + |
| 98 | +Imagine this: |
| 99 | +It's a bit later in the month and |
| 100 | +we have a whole game built using dozens of systems. |
| 101 | +But for some reason the player movement seems a bit broken, |
| 102 | +like it's rendering one frame too late. |
| 103 | +What is the issue? |
| 104 | +After two hours and too much coffee we realize[^warn] that |
| 105 | +we wrote `.after("imput")`. |
| 106 | + |
| 107 | +How can we make sure that a simple typo won't break our game again? |
| 108 | + |
| 109 | + |
| 110 | +[^warn]: To be fair, with the `LogPlugin` Bevy prints a *warning* on start-up about an unknown label. |
| 111 | +But our example immediately starts printing a lot of other things, |
| 112 | +and I guess in this imaginary scenario drinking all this coffee didn't make us more alert after all. |
| 113 | + |
| 114 | + |
| 115 | +[`ParallelSystemDescriptorCoercion`]: https://docs.rs/bevy/0.6.0/bevy/ecs/schedule/trait.ParallelSystemDescriptorCoercion.html |
| 116 | +[`ParallelSystemDescriptor`]: https://docs.rs/bevy/0.6.0/bevy/ecs/schedule/struct.ParallelSystemDescriptor.html |
| 117 | + |
| 118 | +## Get me out of this stringly-typed mess |
| 119 | + |
| 120 | +So far we've used strings to define and refer to labels, |
| 121 | +but if you look at the definition of the `label`, `before`, and `after` methods [here][`ParallelSystemDescriptorCoercion`] |
| 122 | +you will see they actually accept anything that implements [`SystemLabel`]. |
| 123 | + |
| 124 | +If you go to Bevy's API docs you can see [`SystemLabel`] is a trait and defined as |
| 125 | + |
| 126 | +```rust |
| 127 | +pub trait SystemLabel: 'static + DynHash + Debug + Send + Sync { } |
| 128 | +``` |
| 129 | + |
| 130 | +Look at all these bounds! |
| 131 | +You might recognize a few from usual Rust code, |
| 132 | +but `DynHash` stands out as one trait defined in `bevy::utils`. |
| 133 | +We'll come back to it later, and just treat it as the regular `Hash` trait for now. |
| 134 | + |
| 135 | +`SystemLabel` also looks like an empty trait -- but that's actually an illusion. |
| 136 | +Its only item is hidden in the docs. |
| 137 | +We can assume that's because its an implementation detail, |
| 138 | +and instead of implementing this trait manually, |
| 139 | +we are supposed to derive it. |
| 140 | +Indeed, there is a [`SystemLabel` derive macro]. |
| 141 | + |
| 142 | +Okay, so to get a type to be a `SystemLabel`, |
| 143 | +it needs to implement `Debug`, `Hash` |
| 144 | +(the compiler will figure `'static + Send + Sync` out for us). |
| 145 | +As you might know, to derive `Hash`, we also need to derive `PartialEq + Eq`. |
| 146 | +And by experimentation and reading compiler errors, |
| 147 | +we can see that the `SystemLabel` derive actually also adds a requirement on `Clone`. |
| 148 | + |
| 149 | +In the end we arrive at something like this: |
| 150 | + |
| 151 | +```rust |
| 152 | +#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemLabel)] |
| 153 | +enum System { |
| 154 | + Input, |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +Which we can use just like our string previously: |
| 159 | + |
| 160 | +```rust |
| 161 | +.add_startup_system(mouse_input.label(System::Input)) |
| 162 | +.add_startup_system(move_player.after(System::Input)) |
| 163 | +``` |
| 164 | + |
| 165 | + |
| 166 | +[`SystemLabel`]: https://docs.rs/bevy/0.6.0/bevy/ecs/schedule/trait.SystemLabel.html |
| 167 | +[`SystemLabel` derive macro]: https://docs.rs/bevy/0.6.0/bevy/ecs/schedule/derive.SystemLabel.html |
| 168 | + |
| 169 | +## Some notes on the magic |
| 170 | + |
| 171 | +So what is the deal with that [`DynHash`] trait? |
| 172 | +If you look at the [API docs][`DynHash`], |
| 173 | +you can see that it requires an implementation of `DynEq` |
| 174 | +(also from Bevy), |
| 175 | +which in turn requires an implementation of `Any`. |
| 176 | + |
| 177 | +Its methods are also kind of strange: |
| 178 | +Compared to standard library's [`Hash`] trait, |
| 179 | +there are no generics, but a lot of `dyn` keywords. |
| 180 | +This looks to me that someone went out of their way to make |
| 181 | +an [object-safe] version of `Hash`. |
| 182 | + |
| 183 | +The good news is: Users of the API don't have to care: |
| 184 | +`DynHash` is implemented for all types that implement `Hash` and `DynEq`, |
| 185 | +and `DynEq` is in turn implemented for all types that implement `Eq` and `Any`, |
| 186 | + |
| 187 | +Another thing that seems magical is that hidden trait method on `SystemLabel`, |
| 188 | +which is actually called `dyn_clone`. |
| 189 | +Similar to the other dynamic trait implementations, |
| 190 | +this allows cloning any `SystemLabel` type, |
| 191 | +even if all you have is a `Box<dyn SystemLabel>`. |
| 192 | + |
| 193 | + |
| 194 | +[`DynHash`]: https://docs.rs/bevy/0.6.0/bevy/utils/label/trait.DynHash.html |
| 195 | +[`Hash`]: https://doc.rust-lang.org/1.57.0/core/hash/trait.Hash.html |
| 196 | +[object-safe]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html |
| 197 | + |
| 198 | +## Generic label types |
| 199 | + |
| 200 | +Did you think we were done? |
| 201 | +Oh no! There one more thing: |
| 202 | +`SystemLabel` is not alone! |
| 203 | +There is also `StageLabel`, `AmbiguitySetLabel`, and `RunCriteriaLabel`. |
| 204 | + |
| 205 | +These label types are pretty much all the same, |
| 206 | +but distinct traits in the type system. |
| 207 | +That means you will have to explicitly derive `StageLabel` |
| 208 | +if you want to use your type to refer to a stage; |
| 209 | +you can't use a `SystemLabel` or any other label for that. |
| 210 | + |
| 211 | +This is another safety guarantee: |
| 212 | +We already saw that you can mess up your stringy labels by making typos, |
| 213 | +but you can also type everything correctly |
| 214 | +and still refer to a "stage label" in place of a "system label". |
| 215 | +If you use custom types instead of strings, however, |
| 216 | +the compiler will not let you confuse them. |
| 217 | + |
| 218 | +In true Rust fashion all of these labels are implemented using macros. |
| 219 | +The macro is called [`define_label`] |
| 220 | +and it's used [here][label.rs] |
| 221 | +to create all the label traits for the scheduler. |
| 222 | + |
| 223 | +The derive macros are a bit more manual, |
| 224 | +and they live in the `bevy_ecs_macros` crate [here][macros]. |
| 225 | + |
| 226 | + |
| 227 | +[`define_label`]: https://docs.rs/bevy/0.6.0/bevy/utils/macro.define_label.html |
| 228 | +[label.rs]: https://github.com/bevyengine/bevy/blob/e56685370ba82003af60a491667fac209a0f7897/crates/bevy_ecs/src/schedule/label.rs#L4-L7 |
| 229 | +[macros]: https://github.com/bevyengine/bevy/blob/8009af3879fcdb8bad70ee19b36f79100da5ea22/crates/bevy_ecs/macros/src/lib.rs#L429-L438 |
| 230 | + |
0 commit comments