Bevy Reflection
Reflection is how Bevy accomplishes meta programming 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:
- Dynamically access fields by their string names
- Access metadata at runtime about our types
- Interact with fields using their names (for named structs) or indices (for tuple structs)
- "Patch" your types with new values
- Look up nested fields using "path strings""
- Iterate over struct fields
- Automatically serialize and deserialize via Serde
- Trait "reflection"
Bevy uses this power to enable its scenes.
Reflectable types
The Reflect
trait enables serialization, deserialization, and dynamic property access. The only requirement of this trait being that each field also implements Reflect
unless they are ignored or given a default.
The Reflect
trait is a supertrait of PartialReflect
which is responsible for just the introspection side. Reflect
takes this further by allowing downcasting of the type to its actual concrete type during runtime.
When you #[derive(Reflect)]
you also get the following implementations:
GetTypeRegistration
: A trait which allows a type to generate itsTypeRegistration
for registration into theTypeRegistry
.Typed
: A static accessor to compile-time type information.Struct
,TupleStruct
, orEnum
depending on the type
#[derive(Reflect)]
struct MyStruct<T: Reflectable> {
value: T
}
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).
Lets start small by defining a simple 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)]
or #[reflect(default)]
written above the field.
The macro will generate a FromReflect
implementation which allows it to be serialized and deserialized from a scene.
If we derive it on 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 not, 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("a").unwrap() = 2usize;
assert_eq!(slider.value, 2.);
assert_eq!(*slider.get_field::<usize>("a").unwrap(), 2);
// You can also get the &dyn PartialReflect value of a field like this
let field = slider.field("a").unwrap();
// Now, you can downcast your `Reflect` value like this:
let fully_reflected_field = field.try_as_reflect().unwrap();
// you can downcast Reflect values like this:
assert_eq!(*fully_reflected_field.downcast_ref::<usize>().unwrap(), 2);
}
The different operations that are specific to type are encapsulated by the reflection subtraits. This value is inferred automatically by the derive macro.
Tuple
Set
List
Array
Map
Struct
TupleStruct
Enum
Function
Then there are the primitive types that do not fall into any of the subtypes mentioned above. These are known as opaque types. These are the types that cannot be broken down any further.
Serializing and deserializing structs
Structs that are reflectable can be serialized automatically into Rusty Object Notation or ron
for short.
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 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>
// Even though we say the type inside, you're just going to have to imagine
// its opaque.
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: 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),
),
"scene::ComponentA": (
x: 1.0,
y: 2.0,
),
"scene::ComponentB": (
value: "hello",
),
},
),
4294967298: (
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.