提词器:会长的字
字幕要是一次全亮出来,戏就没了悬念。场记提议上提词器:词一个字一个字地蹦上字幕框,像有人在后台现敲。
实现思路在这一章学完的人看来已经平平无奇:Text2d 是个普通组件,里面是个普通 String——改它就是了。配一个资源攥着整句词和节拍:
/// 提词器:整句台词攥在手里,按节拍一个字一个字递出去
#[derive(Resource)]
struct Teleprompter {
script: Vec<char>,
handed_out: usize,
beat: Timer,
}Listing 16-8(节选一):提词器资源——词、进度、节拍各记各的(examples/listing-16-08.rs)
/// 节拍一到就往 Text2d 里添一个字。改组件本身,重排版引擎自己来
fn type_out(
time: Res<Time>,
mut prompter: ResMut<Teleprompter>,
mut line: Single<&mut Text2d, With<SubtitleLine>>,
) {
if !prompter.beat.tick(time.delta()).just_finished() {
return;
}
if let Some(&next) = prompter.script.get(prompter.handed_out) {
line.push(next);
prompter.handed_out += 1;
if prompter.handed_out == prompter.script.len() {
println!("场记:整句递完,{} 个字。", prompter.handed_out);
}
}
}Listing 16-8(节选二):节拍一到,push 一个字——就这么多(examples/listing-16-08.rs)
两个细节:
script存的是Vec<char>而不是String——中文一个字占三个字节,按字节切会切出半个字(Rust 会 panic),按char切才是按“字”切。chars().collect()一步到位。Text2d实现了DerefMut<Target = String>,所以line.push(next)直接落在内部字符串上。Single<&mut Text2d>是第 4 章的老朋友。
cargo run -p ch16-text --example listing-16-08场记:提词就位。一拍一个字。
场记:整句递完,19 个字。
Figure 16-9:提词器开工——注意词长到框边时自动折行,排版始终是新的
值得多看一眼的是没写的代码:没有任何“请重新排版”的调用。第 4 章的变更检测在底层接了线——bevy_text 注册了一个侦察系统,盯着 Text2d、TextFont、TextLayout、LineHeight 和文本实体的子女名单,谁变了就把这块文本标记为“需要重排”,同一帧稍后排版系统照单重算。字符串、字号、颜色、行高——改哪个都是这个待遇,动画文字(比如数字滚动的计分牌)写起来毫无仪式感。
代价同样要心里有数:每改一次就是一次完整的重排 + 可能的新字形光栅化。提词器一秒七个字无关痛痒;但要是把一面墙的日志塞进一个 Text2d 然后每帧追加,重排的就是整面墙。大段频繁变动的文字,留给第 28 章的 UI 文本(同样的变更检测,但有更合适的布局容器)和第 33 章的诊断工具。
窗口出现≠开拍:你可能注意到示例窗口刚出现的一两秒里提词器纹丝不动,之后才开始打字。这不是 Timer 失灵——首帧之前渲染管线还在编译着色器,Bevy 的
Time从第一次更新才开始走表。所有按Time计时的逻辑都以“开拍”为零点,和窗口何时出现无关。
提词器管的是“字的多少”。下一节拆“字的妆容”——同一句词里,角色名、台词、舞台提示各穿各的行头。