mcpField
B
bevy-custom-assets

bevy-custom-assets

github.com/chrisgliddonDevelopment2683 installsUpdated 9w ago
Repository

About bevy-custom-assets

A skill for Bevy 0.19 developers implementing custom asset loaders. It covers async file access, dependent assets, and API changes that affect loader code.

Key features

  1. 01Implement custom asset loaders
  2. 02Read files asynchronously or randomly
  3. 03Load dependencies through LoadContext
  4. 04Adapt to Bevy 0.19 API changes

Use cases

  1. 01Load custom binary or text formats
  2. 02Build level loaders with referenced assets
  3. 03Migrate older nested loading code

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/chrisgliddon/bevy-skills --skill bevy-custom-assets

After installing, type/bevy-custom-assetsin Claude Code to confirm it loaded.

Details

From the project README

Bevy 0.19 — Custom asset loaders

When to use this skill

  • Loading a custom binary or text format (proprietary game data, sidecar configs, etc.).
  • Producing an asset that depends on other assets (e.g. a level loader that pulls referenced textures).
  • Migrating LoadContext::loader() or an old NestedLoader to load_builder() / NestedLoadBuilder.
  • Needing async access to the underlying file (reader.read_to_end, reader.seekable()).
  • Compiler error: "MyLoader: TypePath is not implemented" — 0.18 made TypePath mandatory on loaders.

Canonical pattern

use bevy::asset::io::Reader;
use bevy::asset::{Asset, AssetApp, AssetLoader, LoadContext};
use bevy::prelude::*;
use bevy::reflect::TypePath;
use serde::Deserialize;
use thiserror::Error;

#[derive(Asset, TypePath, Debug, Deserialize)]
pub struct LevelDef {
    pub name: String,
    pub gravity: f32,
    pub thumbnail: String, // path to a referenced texture asset
}

#[derive(TypePath)] // 0.18: required on the loader itself.
pub struct LevelLoader;

#[derive(Debug, Error)]
pub enum LevelLoaderError {
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
    #[error("parse: {0}")]
    Parse(#[from] ron::error::SpannedError),
}

impl AssetLoader for LevelLoader {
    type Asset = LevelDef;
    type Settings = ();
    type Error = LevelLoaderError;

    async fn load(
        &self,
        reader: &mut dyn Reader,
        _settings: &Self::Settings,
        load_context: &mut LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error> {
        // Pull the whole file. For very large files prefer `seekable()`.
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes).await?;
        let level: LevelDef = ron::de::from_bytes(&bytes)?;

        // Pull in a referenced asset so it loads alongside this one.
        // The resulting handle ends up tracked as a dependency.
        let _: Handle<Image> = load_context.load_builder().load(level.thumbnail.clone());

        Ok(level)
    }

    fn extensions(&self) -> &[&str] {
        &["level.ron"]
    }
}

pub struct LevelLoaderPlugin;
impl Plugin for LevelLoaderPlugin {
    fn build(&self, app: &mut App) {
        app.init_asset::<LevelDef>()
            .register_asset_loader(LevelLoader);
    }
}

Asking the reader for random access

// Bevy 0.19 requires Reader implementations to expose `seekable()`.
# use bevy::asset::io::Reader;
# async fn _example(reader: &mut dyn Reader) -> std::io::Result<()> {
match reader.seekable() {
    Ok(seekable) => {
        // SeekableReader supports `seek(SeekFrom::Start(n))`, `seek(SeekFrom::End(-n))`, etc.
        let _ = seekable;
    }
    Err(_) => {
        // Source isn't seekable (e.g. HTTP backend). Fall back to streaming.
    }
}
# Ok(())
# }

Bevy 0.19 gotchas

  • #[derive(TypePath)] is required on the loader struct (not just the asset). 0.18 enforces this so loaders can be reflected. Without it: "MyLoader: TypePath is not implemented".
  • LoadContext::path() returns AssetPath, not &Path. To get the platform path, use .path() on it: ctx.path().path().
  • LoadContext::asset_bytes() is gone. Use let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; inside the load async fn, or reader.seekable() for random access.
  • Dependencies must be loaded through LoadContext. asset_server.load(...) from inside a loader does not register the result as a dependency of the asset being built — use load_context.load_builder().load(...) (or the load(...) convenience method) so AssetEvent::LoadedWithDependencies fires correctly.
  • LoadContext::loader() was replaced in 0.19. Use load_context.load_builder(). Immediate loads became load_value, dynamic loads became load_erased, and unknown-type loads became load_untyped builder operations.
  • Custom Reader implementations must define seekable() in 0.19. Return Ok(self) only when the type implements AsyncSeek; otherwise return Err(ReaderNotSeekableError).
  • AssetLoader is async but you can't tokio::spawn inside it — the executor is Bevy's, not Tokio's. Use bevy::tasks::futures_lite or bevy::tasks::AsyncComputeTaskPool for compute-heavy work.
  • #[derive(Asset)] is required on the asset type and combined with TypePath. The asset's Settings type must be Default + Serialize + DeserializeOwned + Send + Sync + 'static to participate in .meta files.
  • Extensions are matched on the full suffix. "level.ron" matches foo.level.ron but not foo.ron — useful for disambiguating from generic RON.

See also

  • bevy-assets — using a Handle<MyAsset> once it's loaded.
  • bevy-voxel-data — RON-driven asset patterns.
  • bevy-migration-0-17-to-0-18 — LoadContext::asset_bytes removal.
  • bevy-migration-0-18-to-0-19 — nested load builders and reader changes.

Comments

Markdown supported