前面我们已经介绍完了ECS三个主要构件,简单的总结下他们的功能:
- Entity,一条Component组成的记录。
- Component,定义数据类型与功能。
- System,定义Component的行为。
但是我们对于操作Component,我们如何才能找到这些Component实例呢?
查询
查询是一种声明性质的方法,用于制定我们系统中想要的Component数据。它们仅在迭代组件时,才会根据组件的规范从游戏World中获取组件。
我们通过将查询作为参数添加到我们的系统来指定查询。然后,Bevy会在调度的时候将参数注入系统。
指定一个查询
创建一个查询,使用Query,并放入到我们的System参数列表中:
//Q:想要查询到的数据
//F:过滤器
Query<Q, F>
//我要获取Ball数据的引用(只读)
//指定Ball数据必须和Player是同一条Entity
Query<&Ball, With<Player>>
//我要获取Ball数据的可写引用
//指定Ball数据必须和Player,Living是同一条Entity(必须同时包含两个Component)
Query<&mut Ball, (With<Player>, With<Living>)>
当指定Query作为系统参数时,它不会立即从World中获取数据。只有当我们迭代该查询时,它才会进行实际的操作。这意味着,查询本身的成本非常低。
访问组件
我们注意到,在上面的示例中,泛型Q指定了类型&T或者&mut T,我们可以获取借用或者可变借用。
当我们使用&T的时候,我们只能读取数据。如果我们不需要修改数据,那么只读引用就是更好的选择,因为这样Bevy就可以并行运行这些系统,提供个更好的性能。
当我们需要更改数据的时候,我们就需要使用&mut T,这样做的唯一缺点是,无法并行运行同时引用了T的系统。
元组参数
对于Q,我们可以使用原则以获取更多的数据:
//查询Ball和Player
Query<(&Ball, &Player)>
上面这个查询有与的关系,即告诉系统:
我既要
Ball,也要Player,你必须给我返回同时拥有Ball和Player的数据。
其实和Query<&Ball, With<Player>>用法是类似的。
获取数据
Query<Q, F>的第一个参数表示我们想要获取的数据:
#[derive(Component, Debug)]
struct Player;
fn fetch_players(query: Query<&Player>) {
for player in &query {
info!("Player: {:?}", player);
}
}
此查询等于说:
我要所有的
Player。
但是如果我们使用Query<(&Ball, &Player)>,意义就会发生变化:
必须给我返回同时拥有
Ball和Player的数据,同时我会访问Ball和Player。
简单的元组组合不足以指定我们想要的所有复杂查询。有一些方便的类型能够表达更复杂的查询:
| 参数 | 描述 |
|---|---|
Option<T> | 该组件有无皆可,无就返回None |
AnyOf<A, B, C> | 获取具有A,B,C类型中任意类型的组件 |
Ref<T> | T类型的共享借用 |
Entity | 返回实体 |
Option
如果我们想:
给我所有的
Player或者Ball。
那么通过元组和Option的组合,我们就能表达这种逻辑:
fn fetch_players_or_balls(
query: Query<(Option<&Player>, Option<&Ball>)>,
) {
for (player, ball) in &query {
if let Some(player) = player {
info!("Player: {:?}", player);
}
if let Some(ball) = ball {
info!("Ball: {:?}", ball);
}
}
}
AnyOf
如果我们想:
我想要
Player,或者Ball,或者Living,反正给我任何一条数据即可
那么使用AnyOf:
Query<AnyOf<(&Player, &Ball, &Living)>>
这个查询相当于:
Query<(
Option<&Player>,
Option<&Ball>,
Option<&Living>
),
Or<(
With<Player>,
With<Ball>,
With<Living>
)>>
每个类型以Option<T>返回,因为实体可能包含其中任意一个组件。注意这个查询只需要满足一个条件,也就是在Player、Ball、Living中,至少需要一条数据。
Ref
如果我们想知道实体如何改变,我们可以使用Ref<T>参数:
fn react_to_player_spawning(query: Query<Ref<Player>>) {
for player in &query {
if player.is_added() {
// Do something
}
}
}
它和Query<&Player>类似,只不过我们能得到一些别的功能:
| 方法 | 描述 |
|---|---|
is_added | 如果在系统运行后添加此值,则返回true |
is_changed | 如果添加了该值,则返回true,如果使用过可变借用,也会返回true |
last_changed | 记录该数据最近更改的时间 |
Entity
作为查询的一部分,我们可以请求Entity,如Query<Entity, With<Player>>,如果是单独的Entity,它并没有什么实际意义,但是一旦拥有了此Entity,我们可以在查询上使用某些方法来从中获取实体而不是所有实体的组件:
fn fetch_rocket_by_player_entity(
players: Query<Entity, With<Player>>,
query: Query<&Rocket>
) {
for player in &players {
let rocket = query.get(player).unwrap();
}
}
Query过滤器
Query<Q, F>的第二个参数是QueryFilter,也就是过滤器,这些过滤器的条件包含:
| 方法 | 描述 |
|---|---|
With<T> | 必须带有T组件的项目 |
Without<T> | 必须没有T组件的项目 |
Or<F> | 检查元组中的所有过滤器是否有任意满足条件F |
Changed<T> | 发生变化的T组件 |
Added<T> | 刚添加的T组件 |
如果我们对一个查询使用QueryFilter,可以这么编写:
Query<Player, Added<Player>>
这和使用Ref<T>跟踪器异曲同工:
fn react_to_player_spawning(
query: Query<Ref<Player>>
) {
for player in &query {
if player.is_added() {
// Do something
}
}
}
性能上,也是等效的。
可变借用查询的陷阱
假设我们现在有这样一个查询:
fn fetch_players_and_rockets(
players: Query<&mut Player, With<Rocket>>,
rockets: Query<&mut Player, With<Invincibility>>
) {
// This may panic at runtime
}
两个查询中,均有对于Player的可变借用。如果这两条记录中,都有对于Player可变借用,那么该系统在运行时,便会panic。因为在Rust中,对于一条数据不能同时拥有两个可变借用。
下面是修改方法之一:
fn fetch_players_and_rockets(
players: Query<&mut Player, (With<Rocket>, Without<Invincibility>)>,
rockets: Query<&mut Player, (With<Invincibility>, Without<Rocket>)>
) {
// This may panic at runtime
}
使用Without<T>,将两个查询变成不相交的查询。
获取组件
我们已经构建了查询了,那么应该如何获取这些组件呢?Query提供了几种方法:
| 方法 | 描述 |
|---|---|
iter | 返回所有项迭代器 |
for_each | 为每个项并行运行既定函数 |
iter_many | 为每个与实体列表匹配的项运行给定函数 |
iter_combinations | 在指定数量的项上做组合,并返回迭代器 |
par_iter | 返回并行迭代器 |
get | 返回给定实体的查询项 |
get_component<T> | 返回给定实体的组件 |
many | 返回给定实体列表的查询项 |
get_single | single 的安全版本,返回一个 Result<T> |
single | 返回查询项,若不是单项则panic(程序异常终止) |
is_empty | 如果查询为空,返回 true |
contains | 如果查询包含给定的实体,返回 true |
每种方法还具有相应的*_mut变体,该变体将返回具有可变所有权的组件。这使得我们可以更改数据,而不仅仅是阅读数据。
下面,将介绍一些经常用到的获取组件的实用方法。
获取单个实体
fn move_player(
mut query: Query<&mut Transform>
) {
let mut transform = query.single_mut();
transform.translation.x += 1.;
}
但是,如果有一个以上的实体包含一个Transform组件,则此方法会panic。
我们可以更改为更加安全的用法:
fn move_player_safely(
mut query: Query<&mut Transform>
) {
if let Ok(mut transform) = query.get_single_mut() {
transform.translation.x += 1.
}
}
获取多个实体
最常见方法是使用iter来枚举存在的每个组件:
fn move_players(
mut query: Query<&mut Transform>
) {
// We can enumerate all matches
for mut transform in query.iter_mut() {
transform.translation.x += 1.;
}
}
亦或是:
fn move_players_shorthand(
mut query: Query<&mut Transform>
) {
// We can enumerate all matches
for mut transform in &mut query {
transform.translation.x += 1.;
}
}
当然,在Rust中,使用for_each,能够得到更快的性能。
fn fast_move_players(
mut query: Query<&mut Transform>
) {
// We can enumerate all matches
query.iter_mut().for_each(|mut transform| {
transform.translation.x += 1.;
});
}
查询的性能
如果两个系统都访问同一个数据,其中至少有一个访问是可变借用,则两个系统无法并行执行。
for_each方法通常比其iter版本更快(差别不大)。
总结
在Bevy的ECS架构中,查询系统是连接组件与行为的桥梁。通过Query<Q,F>声明式语法,开发者可快速筛选出符合条件的数据(如同时拥有Player和Ball的实体)。而查询仅在迭代时执行,轻量低耗,兼顾灵活性与性能,是复杂游戏逻辑一种优雅解决方案,也是Bevy ECS的重要组成部分。