Skip to content

消息工具箱

三辆车并排冲撞,每一帧三声巨响。实习 DJ 拿着 Listing 7-1 的剧本逐条放音效——砰、砰、砰,吵翻天。老板捂着耳朵提要求:音效这种事,“有没有”比“有几条”重要,一帧只准响一声。

这一节就从这个问题出发,把读端剩下的工具一次清点完。先看三位听众的不同做派:

rust
/// 实习 DJ:逐条响应——一帧三撞就放三声,吵翻天
fn rookie_dj(mut hits: MessageReader<RailHit>) {
    for hit in hits.read() {
        println!("实习 DJ:给{}来一声砰!", hit.driver);
    }
}

/// 老 DJ:有就响一声,几条不管——is_empty + clear 模式
fn veteran_dj(mut hits: MessageReader<RailHit>) {
    if !hits.is_empty() {
        hits.clear();
        println!("老 DJ:砰!一声就够。");
    }
}

/// 灯光师:函数体连消息都不碰,run_if(on_message) 替他把关
fn light_show() {
    println!("灯光师:全场闪一下");
}

接线照旧,唯一的新面孔挂在灯光师身上:

rust
fn main() {
    let mut app = App::new();
    app.add_message::<RailHit>()
        .add_systems(Startup, spawn_cars)
        .add_systems(
            Update,
            (
                drive,
                rookie_dj,
                veteran_dj,
                // 没有新碰撞的帧,灯光师整帧不跑
                light_show.run_if(on_message::<RailHit>),
            )
                .chain(),
        );

    for frame in 1..=2 {
        println!("—— 第 {frame} 帧 ——");
        app.update();
    }
}

Listing 7-6:实习 DJ 与老 DJ——“有就响一声”的两种写法

console
cargo run -p ch07-messages --example listing-07-06
text
—— 第 1 帧 ——
实习 DJ:给阿莱来一声砰!
实习 DJ:给小柔来一声砰!
实习 DJ:给老高来一声砰!
老 DJ:砰!一声就够。
灯光师:全场闪一下
—— 第 2 帧 ——
实习 DJ:给阿莱来一声砰!
实习 DJ:给小柔来一声砰!
实习 DJ:给老高来一声砰!
老 DJ:砰!一声就够。
灯光师:全场闪一下

三个人听的是同一条通道,各自的收成对账如下:

  • 实习 DJread() 逐条遍历——这是 Listing 7-1 的正确写法用错了场合,三撞三响。
  • 老 DJis_empty()不消费地探一眼有没有新货,有就 clear() 把游标一步推到底、响一声完事。这对组合就是“有就办一次,有几条不管”的标准写法。len() 是同族工具,不消费地数个数。
  • 灯光师:把同一个判断前移到调度层——运行条件 on_message::<M> 在有新消息时放行,否则整帧不跑。第 6 章条件清单里欠的那一项,今天补上。函数体因此干干净净,连 MessageReader 参数都不用声明。

关键细节藏在执行顺序里:老 DJ 的 clear() 排在灯光师之前,灯光师却照样每帧点亮——因为 clear() 清的是老 DJ 自己的游标,不是缓冲。消息本体还在 Messages<M> 里,实习 DJ、灯光师的条件、以及任何别的读者照常读到。on_message 同理:条件内部自带一个游标,每次评估顺手消费新消息,但那是条件私有的,不影响任何真正的读者。

写端顺带补全两个变体:消息类型实现了 Default 时(空结构体最常见),write_default() 省去手写构造;一次写一批用 write_batch(iter),比逐条 write 高效。

途中改写:MessageMutator

维修工嫌右护栏挨撞太狠,给它装了一条缓冲垫。这道工序既不是写也不是读——它要在消息送达下游之前改掉里面的数:

rust
/// 维修工给右护栏装了缓冲条:途经的碰撞消息被吸掉 2 点力道
fn cushion(mut hits: MessageMutator<RailHit>) {
    for hit in hits.read() {
        if let Rail::Right = hit.rail {
            hit.force -= 2;
            println!("缓冲条:噗——卸掉 2 点力道");
        }
    }
}

