
I’m a huge fan of the Bevy Engine for developing games in rust. Recently, I came across an unstable rust feature that I find utterly fascinating and that can make certain Bevy code much more ergonomic: Arbitrary Self Types.
This post assumes basic knowledge about the Bevy ECS and that you do not shy away from using nightly rust (in which case you are missing out, IMO).
Components in Bevy come readily equipped with change detection. That is, in your systems you can filter entities for those that had a specific component changed this frame. This is super useful to avoid expensive calculations. Do I need to recompute the path to the goal? Only if the target changed. Do I need to copy this map block to the GPU again this frame? Only if its contents changed and so on. Bevys powerful ECS also allows more complex queries such as: Run this only for entities for which component A AND B have changed (arbitrary complex boolean logic is possible here).
The way Bevy does this change detection under the hood is with the Mut
struct that represents a mutable borrow to some component. When the Mut is mutably dereferenced (which you necessarily have to do in order to change the underlying component), it will mark the component changed.
Lets look at a minimal example to illustrate:
use bevy::prelude::*;
#[derive(Component)]
struct MyState { ... };
impl MyState {
fn change(&mut self) {
// Depending on the situation, change internal state
// ...
}
}
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(
Update,
(update_state, check_changed_states).chain()
);
app.run();
}
fn setup(mut commands: Commands) {
for _ in 0..1000 {
commands.spawn(MyState { ... });
}
}
fn update_state(mut states: Query<&mut MyState>) {
for mut state in states.iter_mut() {
// Change state here.
// This call derefs the Mut<MyState> into a &mut state for calling .change(),
// which marks this component changed.
state.change();
}
}
fn check_changed_states(mut changed: Query<(), Changed<MyState>>) {
for () in changed.iter_mut() {
// Imagine some expensive or disruptive calculation here such
// as resetting an animation or recomputing a path
// ...
}
}
This little program spawns a number of entities that contain nothing but the MyState
component. Then, each frame it calls a method on the component that may change it. Afterwards, another systems does some heavy work for those entities whose MyState
component was changed. That is, if on the call site we change only some of these entities, we can automatically detect that and do some updates only for those, neat!
But what if on the call site we decide to call change()
but it turns out the new state change
produces is exactly the old one? Well, if you are a good ECS citizen, your components are likely small and simple, so Bevy can help avoid marking the state as changed when its actually still the same. We just need to change .change()
to return a new state (rather than modifying it in place) so Bevy can compare it to the current one:
impl MyState {
fn change(&self) -> Self { ... }
}
fn update_state(mut states: Query<&mut MyState>) {
for mut state in states.iter_mut() {
state.set_if_neq(state.change());
}
}
// rest is unchanged
Nice! This way, we can avoid some unnecessary updates: Only when change()
has been called AND the newly computed state is actually different from the previous one, we will mark the component changed and do the updating work. As a disclaimer, in most cases, this should do the trick and you can stop reading here. If this works for you, this is what the Bevy devs intended and you should look no further.
Still with me? Hm, ok. Lets see … what if MyState
is actually big? Say MyState
is a (potentially large) chunk of tile map data and .change()
updates a single tile in it, potentially assigning it to the same value it had before? In this case we would like to avoid copying the whole chunk and comparing original and copy just to find it wasn’t actually changed. In fact, we want something like .change(&mut self)
to take care of doing this check! But, as we have seen above, just calling that method will mark the component changed immediately, even if it doesn’t actually do anything. So, apart from duplicating some update-checking logic to each call site, what can we do?
Arbitrary Self Types
There is a nice Rust RFC that allows object methods to take any weirdo type for self
, as long as it can Deref
(or more precisely, Receiver
) to Self
. This is actually the case for Mut
, so we can do this:
#![feature(arbitrary_self_types)]
impl MyState {
fn change(self: &mut Mut<Self>) { ... }
}
fn update_state(mut states: Query<&mut MyState>) {
for mut state in states.iter_mut() {
// state will only be marked change if .change() actually derefs the Mut internally, so .change() can decide!
state.change();
}
}
// rest is unchanged
Let’s take a second to unpack this: change
is a method with a self
parameter (in contrast to an associated function). But instead of it being either self
, &self
or &mut self
, it is self: &mut Mut<Self>
, that is, a mutable reference to the component wrapped in Bevys mutable borrow abstraction, Mut
. This happens to be the type of state
on the call site (in the previous examples it was automatically DerefMut’ed into &mut MyState
). Via the Receiver
trait that is implemented for Mut
via Deref
, rust knows that such a method call on a Mut<MyState>
shall be resolved to a call on MyState
. Much like with Deref chains we could also implement Receiver
for MyState
and do a next level of lookup, etc… Much like with Deref
you should take care not to overuse this. A good line of thought is asking yourself whether the type you are implementing Receiver
for can be thought of as a smart pointer. If the answer is no, people may likely find it confusing to use what you are building.
The upside of this new way of calling methods is now change
has full control over change detection. It can peek into the component by just immutably dereferencing self (syntactically, that’s simply reading from fields or calling methods that take eg a &self
) without marking the component changed. Whenever it is sure that the change actually would change something it can mutably deref self (write to a field or call a method with a &mut self
) to do the change and Bevy will correctly detect it.
No danger of messing up circumventing Bevys change detection, its transparent to the call site, and has only one place that decides when to trigger a change.
Have fun with this!