本文依据官方反射的示例代码,简要介绍 Bevy 反射系统(0.17.0-dev)的基本实现原理。
Bevy 正在快速迭代中,反射系统的实现与 API 可能发生变动。
Reflect 特质
参考官方示例代码
Bevy 反射系统提供了 Reflect
derive 宏为自定义类型生成代码,使其可以转换为 dyn Reflect
对象进行操作。
1. 动态获取类型名
Bevy 定义了一个 DynamicTypePath
Trait ,它仅提供获取类型名的函数:
pub trait DynamicTypePath {
fn reflect_type_path(&self) -> &str; // 如 crate::my_mod::MyStruct<u32>
fn reflect_short_type_path(&self) -> &str; // 如 MyStruct<u32>
fn reflect_type_ident(&self) -> Option<&str>; // 如 MyStruct
fn reflect_crate_name(&self) -> Option<&str>; // 如 crate
fn reflect_module_path(&self) -> Option<&str>; // 如 my_mode
}
#[derive(Reflect)]
宏会为类型自动实现此 Trait 。
这是 Bevy 反射系统的基础 Trait 之一,其他反射特质都要求实现此特质,因此各种类型可以调用这些函数获取类型名:
// ptr: Box<dyn Reflect>
println!("{}", ptr.reflect_type_path());
2. 类型描述信息
仅有类型名是不够的,我们需要在运行时(以字符串的形式)获取类型的详细定义,Bevy 为此定义了一个 DynamicTyped
Trait :
pub trait DynamicTyped {
fn reflect_type_info(&self) -> &'static TypeInfo;
}
此 Trait 也由 #[derive(Reflect)]
宏自动实现,它返回一个 TypeInfo
枚举,每种枚举值都是一个结构体,代表不同情况的完整类型信息:
StructInfo
TupleStructInfo
TupleInfo
ListInfo
ArrayInfo
MapInfo
SetInfo
EnumInfo
OpaqueInfo
以 StructInfo
为例,它包含以下内容:
- 类型名,类型ID (
std::any::TypeId
) - 泛型信息,自定义属性
- 字段名列表,字段信息列表
OpaqueInfo
是特殊类型,表示内部详情不可见,因此只提供 类型ID、路径名、泛型信息。
注意这些结构体是描述类本身的,因此不涉及具体的字段值。
3. 类型操控 Trait
除了获取类型信息,我们还希望能够通过 dyn Reflect
操控数据,比如通过字符串或整数索引访问内部字段。Bevy 为此定义了一系列 Trait:
Struct
TupleStruct
Tuple
List
Array
Map
Set
Enum
PartialReflect
这里的九种类型和 TypeInfo
中的恰好对应。PartialReflect
是个特殊的类型、我们会单独介绍,它不提供内部字段的访问,因此与 OpaqueInfo
类型对应。
其他八种特质都要求先实现 PartialReflect
,然后各自定义了一些不同的方法访问内部字段。
以 Struct
为例,可以通过字符串形式的字段名获取内部字段的引用,但需要显式指定类型,因为内部字段类型依然不确定。
ReflectRef::Struct(value) => {
_ = value.field("x"); // 返回 Option<&dyn PartialReflect>
// GetFiled 特征,在 Struct 的基础上提供显式转换的功能
println!("{}", value.get_field::<usize>("x").unwrap());
}
这些 Trait 也是通过 #[derive(Reflect)]
宏自动实现的。
4. PartialReflect
PartialReflect
特质如同字面意思“部分反射”,它要实现 DynamicTypePath
,本身不提供访问内部字段的功能,但给出了一组适用于所有反射类型的操作函数,重点如下:
reflect_ref
函数:尝试转换成上面提到的类型操控 Trait 的对象,返回一个枚举表示九种特质apply
函数:将另一个dyn PartialReflect
对象逐字段赋值给自身,不要求类型相同,无视多余字段try_as_reflect
函数:尝试转换成&dyn Reflect
以获得更多功能to_dynamic
函数:根据自身内容生成一个动态类型的dyn PartialReflect
参考官方示例代码
由于 apply
的存在,很多功能都是以 PartialReflect
类型为基础的。
dyn PartialReflect
类型还提供了向下转换,但实际是先转换成 dyn Reflect
在调用它的向下转换。
5. Reflect
Reflect
Trait 要求实现 PartialReflect
、 DynamicTyped
和 Any
,这里的重点是 Any
约束,因此可以向上转换成 &dyn Any
,还可以调用 Any
的向下转换变回原始类型。
实际上它也只提供了向上转换到 &dyn Any
和尝试向下转换回原类型这两种功能。
向下转换实际调用 Any
的向下转换,因此要求对象本身的 TypeId
和转换的目标类型一致。
ReflectRef::Struct(value) => {
println!("{}", value.get_field::<usize>("x").unwrap());
}
比如这段代码,实际是使用 Struct
的 field
方法获取了 &dyn PartialReflect
,然后转换回 &dyn Reflect
,再转换回目标类型 &usize
。
6. 动态类型
参考官方示例代码
特质对象 &dyn ...
实际是一个指向原对象的指针,并绑定了一个虚表。
由于原对象的类型是固定的,虽然你可以尝试访问和修改特定字段,却无法自由地增加、减少或重命名字段。
Bevy 为此提供了一些动态类型,可以自由增减字段:
DynamicStruct
DynamicTupleStruct
DynamicTuple
DynamicList
DynamicArray
DynamicMap
DynamicSet
DynamicEnum
DynamicOpaque
。
以 DynamicStruct
为例,它包含如下字段:
- TypeInfo
- 字段名列表,字段值列表(
Vec<Box<dyn PartialReflect>>
) - 字段名->字段索引的映射(类似
HashMap<&str, usize>
)
它们的类型操作特质(比如 Struct
)的函数实现不再是访问实际字段,而是指向了这些容器中的元素。
这样的好处是你可以自由的增加想要的字段和删除不需要的字段,但它们是独立的类型,具有固定的 TypeId
,无法通过 dyn Reflect
向下转换成其他类型,即使内部数据能够匹配。
回顾之前的内容,我们提到 PartialReflect
的 apply
方法能够将另一个 dyn PartialReflect
逐字段赋值给自身。它注重内容而非 TypeId,因此你可以先创建一个自定义类型的对象,然后使用 apply
方法用动态类型对象的内容填充自定义对象。
这在序列化与反序列化中非常有用,比如你可以读取 GLFT 数据生成动态类型对象,然后使用它填充自定义类型(比如 Mesh、Scene)。
注册器与 TypeData
通过上面的内容,我们已经能够通过 &dyn Reflect
操控各种不同的对象,在运行时通过输入字符串访问特定字段。
注册器主要用于将字符串形式的类型名与具体的类型信息一一对应,以便运行时查询。 而 TypeData 则是解决函数调用的问题,下面就会提到。
参考官方示例代码
1. TypeRegistry
Bevy 的反射库提供了一个 TypeRegistry
,它用于存储类型信息,也就是 TypeInfo
。
它的内容如下:
pub struct TypeRegistry {
registrations: TypeIdMap<TypeRegistration>,
short_path_to_id: HashMap<&'static str, TypeId>,
type_path_to_id: HashMap<&'static str, TypeId>,
ambiguous_names: HashSet<&'static str>,
}
它存储了类型名到 TypeId
的映射和 TypeId
到 TypeRegistration
的映射 ,后者的定义如下:
pub struct TypeRegistration {
data: TypeIdMap<Box<dyn TypeData>>,
type_info: &'static TypeInfo,
}
这里的 type_info
就是自身的类型名定义信息。
奇怪的是 TypeIdMap<Box<dyn TypeData>>
,用其他类型的 TypeId
映射到了 TypeData
,什么是 TypeData
?
2. TypeData
思考这样一个问题,你有类型 T
并通过宏为其实现了反射功能,现在可以通过 &dyn Reflect
操作它。
仅字段修改是不够的,你还为 T
实现了一个 Print
特质、内涵 print
函数,可是怎么通过 &dyn Reflect
调用它?
即使底层类型就是 T
也没法通过 &dyn Reflect
对象调用 print
方法,一种可行的思路是将 &dyn Reflect
转换回 &T
再转换成 &dyn Print
,而 TypeData
做的就是这件事。
你只需要为 Print
特质加上 #[reflect_trait]
宏(或者为 T
类型加上 #[reflect(Print)]
宏),Bevy 会为你自动生成一个 ReflectPrint
类型,它内部只有几个函数指针:
struct ReflectPrint {
get_func: fn(&dyn Reflect) -> Option<&dyn Print>,
get_mut_func: fn(&mut dyn Reflect) -> Option<&mut dyn Print>,
get_boxed_func: fn(Box<dyn Reflect>) -> Result<Box<dyn Print>, Box<dyn Reflect>>,
}
宏会为此类型实现 TypeData
和 From<T>
特质,后者仅提供一个 from_type()
函数用于填充这三个函数指针。
这些函数指针的内容并不复杂,调用 &dyn Reflect
的向下转换函数转换成 &T
,然后在转换回 &dyn Print
。
使用方式请参考官方示例代码
3. AppTypeRegistry
AppTypeRegistry
是反射系统为 ECS 添加的一个全局资源,内部是一个 Arc<RwLock<TypeRegistry>>
,你可以线程安全地获取内部类型注册器并使用。
特殊的一点是,使用 #[derive(Reflect)]
标记的类会自动注册到此资源中,这里使用了一些“黑魔法”。
自动注册-参考官方示例文档
反射函数
除了简单的类型反射,Bevy 还实现了一套高级的函数动态反射系统,这需要启用 reflect_functions
特性。
参考官方示例代码
// TODO