Skip to content

开机进度条

一件一件等货,戏没法拍。真实游戏的做法是把一关用到的素材整单装货:列清单、全部开单、看着到货数爬升、全齐了才进关卡——玩家看到的就是那根进度条。这一节用已有的零件把它拼出来:清单是 Vec 里的一把 Handle,进度是“数到货”,切换是第 10 章的 States。

清单、状态与行头

rust
#[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;它照样参与引用计数,清单本身就是持单人,装货期间谁也不会被回收。

rust
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 章):状态一切走,加载画面自动清场。

数到货

rust
/// 数到货:每帧清点清单,进度条跟着走,全齐切状态
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)

核心一行是那个 filteris_loaded_with_dependencies(handle.id()) 对清单逐件发问“连配件全到了吗”,count 出到货数。进度条的几何是第 12 章的算术——金条宽度按比例伸长,同时把中心右移半个增量,让它看起来从左端往右长,而不是从中心向两边胀。

全齐的那帧把 NextState 设成 Rolling,装台系统在 OnEnter(Rolling) 里搭场:

rust
/// 装台:此刻所有道具都已在架,再 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,只要约好路径。

console
cargo run -p ch14-assets --example listing-14-06
text
老顾:《长风渡》整本戏的家当,7 件,单子全开出去了。
老顾:(第 1 帧)到货 1/7。
老顾:(第 2 帧)到货 3/7。
老顾:(第 3 帧)到货 7/7。
老雷:全齐了?——装台,开机!
场务:夜渡幕布挂好,三件道具各就各位。

到货数分三帧爬完:第 1 帧三件小道具里先到一件,第 2 帧凑到三件,第 3 帧四张幕布全部解码完毕。机制全部成立——只是这台机器装得太快,进度条在屏幕上一闪而过。真实项目里几百件资产、几百 MB 的体量,这根条要走上好几秒;本章总装(14.8 节)会给它加一道“最短亮相时间”的闸,这是真实游戏处理“加载太快”的标准手法——是的,加载条的烦恼有两个方向。

还有一笔账记在 14.3 节欠条上:万一清单里有件货 Failedis_loaded_with_dependencies 永远是 false,进度条卡在 6/7 死等——这正是练习 1 要你补的洞。