Skip to content

run_if:条件运行

第 5 章末尾留过一个预告:记分牌用 is_changed() 偷懒,固然省了刷新,但系统本身每帧照常启动、照常进函数——还能更省,让它在分数没变的帧根本不运行

这就是 run condition(运行条件):挂在系统上的一个判断,每次系统即将运行前评估一次,返回 false 就整帧跳过。打靶场最后一次营业,这回每个旁观者都各按条件出场:

rust
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 不留变更记录)。运行:

console
cargo run -p ch06-schedules --example listing-06-07
text
第 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) 也是一个普通的运行条件——状态机的“暂停时游戏逻辑全体停摆”,底层就是本节的机制。

顺序、分组、条件三件工具齐了。最后回到悬了三章的问题:命令到底什么时候落地。