Skip to content

库房的细则

道具库的主流程到此跑通了。这一节处理三件迟早会撞上的细务,每件都从一个具体的麻烦开始:海报糊了、加载画面自己也要图、以及发布前该不该把素材“预加工”。

像素海报糊了:加载设置与 .meta

宣传组想拿青霜剑的 16×16 像素原稿放大做海报。Listing 14-9 把同一张稿放大十六倍,摆了三份:

rust
fn print_posters(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);

    // 左:16×16 像素原稿,默认设置(线性采样)放大十六倍——糊成一团
    commands.spawn((
        Sprite {
            image: asset_server.load("props/sword-16.png"),
            custom_size: Some(Vec2::splat(256.0)),
            ..default()
        },
        Transform::from_xyz(-300.0, 0.0, 0.0),
    ));

    // 中:同一张稿的副本,旁边躺着一份同名 .meta 档案,里面把采样写成 Nearest
    commands.spawn((
        Sprite {
            image: asset_server.load("props/sword-16-meta.png"),
            custom_size: Some(Vec2::splat(256.0)),
            ..default()
        },
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));

    // 右:下单时现场改设置——load_with_settings 只动要动的字段
    commands.spawn((
        Sprite {
            image: asset_server.load_with_settings(
                "props/sword-16-settings.png",
                |settings: &mut ImageLoaderSettings| {
                    settings.sampler = ImageSampler::nearest();
                },
            ),
            custom_size: Some(Vec2::splat(256.0)),
            ..default()
        },
        Transform::from_xyz(300.0, 0.0, 0.0),
    ));

    println!("老顾:同一张十六格的剑稿放大十六倍,三种洗法——");
    println!("老顾:左边默认线性,糊;中间走 .meta 档案,利;右边下单现改设置,一样利。");
}

Listing 14-9:同一张像素稿的三种洗法——默认、.meta 档案、load_with_settings(examples/listing-14-09.rs)

console
cargo run -p ch14-assets --example listing-14-09

三把放大的像素剑并排:左边一把边缘模糊发虚,中间与右边两把棱角分明

Figure 14-5:左:线性采样把 16 个像素糊成渐变;中、右:Nearest 采样保住像素的棱角

左边那把糊了。罪魁是采样器(sampler——GPU 把贴图像素映射到屏幕像素时的取值规则):默认设置是线性插值,相邻像素取平均,照片类素材放大时显得平滑,像素画放大时直接糊成一摊。解法是按件改设置,两条路:

  • 代码路load_with_settings(路径, 闭包)。闭包收到该资产装载器的设置类型——图片是 ImageLoaderSettings(14.5 节我们自己的装载器把 Settings 定成 (),图片装载器则是有选项的范例)——只改要改的字段,这里把 sampler 换成 ImageSampler::nearest()(取最近的像素,不平均,棱角分明);
  • 档案路:在素材文件旁边放一个同名加 .meta 后缀的文件,逐字段写明加载设置。中间那把剑走的就是这条路,sword-16-meta.png.meta 内容如下:
text
(
    meta_format_version: "1.0",
    asset: Load(
        loader: "bevy_image::image_loader::ImageLoader",
        settings: (
            format: FromExtension,
            is_srgb: true,
            sampler: Descriptor (ImageSamplerDescriptor(
                address_mode_u: ClampToEdge,
                address_mode_v: ClampToEdge,
                address_mode_w: ClampToEdge,
                mag_filter: Nearest,
                min_filter: Nearest,
                mipmap_filter: Nearest,
                lod_min_clamp: 0,
                lod_max_clamp: 32.0,
                compare: None,
                anisotropy_clamp: 1,
                border_color: None,
                label: None,
            )),
            asset_usage: RenderAssetUsages("RENDER_WORLD | MAIN_WORLD"),
        ),
    ),
)

.meta 档案:跟着素材文件走的加载设置,mag/min/mipmap 三处过滤全改成 Nearest(assets/props/sword-16-meta.png.meta)

档案路的好处是不动代码、跟文件走;代价是字段必须写全(它是整份设置的存档,不是补丁)。代码路反过来:是补丁、可以只写一处,但散在代码里。团队素材管线通常偏向 .meta,一次性的特例用 load_with_settings 顺手。

