开机进度条
一件一件等货,戏没法拍。真实游戏的做法是把一关用到的素材整单装货:列清单、全部开单、看着到货数爬升、全齐了才进关卡——玩家看到的就是那根进度条。这一节用已有的零件把它拼出来:清单是 Vec 里的一把 Handle,进度是“数到货”,切换是第 10 章的 States。
清单、状态与行头
#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)]
enum SetupPhase {
/// 装货中:片场只有一根进度条
#[default]
Loading,
/// 开机:装台开拍
Rolling,
}
/// 整本戏的提货单。UntypedHandle 不记资产类型——以后混进剧本、音频也是这张单
#[derive(Resource)]
struct PropManifest {
handles: Vec<UntypedHandle>,
}
#[derive(Component)]
struct BarFill;Listing 14-6(节选一):两段式开机——Loading 装货,Rolling 开拍(examples/listing-14-06.rs)
清单里装的是 UntypedHandle——抹掉了类型的提货单。普通的 Handle<Image> 只能提图片,而一份真实的资产清单什么都有:图片、音频、剧本。.untyped() 把各色单子统一成一种,塞进同一个 Vec;它照样参与引用计数,清单本身就是持单人,装货期间谁也不会被回收。
const PROP_LIST: [&str; 7] = [
"backdrops/night-crossing.png",
"backdrops/bamboo-sea.png",
"backdrops/old-road.png",
"backdrops/ferry-dock.png",
"props/qingshuang-sword.png",
"props/lantern.png",
"props/changfeng-banner.png",
];
fn start_loading(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
// 整张单子一次开完,handle 全部攥在手里
let handles: Vec<UntypedHandle> = PROP_LIST
.iter()
.map(|path| asset_server.load::<Image>(*path).untyped())
.collect();
println!("老顾:《长风渡》整本戏的家当,{} 件,单子全开出去了。", handles.len());
commands.insert_resource(PropManifest { handles });
// 进度条:深色底槽 + 金色填充,整套行头只活在 Loading 状态里
commands.spawn((
Sprite::from_color(Color::srgb(0.16, 0.16, 0.19), Vec2::new(560.0, 26.0)),
DespawnOnExit(SetupPhase::Loading),
));
commands.spawn((
BarFill,
Sprite::from_color(Color::srgb(0.93, 0.74, 0.29), Vec2::new(0.0, 16.0)),
Transform::from_xyz(-280.0, 0.0, 1.0),
DespawnOnExit(SetupPhase::Loading),
));
}Listing 14-6(节选二):七件家当一次开单;进度条只活在 Loading 状态(examples/listing-14-06.rs)
进度条就是两个色块 Sprite:深色底槽加金色填充——第 2 章的老手艺。两件行头都挂着 DespawnOnExit(SetupPhase::Loading)(第 10 章):状态一切走,加载画面自动清场。
数到货
/// 数到货:每帧清点清单,进度条跟着走,全齐切状态
fn track_progress(
manifest: Res<PropManifest>,
asset_server: Res<AssetServer>,
fill: Single<(&mut Sprite, &mut Transform), With<BarFill>>,
mut next: ResMut<NextState<SetupPhase>>,
mut last_done: Local<usize>,
mut frames: Local<u32>,
) {
*frames += 1;
let total = manifest.handles.len();
let done = manifest
.handles
.iter()
.filter(|handle| asset_server.is_loaded_with_dependencies(handle.id()))
.count();
// 金条从左往右长:宽度与中心点都按到货比例算
let frac = done as f32 / total as f32;
let (mut sprite, mut transform) = fill.into_inner();
sprite.custom_size = Some(Vec2::new(560.0 * frac, 16.0));
transform.translation.x = -280.0 + 280.0 * frac;
if done != *last_done {
println!("老顾:(第 {} 帧)到货 {done}/{total}。", *frames);
*last_done = done;
}
if done == total {
println!("老雷:全齐了?——装台,开机!");
next.set(SetupPhase::Rolling);
}
}Listing 14-6(节选三):清点、推进度条、全齐切状态(examples/listing-14-06.rs)
核心一行是那个 filter:is_loaded_with_dependencies(handle.id()) 对清单逐件发问“连配件全到了吗”,count 出到货数。进度条的几何是第 12 章的算术——金条宽度按比例伸长,同时把中心右移半个增量,让它看起来从左端往右长,而不是从中心向两边胀。
全齐的那帧把 NextState 设成 Rolling,装台系统在 OnEnter(Rolling) 里搭场:
/// 装台:此刻所有道具都已在架,再 load 同一路径只是取回同一张提货单
fn build_the_set(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
Sprite {
image: asset_server.load("backdrops/night-crossing.png"),
custom_size: Some(Vec2::new(1280.0, 720.0)),
..default()
},
Transform::from_xyz(0.0, 0.0, -10.0),
));
for (path, x) in [
("props/lantern.png", -420.0),
("props/qingshuang-sword.png", 0.0),
("props/changfeng-banner.png", 420.0),
] {
commands.spawn((
Sprite::from_image(asset_server.load(path)),
Transform::from_xyz(x, -160.0, 0.0),
));
}
println!("场务:夜渡幕布挂好,三件道具各就各位。");
}Listing 14-6(节选四):装台——此刻 load 同一路径,只是取回已在架的货(examples/listing-14-06.rs)
注意装台系统取资产的姿势:直接再 load 一遍同样的路径。14.2 节的规矩在这里兑现——同路径同单,这几个 load 一两微秒就返回现成的 Handle,一个字节的磁盘都不碰。清单负责保活,用货处随取随用,两边不用传 Handle,只要约好路径。
cargo run -p ch14-assets --example listing-14-06老顾:《长风渡》整本戏的家当,7 件,单子全开出去了。
老顾:(第 1 帧)到货 1/7。
老顾:(第 2 帧)到货 3/7。
老顾:(第 3 帧)到货 7/7。
老雷:全齐了?——装台,开机!
场务:夜渡幕布挂好,三件道具各就各位。到货数分三帧爬完:第 1 帧三件小道具里先到一件,第 2 帧凑到三件,第 3 帧四张幕布全部解码完毕。机制全部成立——只是这台机器装得太快,进度条在屏幕上一闪而过。真实项目里几百件资产、几百 MB 的体量,这根条要走上好几秒;本章总装(14.8 节)会给它加一道“最短亮相时间”的闸,这是真实游戏处理“加载太快”的标准手法——是的,加载条的烦恼有两个方向。
还有一笔账记在 14.3 节欠条上:万一清单里有件货 Failed,is_loaded_with_dependencies 永远是 false,进度条卡在 6/7 死等——这正是练习 1 要你补的洞。