MessageMutator<M>MessageReader 一样带游标逐条遍历,但给出的是 &mut M。流水线照旧用 chain 排定,写 → 改 → 读:

rust
fn main() {
    let mut app = App::new();
    app.add_message::<RailHit>()
        .add_systems(Startup, spawn_car)
        // 写 → 改 → 读,三段流水线
        .add_systems(Update, (drive, cushion, play_sound).chain());

    for frame in 1..=4 {
        println!("—— 第 {frame} 帧 ——");
        app.update();
    }
}

Listing 7-7:MessageMutator——缓冲条在途中卸力

console
cargo run -p ch07-messages --example listing-07-07
text
—— 第 1 帧 ——
缓冲条:噗——卸掉 2 点力道
DJ:砰!(力道 2)
—— 第 2 帧 ——
DJ:砰!(力道 4)
—— 第 3 帧 ——
缓冲条:噗——卸掉 2 点力道
DJ:砰!(力道 2)
—— 第 4 帧 ——
DJ:砰!(力道 4)

撞右护栏(第 1、3 帧)的消息写入时力道是 4,DJ 拿到手已被削成 2;左护栏(第 2、4 帧)没装垫子,原值送达。车手照旧不知道这道工序存在——加工序和加读者一样,是纯增量改动

一张表与一条红线

三种参数的并发性质各不相同,根源都是第 4 章的借用规则——它们不过是 Messages<M> 这个资源的三种访问姿势:

参数Messages<M> 的访问并发性质
MessageReader<M>Res + 私有游标读者之间可并行
MessageWriter<M>ResMut与同型一切读写互斥
MessageMutator<M>ResMut + 私有游标与同型一切读写互斥

这张表划出一条红线:同一个系统里同时声明同型的 MessageWriter<M>MessageReader<M>,等于 ResMutRes。亲眼看一次:

rust
//! Listing 7-8:同型消息一写一读挤进同一个系统——首帧 panic

use bevy::prelude::*;

#[derive(Message)]
struct RailHit;

fn main() {
    let mut app = App::new();
    app.add_message::<RailHit>();
    // 想在一个系统里既写又读同一种消息——借用冲突
    app.add_systems(Update, impossible);
    app.update();
}

/// MessageWriter 内部是 ResMut,MessageReader 内部是 Res——同一资源一写一读
fn impossible(_writer: MessageWriter<RailHit>, _reader: MessageReader<RailHit>) {}

Listing 7-8:同型消息一写一读挤进同一个系统——首帧 panic

console
cargo run -p ch07-messages --example listing-07-08
text
error[B0002]: Res<bevy_ecs::message::messages::Messages<listing_07_08::RailHit>>
in system listing_07_08::impossible conflicts with a previous
ResMut<bevy_ecs::message::messages::Messages<listing_07_08::RailHit>> access.
Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002

剧目编号 B0002——和第 5 章资源冲突同款,因为消息缓冲就是资源。报错原文还替本章第一节的说法验明正身:冲突双方写得明明白白,ResMut<Messages<RailHit>>(writer)撞上了 Res<Messages<RailHit>>(reader)。修法也是第 4 章的老朋友 ParamSet,把两者错开取用;至于不同类型之间读写混搭(读 RailHitCombo),那是消息流水线的常态,毫无问题。

别的写入口

最后两个写入口,都来自“消息缓冲只是个资源”这一事实:

  • World 的直接访问能写消息:world.write_message(...)(World API 在第 11 章展开)。
  • Commands 也能写:commands.write_message(...)。注意它走的是第 3 章的命令队列——消息不会当场入仓,要等命令在同步点应用时才真正写入,第 6 章的三条规则全部适用。它的用武之地是类型擦除的场合(运行期才决定写什么类型);常规代码一律用 MessageWriter,更快也更直白。

大批量消息还有一个 par_read(),把读取分摊到多个线程,与第 4 章的并行查询同族,第 34 章谈并行时再见。

工具齐了。回到碰碰车场,把全章拼成一个会自己打烊的程序。