Tainted\\Coders

Bevy Reflection

Bevy version: 0.19Last updated:

Reflection is how Bevy accomplishes metaprogramming and runtime introspection.

It's mostly used as a way for plugins (including your own) to interact with reflected objects (dyn Reflect) without knowing their real types at compile time.

With reflection we can:

Bevy uses this power to enable its scenes.

Reflectable types

The Reflect trait enables serialization, deserialization, and dynamic property access. Deriving it requires the type to be 'static (so it implements Any) and Send + Sync, and every active field must itself be reflectable.

PartialReflect is a supertrait of Reflect. It is responsible for just the introspection side. Reflect takes this further, it guarantees the value mirrors a single Rust type: a dyn Reflect trait object. This trait object can be directly downcast to its concrete type at runtime, while a dyn PartialReflect cannot.

When you #[derive(Reflect)] you also get the following implementations:

use bevy_reflect::{Reflect, Reflectable};

#[derive(Reflect)]
struct MyStruct<T: Reflectable> {
  value: T,
}

Reflectable is a convenience trait that bundles the core reflection traits. It's auto-implemented for any type satisfying them. It isn't in the prelude, so you need to import it. It's the recommended bound for generic type parameters in reflection code.

Reflect 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 (usually through the Reflect derive macro).

Let's start small by defining a simple reflected type:

use std::ops::RangeInclusive;

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

When you use a derive macro for reflection, all active fields need to also be reflectable. Fields can opt out entirely with #[reflect(ignore)] because the generated FromReflect still has to reconstruct them. Ignored fields must implement Default or provide #[reflect(default = "path::to::function")].

The macro will also generate a FromReflect implementation, which lets a dynamic representation be converted back into this concrete type when loading a scene.

Every non-generic type that derives Reflect is automatically registered on App startup (reflect_auto_register is part of Bevy's default features). Generic types still need to be registered manually, and any type can opt out with #[reflect(no_auto_register)].

To take part in scene/world extraction, a component or resource must also register the relevant type data with #[reflect(Component)] or #[reflect(Resource)]. Without it, the type is reflected, but DynamicWorldBuilder won't be able to extract it.

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    // `Slider` derives `Reflect`, so it is registered automatically on startup.
    // Generic types are not, and would need an explicit `.register_type::<...>()`.
    .add_systems(Startup, some_system);
}

Bevy adds registered types 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 fields by their string names:

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::<f32>("value").unwrap() = 0.75;
  assert_eq!(slider.value, 0.75);

  // You can read a field back by name and type like this:
  assert_eq!(*slider.get_field::<f32>("value").unwrap(), 0.75);

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

  // Fields are handed out as `&dyn PartialReflect`, which cannot be downcast,
  // so we convert it to a `&dyn Reflect` first:
  let fully_reflected_field = field.try_as_reflect().unwrap();

  // Now you can downcast Reflect values like this:
  assert_eq!(*fully_reflected_field.downcast_ref::<f32>().unwrap(), 0.75);
}

The operations specific to a type are encapsulated by the reflection subtraits. The derive macro implements the appropriate subtrait automatically.

Types that don't fall into any of the subtraits above are known as opaque types. They hide their internal structure from reflection, either because it isn't possible or isn't useful to expose it. This covers types like String and Instant, all of the primitive types (bool, usize, etc.), and any type marked with #[reflect(opaque)].

Types from crates

If we use something like rust_decimal as a field on our components you can run into trouble deriving Reflect on your types:

#[derive(Reflect)]
struct Trader {
  balance: Decimal
}

This would pop up saying:

error[E0277]: the trait bound `rust_decimal::Decimal: FromReflect` is not satisfied
  = note: `rust_decimal::Decimal` does not implement `FromReflect` so cannot be created through reflection
  = note: consider annotating `rust_decimal::Decimal` with `#[derive(Reflect)]`

We have a few options to get around this for foreign types:

  1. Maintain your own fork and derive it
  2. Provide a type you convert to/from Decimal
  3. Create a newtype that wraps Decimal and manually implement the trait
  4. Treat the containing type as opaque and serialize it through serde

For rust_decimal we can mark our type opaque with #[reflect(opaque)], implement Serialize and Deserialize (which rust_decimal already provides), and register those traits:

use serde::{Serialize, Deserialize};

#[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)]
#[reflect(Serialize, Deserialize)]
struct Trader {
  balance: Decimal,
}

#[reflect(opaque)] requires the type to implement Clone and hides its fields from reflection. Because the serializer falls back to the type's own Serialize/Deserialize implementations, Decimal no longer needs to implement Reflect itself.

Reflecting Serialize/Deserialize alone does not remove the field bound: a non-opaque type with a foreign field still has to make that field reflectable (options 1-3).

Serializing and deserializing structs

Structs that are reflectable can be serialized automatically into Rusty Object Notation or ron for short. The derive registers the SerializationData the reflection serializers need, so reflectable types don't also have to derive serde::Serialize.

Deserializing works the same way but in reverse. We take a ron string and convert it back to the object.

fn serialize_type(type_registry: Res<AppTypeRegistry>) {
  let mut slider = Slider { value: 0.5 };
  let type_registry = type_registry.read();
  let serializer = ReflectSerializer::new(&slider, &type_registry);
  let ron_string =
    ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default())
      .unwrap();
  info!("{}\n", ron_string);

  let reflect_deserializer = ReflectDeserializer::new(&type_registry);
  let mut deserializer = ron::de::Deserializer::from_str(&ron_string).unwrap();
  let reflect_value =
    reflect_deserializer.deserialize(&mut deserializer).unwrap();
}

This is the basics of how scenes work. We save a scene (a collection of our entities and components) as a ron file and deserialize them to load the scene into our game.

Reflecting traits

Traits can also be set up 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.ron files) that contain a collection of entities and components that represent a snapshot of game data.

Here is what a .scn.ron file looks like:

(
  resources: {
    "world_serialization::ResourceA": (
      score: 1,
    ),
  },
  entities: {
    4294967297: (
      components: {
        "bevy_ecs::name::Name": "joe",
        "bevy_transform::components::global_transform::GlobalTransform": ((
          1.0,
          0.0,
          0.0,
          0.0,
          1.0,
          0.0,
          0.0,
          0.0,
          1.0,
          0.0,
          0.0,
          0.0
        )),
        "bevy_transform::components::transform::Transform": (
          translation: (0.0, 0.0, 0.0),
          rotation: (0.0, 0.0, 0.0, 1.0),
          scale: (1.0, 1.0, 1.0),
        ),
        "world_serialization::ComponentA": (
          x: 1.0,
          y: 2.0,
        ),
        "world_serialization::ComponentB": (
          value: "hello",
        ),
        "bevy_world_serialization::components::WorldAssetRoot": (Path("models/FlightHelmet/FlightHelmet.gltf#Scene0")),
      },
    ),
    4294967298: (
      components: {
        "world_serialization::ComponentA": (
          x: 3.0,
          y: 4.0,
        ),
      },
    ),
  },
)

We don't write these by hand, instead we use the bevy_world_serialization 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.