Skip to content

加载状态与自定义加载器

轮询加载状态

AssetServer::load_state(&handle) 返回资产的当前状态(第 14 章已详细讲过)。对 glTF 这种带依赖的大资产,常用 is_loaded_with_dependencies 判断"连同纹理、网格全部到齐":

rust
use bevy::asset::LoadState;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, check_loaded)
        .run();
}

#[derive(Resource)]
struct Helmet(Handle<WorldAsset>);

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
    ));

    commands.spawn((
        DirectionalLight {
            contact_shadows_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 25.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    let handle = asset_server
        .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"));
    commands.insert_resource(Helmet(handle));
}

fn check_loaded(
    mut commands: Commands,
    helmet: Res<Helmet>,
    asset_server: Res<AssetServer>,
    mut done: Local<bool>,
) {
    if *done {
        return;
    }
    match asset_server.load_state(&helmet.0) {
        LoadState::Loaded => {
            *done = true;
            commands.spawn(WorldAssetRoot(helmet.0.clone()));
            println!("helmet loaded");
        }
        LoadState::Failed(err) => {
            *done = true;
            println!("load failed: {err}");
        }
        _ => {}
    }
}

Listing 23-7:轮询加载状态,到货后再 spawn 场景(examples/listing-23-07.rs)

console
cargo run -p ch23-gltf --example listing-23-07

这个模式在需要延迟 spawn 或显示加载进度时很有用。注意 LoadState 需要从 bevy::asset::LoadState 显式引入。

自定义资产加载器

Bevy 的资产系统是可扩展的。任何格式,只要实现了 AssetLoader trait,就能用 AssetServer::load 加载。下面用一个简单的键值对格式 .cfg 演示:

rust
use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, LoadContext};
use bevy::prelude::*;
use thiserror::Error;

#[derive(Asset, TypePath, Debug)]
struct Config {
    entries: Vec<(String, String)>,
}

#[derive(Debug, Error)]
enum ConfigLoaderError {
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("not utf-8: {0}")]
    NotText(#[from] std::string::FromUtf8Error),
    #[error("bad line {line}: {content:?}")]
    BadLine { line: usize, content: String },
}

#[derive(Default, TypePath)]
struct ConfigLoader;

impl AssetLoader for ConfigLoader {
    type Asset = Config;
    type Settings = ();
    type Error = ConfigLoaderError;

    async fn load(
        &self,
        reader: &mut dyn Reader,
        _settings: &(),
        _load_context: &mut LoadContext<'_>,
    ) -> Result<Config, ConfigLoaderError> {
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes).await?;
        let text = String::from_utf8(bytes)?;
        let mut entries = Vec::new();
        for (i, line) in text.lines().enumerate() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            let Some((key, value)) = line.split_once('=') else {
                return Err(ConfigLoaderError::BadLine {
                    line: i + 1,
                    content: line.to_string(),
                });
            };
            entries.push((key.trim().to_string(), value.trim().to_string()));
        }
        Ok(Config { entries })
    }

    fn extensions(&self) -> &[&str] {
        &["cfg"]
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_asset::<Config>()
        .init_asset_loader::<ConfigLoader>()
        .add_systems(Startup, setup)
        .add_systems(Update, print_config)
        .run();
}

#[derive(Resource)]
struct ConfigHandle(Handle<Config>);

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let handle = asset_server.load("config/app.cfg");
    commands.insert_resource(ConfigHandle(handle));
}

fn print_config(
    handle: Res<ConfigHandle>,
    configs: Res<Assets<Config>>,
    mut printed: Local<bool>,
) {
    if *printed {
        return;
    }
    let Some(config) = configs.get(&handle.0) else {
        return;
    };
    *printed = true;
    for (k, v) in &config.entries {
        println!("{k} = {v}");
    }
}

Listing 23-8:自定义 AssetLoader——教 Bevy 读 .cfg 文本格式(examples/listing-23-08.rs)

console
cargo run -p ch23-gltf --example listing-23-08

要点:

  • Asset trait#[derive(Asset)] 把自定义类型注册为资产。Config 结构体存解析后的键值对
  • AssetLoader trait:三个关联类型——Asset(产出类型)、Settings(加载参数,不用时填 ())、Error(错误类型)
  • load 方法:异步,接收 Reader 读取原始字节,返回解析后的资产。LoadContext 可以用来加载子资产(如纹理)
  • extensions 方法:返回这个加载器认领的文件扩展名。.cfg 文件会自动路由到 ConfigLoader
  • 注册app.init_asset::<Config>().init_asset_loader::<ConfigLoader>()——先给类型上户口,再注册加载器

glTF 加载器本身就是这么实现的——GltfLoader 认领 ["gltf", "glb"] 扩展名,load 方法里调用 gltf crate 解析二进制,产出 GltfSceneMeshStandardMaterial 等一串资产。DefaultPlugins 帮你完成了注册,所以前几节的代码不需要这一步。