run_if:条件运行
第 5 章末尾留过一个预告:记分牌用 is_changed() 偷懒,固然省了刷新,但系统本身每帧照常启动、照常进函数——还能更省,让它在分数没变的帧根本不运行。
这就是 run condition(运行条件):挂在系统上的一个判断,每次系统即将运行前评估一次,返回 false 就整帧跳过。打靶场最后一次营业,这回每个旁观者都各按条件出场:
fn main() {
let mut app = App::new();
app.init_resource::<Score>().add_systems(
Update,
(
shoot,
// 兑现第 5 章的预告:分数没变的帧,记分牌根本不运行
scoreboard.run_if(resource_changed::<Score>),
// not() 取反:没动静的帧,轮到观众起哄
heckle.run_if(not(resource_changed::<Score>)),
// 组合条件:.and() 要求两边都成立——刚变过 且 满 30 分,才颁奖
ceremony.run_if(resource_changed::<Score>.and(|score: Res<Score>| score.0 >= 30)),
)
.chain(),
);
for _ in 1..=4 {
app.update();
}
}Listing 6-7:run_if——条件不满足,系统整帧不跑
射手还是第 5 章那套剧本(四枪:命中、脱靶、命中、脱靶,脱靶时 set_if_neq 不留变更记录)。运行:
cargo run -p ch06-schedules --example listing-06-07第 1 枪:命中
记分牌 → 10 分
第 2 枪:脱靶
观众:嘘——
第 3 枪:命中
记分牌 → 30 分
颁奖台:30 分达标,金靶奖章!
第 4 枪:脱靶
观众:嘘——三个条件逐一对账:
resource_changed::<Score>——第 5 章的债就此还清。对比一下:上一章是系统自己跑起来再用if score.is_changed()把关;现在把关前移到调度层,脱靶的帧记分牌连函数都不进。判定口径与第 5 章完全一致(写访问即变更、首帧一切皆新),所以第 2、4 枪set_if_neq不记账,记分牌沉默。not(...)——把任意条件取反。观众恰好在记分牌沉默的帧起哄。.and(...)——组合两个条件,两边都成立才放行。颁奖台要求“分数刚变过 且 满 30 分”:第 3 枪两条全中,颁奖;第 4 枪分数仍是 30,但没变过,不会反复颁奖。还有对称的.or(),以及一个等价写法——同一个系统挂多个.run_if(),效果也是全部为真才运行。
.and() 右边那个参数暴露了运行条件的本质:条件就是一种系统——参数必须全部只读、返回 bool 的系统。所以闭包 |score: Res<Score>| score.0 >= 30 能直接当条件用,你需要的任何自定义判断都这么写。resource_changed 这些现成条件也不神秘,它们就是标准库里备好的小函数,全家在 common_conditions 模块(已含在 prelude 里),常用的有:
| 条件 | 为真的时机 |
|---|---|
resource_exists::<T> | 资源在场(第 5 章双倍卡的调度层写法) |
resource_changed::<T> | 资源自上次检查以来被写过 |
resource_added::<T> / resource_removed::<T> | 资源刚插入 / 刚移除 |
resource_equals(value) | 资源等于给定值(要求 PartialEq) |
any_with_component::<T> | 至少一个实体带有组件 T |
run_once | 只放行第一次,此后永远拦下 |
on_message::<M> | 有新消息到达(第 7 章) |
几条细则,决定了条件的使用边界:
- 评估时机:系统即将运行前、每调度一次。挂在
.chain()里的系统,条件在前序系统跑完后才评估——所以 Listing 6-7 里记分牌的条件能看见本帧射手刚写下的分数。 - 跳过不丢账:被
run_if拦下的帧,系统的变更检测基准不前进。下次真正运行时,Changed/is_changed仍以“上次实际运行”为基准,跳过期间的变更一笔不漏。 - 跳过是常态,不是错误:和第 4 章
Single的静默跳过、第 5 章If<Res<T>>的让路是同一族行为。区别在于那两位是参数自带的保险丝,run_if是你显式声明的开关。
条件也能挂在集合上:configure_sets(Update, MintStage::Settle.run_if(...)),整道工序一起受控。给元组挂条件同理——(a, b).run_if(cond) 会把两个系统编进一个匿名集合,条件只评估一次、整组同生共死;如果你要的是“每个系统各自评估一次”,用 distributive_run_if,它等价于逐个挂 run_if。两者的差别只在条件本身有副作用或评估之间数据可能变化时显现,拿不准就用前者。
顺带认识一下家族里最常用的开关:第 10 章的
in_state(GameState::Playing)也是一个普通的运行条件——状态机的“暂停时游戏逻辑全体停摆”,底层就是本节的机制。
顺序、分组、条件三件工具齐了。最后回到悬了三章的问题:命令到底什么时候落地。