Bevy & Arbitrary Self Types: A match made in heaven

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!

Leave a Comment