Tainted\\Coders

Bevy Reflection

Bevy version: 0.14Last updated:

Reflection is how Bevy accomplishes its meta programming which is a dynamic way of interacting with Rust types at runtime.

With reflection we can:

Bevy uses this power to enable its scenes.

The Reflect trait enables serialization, deserialization, and dynamic property access.

Reflectable types

The Reflect trait is what powers reflection in Bevy.

It allows Bevy to pass around types that implement this trait as a dyn Reflect trait object.

This means we don't have to care about the specific type at compile time. We can take these trait objects and turn them back into their original types by implementing the FromReflect trait.

Lets start small by defining a reflected type:

use std::ops::RangeInclusive;

#[derive(Reflect, Component)]
struct Slider {
  #[reflect(@RangeInclusive::<f32>::new(0.0, 1.0))]
  value: f32,
}

When you use a derive macro for reflection, all fields need to also be Reflect unless you explicitly disable it with the #[reflect(ignore)] attribute.

The macro will generate a FromReflect implementation which allows it to be serialized and deserialized from a scene.

If we are a component or resource, the macro will register our type in the worlds AppTypeRegistry resource. This enables automatic extraction when we serialize our scene using a DynamicSceneBuilder.

If we were not a component or resource, the next step would be to register this type in our App so our type registry can be aware of it.

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .register_type::<Slider>();
}

When we do this, Bevy adds our type to its AppTypeRegistry resource. This is actually a shared pointer to the real TypeRegistry which is where we store all the metadata about each of our types.

This lets us dynamically access the fields from the string value of its property:

fn some_system() {
  let mut slider = Slider { value: 0.5 };

  // You can set field values like this. The type must match 
  // exactly or this will fail.
  *slider.get_field_mut("value").unwrap() = 2usize;
  assert_eq!(slider.value, 2.);
  assert_eq!(*slider.get_field::<usize>("a").unwrap(), 2);

  // You can also get the &dyn Reflect value of a field like this
  let field = slider.field("value").unwrap();

  // you can downcast Reflect values like this:
  assert_eq!(*field.downcast_ref::<usize>().unwrap(), 2);
}

In addition to Reflect there are a bunch of subtypes, each with operations specific to their type:

This lets us be specific about the type of of trait object we are interested in:

let my_struct: Box<dyn Struct> = Box::new(MyType { value: 123 });
let foo: &dyn Reflect = my_struct.field("value").unwrap();
assert_eq!(Some(&123), foo.downcast_ref::<i32>());

Then there are the primitive types that do not fall into any of the subtypes mentioned above. These are known as value types. These are the types that cannot be broken down any further.

Reflecting traits

Traits can also be setup for reflection using the reflect_trait attribute macro:

#[derive(Reflect)]
#[reflect(DoThing)]
struct MyType {
  value: String,
}

#[reflect_trait]
trait DoThing {
  fn do_thing(&self) -> String;
}

impl DoThing for MyType {
  fn do_thing(&self) -> String {
    format!("{} World!", self.value)
  }
}

This will generate a ReflectDoThing type we can use to dynamically access our types from a trait:

fn trait_reflection(type_registry: Res<AppTypeRegistry>) {
  // First, lets box our type as a Box<dyn Reflect>
  let reflect_value: Box<dyn Reflect> = Box::new(MyType {
    value: "Hello".to_string(),
  });

  // This means we no longer have direct access to MyType or its methods. We can only call Reflect
  // methods on reflect_value. What if we want to call `do_thing` on our type? We could
  // downcast using reflect_value.downcast_ref::<MyType>(), but what if we don't know the type
  // at compile time?

  // Normally in rust we would be out of luck at this point. Lets use our new reflection powers to
  // do something cool!
  let type_registry = type_registry.read();

  let reflect_do_thing = type_registry
    .get_type_data::<ReflectDoThing>(reflect_value.type_id())
    .unwrap();

  // We can use this generated type to convert our `&dyn Reflect` reference to a `&dyn DoThing`
  // reference
  let my_trait: &dyn DoThing = reflect_do_thing.get(&*reflect_value).unwrap();

  // Which means we can now call do_thing(). Magic!
  info!("{}", my_trait.do_thing());
}

Reflection and scenes

Scenes are serialized files (usually stored as .scn files) that contain a collection of entities and components that represent a snapshot of game data.

Here is what a .scn file looks like:

(
  resources: {
    "scene::ResourceA": (
      score: 2,
    ),
  },
  entities: {
    4294967296: (
      components: {
        "bevy_transform::components::transform::Transform": (
          translation: (
            x: 0.0,
            y: 0.0,
            z: 0.0
          ),
          rotation: (
            x: 0.0,
            y: 0.0,
            z: 0.0,
            w: 1.0,
          ),
          scale: (
            x: 1.0,
            y: 1.0,
            z: 1.0
          ),
        ),
        "scene::ComponentB": (
          value: "hello",
        ),
        "scene::ComponentA": (
          x: 1.0,
          y: 2.0,
        ),
      },
    ),
    4294967297: (
      components: {
        "scene::ComponentA": (
          x: 3.0,
          y: 4.0,
        ),
      },
    ),
  }
)

We don't write these by hand, instead we use the bevy_scene crate to serialize our scenes and save them to disk.

Bevy will take these files, and use reflection to deserialize them into the actual components and types your game needs.