Query:精确取数据
Query 的类型有两个槽位:Query<D, F>。D(data)说“取什么”,F(filter)说“筛哪些行”,F 可以省略。第 3 章用过的形态都能对号入座:Query<&Health> 只填了 D;Query<(Entity, &Name, &Health)> 的 D 是元组;Query<&mut Health, With<Monster>> 两个槽位都有。本节穷尽 D 槽位的常用写法,过滤器留给下一节。
必有、可选、在场与否
到目前为止,D 里写 &T 意味着“该实体必须有 T 这一列,借我读”——缺这一列的行直接不匹配。但牧场上的住户参差不齐:羊有饥饿值,牧羊犬没有;有的戴铃铛,有的没戴。想把它们放进同一张花名册,就需要两种更宽容的取法:
fn spawn_farm(mut commands: Commands) {
commands.spawn((Name::new("小白"), Hunger(6), Bell));
commands.spawn((Name::new("小黑"), Hunger(9)));
commands.spawn((Name::new("阿黄"), Bell));
commands.spawn(Name::new("灰背"));
}fn roster(animals: Query<(Entity, &Name, Option<&Hunger>, Has<Bell>)>) {
println!("=== 农场花名册 ===");
for (entity, name, hunger, has_bell) in &animals {
let hunger_text = match hunger {
Some(hunger) => format!("饥饿 {}", hunger.0),
None => "饥饿 —".to_string(),
};
let bell_text = if has_bell { "戴铃铛" } else { "无铃铛" };
println!("{entity} {name} {hunger_text} {bell_text}");
}
}Listing 4-2(节选):Option 取“可能没有的列”,Has 只问在不在场
Option<&T>:实体有T就给Some(&T),没有也照样匹配,给None。带上它不会缩小查询范围。Has<T>:只回答“这一列在不在场”,给一个bool,完全不读数据。
cargo run -p ch04-systems-queries --example listing-04-02=== 农场花名册 ===
2v0 阿黄 饥饿 — 戴铃铛
3v0 灰背 饥饿 — 无铃铛
1v0 小黑 饥饿 9 无铃铛
0v0 小白 饥饿 6 戴铃铛四个实体全部上榜——&Name 是唯一的硬性条件,Option 和 Has 都不淘汰任何行。顺序又一次不按生成顺序来:四个实体的组件组合各不相同,分属四张子表(第 3 章讲过的 Archetype),不必惊讶。
Option<&T> 和 Has<T> 的分工很简单:要读值用前者,只看有无用后者。Has 还有个隐藏的好处——它不访问数据,所以永远不会和别的系统抢 T 这一列,这在后面讲冲突时会显出价值。
顺带补全元组的规则:D 槽位的元组最多 15 个成员,嵌套元组可以突破上限——和 Bundle 一个味道。
按 Entity 直取某一行
for 循环是“全体过一遍”,但很多逻辑长的是另一个样子:手里攥着某个实体的 ID,只想要它那一行。第 3 章说过 Entity 可以存进组件,作为指向另一个实体的引用——现在兑现它。牧羊犬阿黄心里有一只最爱的羊:
fn spawn_farm(mut commands: Commands) {
commands.spawn((Name::new("小白"), Sheep, Hunger(6)));
let curly = commands.spawn((Name::new("卷卷"), Sheep, Hunger(4))).id();
commands.spawn((Name::new("阿黄"), Sheepdog, Favorite(curly)));
commands.spawn((Name::new("灰背"), Wolf));
}spawn(...).id() 当场拿到卷卷的 Entity(第 3 章讲过:ID 是预留的,不用等实体真正出生),转手存进阿黄的 Favorite 组件。之后任何系统都能顺着这个 ID 找上门:
/// 阿黄探望心爱的羊:按存好的 Entity 直取那一行
fn visit_favorite(
dog: Single<(&Name, &Favorite)>,
mut flock: Query<(&Name, &mut Hunger), With<Sheep>>,
) {
let (dog_name, favorite) = *dog;
match flock.get_mut(favorite.0) {
Ok((sheep_name, mut hunger)) => {
hunger.0 -= 1;
println!("{dog_name} 给 {sheep_name} 留了口粮(饥饿降到 {})", hunger.0);
}
Err(_) => println!("{dog_name} 到处找不到心爱的羊……"),
}
}Listing 4-3(节选):get_mut 按 ID 精确取行(Single 参数稍后解释)
get(entity) 和 get_mut(entity) 是 Query 的随机访问接口:不遍历,直接要某一行的数据,返回 Result。拿到 Err 有两种可能——实体已经不存在,或者实体还在但不匹配这条查询(缺列,或被过滤器拦下)。示例里两个分支都写了,哪边会执行,运行输出见分晓。
另一个常用搭配值得记住:可变查询的 iter() 给出的是只读视图。先用 iter() 配合迭代器适配器找出目标(比如生命值最低的那只羊),再用 get_mut 对它精确动手——“先侦察、后开刀”,本章最终示例的狼就这么捕猎。
恰好一个:single() 与 Single
还有一类查询,目标天生只有一个:唯一的玩家、唯一的相机、唯一的牧羊犬。为它写 for 循环很别扭,Bevy 给了两种正经写法。
第一种是方法 single()(可变版 single_mut()):恰好一个匹配实体时返回 Ok,零个或多个都返回 Err,怎么处置由你:
/// single() 把"是否恰好一只"交给你自己处理
fn wolf_census(wolves: Query<&Name, With<Wolf>>) {
match wolves.single() {
Ok(name) => println!("狼口普查:只有 {name} 一只"),
Err(_) => println!("狼口普查:不是一只({} 只),全员戒备!", wolves.iter().count()),
}
}第二种是系统参数 Single<D, F>——把“恰好一个”提升为整个系统的前置条件:
/// 狼恰好一只时,这个系统才运行;零只或多只都直接跳过
fn watch_lone_wolf(wolf: Single<&Name, With<Wolf>>) {
println!("独狼 {} 在围栏外游荡,盯紧它", *wolf);
}Listing 4-3(节选):single() 自己处理失败,Single 让系统整个跳过
Single 在系统运行前校验:匹配的实体恰好一个,解引用直达数据(*wolf 就是 &Name,不用再迭代);零个或多个,这个系统本帧直接被跳过,静默无声——不报错、不打日志。这正是两者的分野:
- 拿不到该当异常处理、函数体还有别的事要干——用
single(),自己接Result; - “没有就歇着,有了再干活”本身就是业务语义——用
Single,签名替你把守。
注意 Query 参数自己从不“失败”:匹配零行的查询完全合法,只是迭代零次。Single 是把“必须恰好一个”这个额外要求显式写进了签名。还有个近亲 Populated<D, F>:要求“至少一个”,不满足同样跳过系统,解引用后当普通 Query 用。
一夜变天
Listing 4-3 把这三样工具放进了一个两幕剧:第一帧岁月静好;入夜后牧场主把卷卷卖了,围栏外又摸来一只狼:
/// 入夜:心爱的羊被牧场主卖掉了,同时又来了一只狼(只发生一次)
fn nightfall(mut commands: Commands, dog: Single<&Favorite>, mut done: Local<bool>) {
if *done {
return;
}
*done = true;
println!("〔夜里:卷卷被卖掉了,又一只狼摸了过来〕");
commands.entity(dog.0).despawn();
commands.spawn((Name::new("黑爪"), Wolf));
}Listing 4-3(节选):用 Commands 制造变故,Local<bool> 保证只发生一次
cargo run -p ch04-systems-queries --example listing-04-03阿黄 给 卷卷 留了口粮(饥饿降到 3)
独狼 灰背 在围栏外游荡,盯紧它
狼口普查:只有 灰背 一只
〔夜里:卷卷被卖掉了,又一只狼摸了过来〕
阿黄 到处找不到心爱的羊……
狼口普查:不是一只(2 只),全员戒备!前四行是第一帧,后两行是第二帧。对照三个系统看第二帧:
get_mut走了Err分支——Favorite里的 ID 还在,但那一行没了。存起来的Entity不保证永远有效,每次使用都要面对Result,这是 ECS 引用的常态;watch_lone_wolf那行消失了——狼变成两只,Single校验失败,系统被静默跳过。好用,但调试时要记得这种“无声缺席”;wolf_census自己消化了Err,照常发声。
取数据的功夫齐了。但目前为止我们圈定实体的手段还只有 With——下一节把过滤器配齐。