Skip to content

Commit 4de4bdf

Browse files
author
Pascal Hertleif
authored
Merge pull request #41 from killercup/bevy-labels
Add posts on Bevy labels
2 parents 8c63583 + 203a679 commit 4de4bdf

1 file changed

Lines changed: 230 additions & 0 deletions

File tree

_posts/2022-01-10-bevy-labels.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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

Comments
 (0)