打包参数:SystemState、SystemParam 与 QueryData
这一节解决三个“写着费劲”,工具各一件。三件的思路相同:把常用的形状起成名字。
SystemState:在独占系统里开柜台
独占系统什么都能干,可前两节的代码你也看见了——world.query_filtered::<(), With<Notice>>() 这种长串每写一次都费劲,柜台时代一个 Query 参数的事。SystemState 把柜台搬进独占系统:在类型参数里写下你想要的那组系统参数,它替你按单配货。它本身就在独占系统参数的白名单上(11-1 节欠的名单),作为第二参数传 &mut SystemState<...> 即可:
/// 盘点:独占系统里用 SystemState 开出惯用的柜台
fn take_census(
world: &mut World,
counter: &mut SystemState<(Query<&Name, With<Resident>>, Commands)>,
mut frame: Local<u32>,
) {
*frame += 1;
if *frame != 2 {
return;
}
// get_mut 借出参数——用起来和普通系统一模一样
let (residents, mut commands) = counter.get_mut(world).unwrap();
let roll: Vec<&str> = residents.iter().map(Name::as_str).collect();
println!(" 艾达点名:{}。", roll.join("、"));
commands.spawn((Notice, Name::new("年度盘点完毕")));
// SystemState 借出的 Commands 同样是缓冲的——还没落地
let before = world.query_filtered::<(), With<Notice>>().iter(world).count();
counter.apply(world); // 手动结账,命令立刻执行
let after = world.query_filtered::<(), With<Notice>>().iter(world).count();
println!(" 公告牌:apply 之前 {before} 块,之后 {after} 块。");
}Listing 11-8:SystemState——独占系统里借出 Query 和 Commands
get_mut(world) 借出参数包,解构出来的 residents 和 commands 与普通系统里的同名参数用法完全一致。但有一处必须自己上心,运行:
cargo run -p ch11-deep-ecs --example listing-11-08—— 第 1 帧 ——
巡逻队:住户 3 人,公告牌 0 块。
—— 第 2 帧 ——
巡逻队:住户 3 人,公告牌 0 块。
艾达点名:罗兰、老蔫儿、杂货铺老板。
公告牌:apply 之前 0 块,之后 1 块。
—— 第 3 帧 ——
巡逻队:住户 3 人,公告牌 1 块。“之前 0 块,之后 1 块”——SystemState 借出的 Commands 依旧是缓冲的,第 3 章的延迟语义原样保留;区别在于没有调度器替你清算,结账要自己来:counter.apply(world)。忘了 apply,命令不会丢,但会一直窝在 SystemState 里等下一次 apply——排查起来相当折磨人,养成“借了就结”的习惯。
为什么参数要写成 &mut SystemState 而不是每次现 new 一个?和 Query 参数背后缓存 QueryState 同理:SystemState::new 要登记访问、建查询缓存,每帧重建是纯浪费。挂在参数上,引擎就替你把它在两次运行之间保管好——和 Local<T> 的保管机制是同一套。顺带把第 4 章的一句旧话补全:Local<T> 的初始值“准确地说来自 FromWorld”——第 5 章资源那套 FromWorld 协议,对 Local 同样生效,默认实现仍是 Default。
#[derive(SystemParam)]:参数打包
第二个费劲发生在普通系统:同一组参数——名册查询加镇库资源——艾达的每件公务都要原样抄一遍,签名越来越长。打包:
/// 随身柜台:名册 + 镇库,一袋装走
#[derive(SystemParam)]
struct CensusDesk<'w, 's> {
residents: Query<'w, 's, &'static Name, With<Resident>>,
funds: ResMut<'w, TownFunds>,
}
impl CensusDesk<'_, '_> {
/// 点名并收人头税:每户 1 枚
fn collect(&mut self) -> u32 {
let n = self.residents.iter().count() as u32;
self.funds.0 += n;
n
}
}Listing 11-9(其一):derive(SystemParam)——几个参数拼成一个新参数
规则三条:结构体带 <'w, 's> 两个生命周期槽(world 借用和系统状态借用,照抄即可);字段类型就是平时写在签名里的参数类型,Query 字段把两个生命周期填进去、数据位写 &'static(这个 'static 是占位写法,下一小节细说);可以给它 impl 方法,把操作和数据捆在一起。用的时候,它就是一个普通参数:
/// 春盘与秋盘:同一只柜台,原样复用
fn spring_census(mut desk: CensusDesk) {
let n = desk.collect();
println!("春盘:{n} 户造册,镇库现银 {} 枚。", desk.funds.0);
}
fn autumn_census(mut desk: CensusDesk, mut exit: MessageWriter<AppExit>) {
let n = desk.collect();
println!("秋盘:{n} 户造册,镇库现银 {} 枚。", desk.funds.0);
exit.write(AppExit::Success);
}Listing 11-9(其二):春盘秋盘,同一只柜台
cargo run -p ch11-deep-ecs --example listing-11-09春盘:3 户造册,镇库现银 103 枚。
秋盘:3 户造册,镇库现银 106 枚。并行调度毫不受影响——调度器看的是包里每个字段的访问集合,打包只是给人看的语法糖。第 4 章参数表的最后一行“自定义参数”,到此兑现。
#[derive(QueryData)]:给表格的一行起名字
第三个费劲在查询本身:盘点表一行五列,元组写法 (Entity, &Name, &Stock, Option<&Lodger>, Has<Registered>) 解构起来全靠数位置,加一列全身动。同样的药方,打包成结构体:
/// 盘点表的一行(只读):字段名就是列名
#[derive(QueryData)]
struct CensusRow {
entity: Entity,
name: &'static Name,
stock: &'static Stock,
lodger: Option<&'static Lodger>, // 不是每户都有的列
registered: Has<Registered>,
}Listing 11-10(其一):derive(QueryData)——一行一个结构体
现在把 &'static 说清楚:QueryData 结构体里的引用字段统一写 &'static T,这是 derive 宏规定的占位拼法——它只声明“这一列是 T 的引用”,实际借用的生命周期由查询临场决定,绝不是真的 'static。Option 列、Has 列照常嵌进来。使用处按字段名取数:
/// 念名册:按字段名取数,不再数元组位置
fn read_table(table: Query<CensusRow>) {
println!("盘点表:");
for row in &table {
print!(" {} {}:存粮 {} 袋", row.entity, row.name, row.stock.0);
if let Some(lodger) = row.lodger {
print!(",借宿于{}家", lodger.host);
}
println!("{}", if row.registered { "(已盖章)" } else { "(未盖章)" });
}
}Listing 11-10(其二):row.name、row.stock——不再数元组位置
要写的列,给结构体标 mutable,字段写 &'static mut:
/// 可写的一行:标 mutable,&'static mut 才放行
#[derive(QueryData)]
#[query_data(mutable)]
struct TaxRow {
name: &'static Name,
stock: &'static mut Stock,
}
/// 收粮税:每户 1 袋
fn collect_grain_tax(mut rows: Query<TaxRow>, mut exit: MessageWriter<AppExit>) {
for mut row in &mut rows {
row.stock.0 -= 1;
println!("收税:{}缴 1 袋,余 {} 袋", row.name, row.stock.0);
}
exit.write(AppExit::Success);
}Listing 11-10(其三):query_data(mutable)——可写的一行
cargo run -p ch11-deep-ecs --example listing-11-10盘点表:
2v0 杂货铺老板:存粮 40 袋(未盖章)
1v0 老蔫儿:存粮 7 袋(已盖章)
0v0 罗兰:存粮 3 袋,借宿于杂货铺老板家(未盖章)
收税:杂货铺老板缴 1 袋,余 39 袋
收税:老蔫儿缴 1 袋,余 6 袋
收税:罗兰缴 1 袋,余 2 袋迭代拿到的不是 TaxRow 本身,而是宏生成的条目类型(TaxRowItem),可写字段在里面是 Mut<Stock>——所以 row.stock.0 -= 1 照常触发变更检测。标了 mutable 的结构体还附赠一个只读版本(TaxRowReadOnly),给只读借用时自动用上,不必自己写两份。
变更检测——上一段刚冒出来的词。Mut 是怎么知道你写没写的?Changed 过滤器的“自上次以来”到底量到哪根刻度?第 4 章欠的“更多机关”,下一节全部拆开。