一个埋得很深的坑,提前替你踩了:同一路径只认第一次的设置。资产按路径去重(14.2 节),如果某处先用默认设置 loadsword-16.png,后面再 load_with_settings 同一路径,设置会被静默忽略——单子还是原来那张。Listing 14-9 给三把剑准备三份文件副本,原因就在这。

整目录批量加载,库房也接单:load_folder("props") 返回一张 Handle<LoadedFolder>,文件夹里的货全算它的依赖——等 LoadedWithDependencies 一声广播,整夹到齐。

加载画面自己的图:嵌入资产

14.4 节的进度条是纯色块,想配上场记板图标就遇到先有鸡还是先有蛋:加载画面是给“素材还没到”的时间段看的,它自己的素材谁来加载?

答案是不加载——把文件直接编进可执行文件embedded_asset! 宏在编译期用 include_bytes! 把字节焊进二进制,运行期注册成一件内存里的资产:

rust
struct EmbeddedClapperPlugin;

impl Plugin for EmbeddedClapperPlugin {
    fn build(&self, app: &mut App) {
        // 把 examples/embedded/clapper.png 的字节编进二进制。
        // 第二个参数是路径前缀:本文件不在 src/ 下,得告诉宏从 examples 起算
        embedded_asset!(app, "examples", "embedded/clapper.png");
    }
}

Listing 14-10(节选一):embedded_asset! 把场记板焊进二进制(examples/listing-14-10.rs)

rust
fn raise_clapper(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);

    // 嵌入资产住在 embedded:// 来源下;“crate 名”是本例的可执行名 listing_14_10
    let clapper: Handle<Image> =
        asset_server.load("embedded://listing_14_10/embedded/clapper.png");
    commands.spawn(Sprite::from_image(clapper));

    println!("老顾:场记板不走库房——缝在戏服里,走到哪带到哪。");
    println!("老顾:就算把 assets/ 整个搬空,这块板子照样举得起来。");
}

Listing 14-10(节选二):从 embedded:// 来源取货——没有磁盘 IO,但流程照旧(examples/listing-14-10.rs)

console
cargo run -p ch14-assets --example listing-14-10
text
老顾:场记板不走库房——缝在戏服里,走到哪带到哪。
老顾:就算把 assets/ 整个搬空,这块板子照样举得起来。

那条路径值得解剖:embedded://listing_14_10/embedded/clapper.pngembedded:// 前缀指明来源(asset source——资产从哪类仓库来;默认来源是 assets/ 文件夹,embedded 是第二个内置来源);随后是 crate 名(cargo 把每个 example 当独立 crate 编译,所以这里是例子自己的名字);再后面是文件相对位置。宏默认假设文件住在 src/ 下,我们的例子住在 examples/ 下,所以多传了一个参数说明前缀——总装的 main.rs 就不用了,它还能用更顺手的 load_embedded_asset! 宏直接取单,14.8 节见。

嵌入资产的取舍很直白:可执行文件变大、改素材要重编译,换来零文件依赖——引擎自己的内置资源(默认字体、内置 shader)全是这么发货的。给你的游戏做加载画面、应用图标这类“开机就要、永不缺席”的素材,这是正解。

发布前的预加工:asset processing 概述

最后一件细务关乎发布。开发期我们直接加载“原稿”:PNG 要在玩家机器上解码,.meta 要逐个检查,谁也没优化过谁。Bevy 还内置另一种工作模式——asset processing(资产预加工):开发机上先跑一道流水线,把原始素材转换、压缩、烘焙成对运行时最友好的形态(比如把 PNG 转成 GPU 能直接吞的压缩纹理格式),写进 imported_assets/ 目录;发布出去的游戏改从加工成品里加载,AssetPluginmode 从默认的 AssetMode::Unprocessed 换成 AssetMode::Processed 即是切换开关。.meta 档案在那套流程里身兼二职:既写加载设置,也写加工规则。

对中小项目,默认的 Unprocessed 模式足够走到发布;预加工在素材量大、平台多的项目里才回本。本书第 38 章谈发布时再回到这个话题。同属“知道就好”的还有两件:来源不止本地文件——开 http/https feature 可以直接 load("https://…") 从网络取货;AssetReader trait 还允许你自造来源(从 zip 包、从自家服务器读),官方仓库的 custom_asset_reader 示例是现成的样板。