锣鼓点:一次性音效与 PlaybackMode
鼓师的活和琴师的正相反。曲子是常驻的,锣鼓点是一次性的:cue 来了响一声,响完这一声就不存在了。按上一节的世界观,“发声 = spawn 实体”,那敲一声锣自然就是 spawn 一个锣实体——鼓师照办,场记不放心,搬个小凳坐在后台数家伙:
/// 每按一次 G,spawn 一个新的发声实体——默认 PlaybackSettings::ONCE
fn strike_gong(
keyboard: Res<ButtonInput<KeyCode>>,
gong: Res<GongSound>,
mut commands: Commands,
) {
if keyboard.just_pressed(KeyCode::KeyG) {
commands.spawn(AudioPlayer::new(gong.0.clone()));
println!("鼓师:哐——");
}
}
/// 场记的后台点数:带 AudioPlayer 的实体有几个?数目变了才报
fn stage_count(players: Query<Entity, With<AudioPlayer>>, mut last: Local<usize>) {
let count = players.iter().count();
if count != *last {
println!("场记:后台躺着 {count} 个音频实体。");
*last = count;
}
}Listing 19-3:每按一次 G spawn 一声锣;场记清点带 AudioPlayer 的实体,数目变了才报(examples/listing-19-03.rs)
锣的提货单在 Startup 里开好、存进 Resource,每声锣克隆一份——第 14 章“同路径同单、克隆不花钱”的标准取单法。跑起来敲三声:
cargo run -p ch19-audio --example listing-19-03鼓师:按 G 敲锣。场记盯着后台数家伙。
鼓师:哐——
场记:后台躺着 1 个音频实体。
鼓师:哐——
场记:后台躺着 2 个音频实体。
鼓师:哐——
场记:后台躺着 3 个音频实体。锣声没问题,三声都响了,错落有致。问题在场记的账上:锣一声 2.2 秒,眼下早就静场了,实体数却停在 3,一个都没少。这就是默认档 PlaybackMode::Once 的本性:播完了,它什么也不做——声音停了,实体和身上那套音频组件原地留场。一场武戏敲两百声锣,散场后后台就躺着两百具空壳。
更糟的是这些空壳连废物利用都不行。你可能想:留着也好,下次敲锣不就不用 spawn 了?PlaybackMode::Once 的文档把话说死了:播完之后这个 AudioPlayer 没法复用——想让它再响,得把音频组件拆下来再装回去。(这是写在源码里的行为:实体上的 AudioSink 一旦被移除、而 AudioPlayer 还在,引擎会视它为一笔新的播放从头开播。靠 remove::<AudioSink>() 实现“重播按钮”是个合法偏方,但对一次性音效,有更省心的正路。)
DESPAWN:自动拆台
正路是换一档 PlaybackMode,让引擎播完顺手把实体拆了。顺便鼓师还要露一手省钱绝活——同一面锣,调转速敲出三个音:
/// 数字键选音高:speed 既是快慢也是音高,0.8 倍速就是低八成的锣
fn strike_gong(
keyboard: Res<ButtonInput<KeyCode>>,
gong: Res<GongSound>,
mut commands: Commands,
) {
for (key, speed, name) in [
(KeyCode::Digit1, 0.7, "低"),
(KeyCode::Digit2, 1.0, "中"),
(KeyCode::Digit3, 1.4, "高"),
] {
if keyboard.just_pressed(key) {
commands.spawn((
AudioPlayer::new(gong.0.clone()),
// 播完连实体一起拆掉,后台不留家伙
PlaybackSettings::DESPAWN.with_speed(speed),
));
println!("鼓师:{name}锣,哐——");
}
}
}Listing 19-4:DESPAWN 播完自动拆台;speed 既是快慢也是音高(examples/listing-19-04.rs)
cargo run -p ch19-audio --example listing-19-04鼓师:1 低锣、2 中锣、3 高锣——同一面锣,转速不同。
鼓师:低锣,哐——
场记:后台躺着 1 个音频实体。
鼓师:中锣,哐——
场记:后台躺着 2 个音频实体。
鼓师:高锣,哐——
场记:后台躺着 3 个音频实体。
场记:后台躺着 2 个音频实体。
场记:后台躺着 1 个音频实体。
场记:后台躺着 0 个音频实体。账面这回有涨有落:连敲三声冲到 3,随后一声声播完、一具具拆掉,归零。引擎在后台盯着每个 sink,声音排空的那一刻 despawn 整个实体——连同它的子实体。“连同子实体”不是废话:实战里常给音效实体挂粒子特效之类的随从,DESPAWN 一锅端走,不留孤儿。
两个数字键的差别也值得侧耳:with_speed(0.7) 的低锣不只慢,还沉——speed 改变的是采样的播放速率,快慢和音高绑在一起变,0.7 倍速就是低七成的音、长十分之十的余韵(rodio 的文档原话:提速会把音高提高同样的倍数)。一份素材听出一族音色,这招在真实项目里是给打击音效去重复感的标准手法。
四档点齐
PlaybackMode 总共四档,至此都有了名分:
| 档位 | 播完之后 | 用途 |
|---|---|---|
Once(默认) | 什么也不做——实体与哑组件留场 | 配合手动管理;当心堆积 |
Loop | 没有“播完”——无缝从头再来 | BGM、环境声、引擎嗡鸣 |
Despawn | despawn 整个实体(连子实体) | 一次性音效的标准答案 |
Remove | 只卸下音频组件,实体留用 | 给“还有正事”的实体配一声音效 |
Remove 补一个场景就清楚了:让角色实体自己“说”一句受击音——声音得挂在角色身上,但播完总不能把角色拆了,于是只卸音频组件、人留下。四档之外,PlaybackSettings 还有几个开播选项是 builder 方法一挂就走的:paused() 先就位不出声(蓄势待发的“预备-放”)、muted() 静音开播、with_start_position(...)/with_duration(...) 截取片段循环。都属于“开播那一刻”的设定。
“开播那一刻”这个限定语,本节已经悄悄说了三遍。PlaybackSettings 的文档里还有一句更扎心的:开播之后再改这个组件,对正在播的声音没有任何效果。那曲子放到一半想拧音量、想暂停呢——spawn 完就管不着了?管得着,但钥匙不在 PlaybackSettings 手里。下一节去会一会真正的掌钥人:AudioSink。