Bevy Reflection
Reflection is how Bevy accomplishes its meta programming which is a dynamic way of interacting with Rust types at runtime.
With reflection we can:
- Dynamically grab rust values
- Access metadata at runtime about our types
- Serialize and deserialize data
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:
Tuple
Array
List
Map
Struct
TupleStruct
Enum
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.