bevy-custom-assets
Skill29 starsChanged 11 days agoMITView on GitHub
Use when implementing `AssetLoader` for a custom format, deriving `TypePath` on the loader, loading dependencies through `LoadContext::load` or `LoadContext::load_builder().with_settings(..).load(..)`, marking handle fields `#[dependency]`, reading with `Reader::read_to_end`, seeking via `Reader::seekable` / `AsyncSeekExt`, or implementing the required `Reader::seekable` on your own reader in Bevy 0.20.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- bevy-a11ySkill
- bevy-animationSkill
- bevy-assetsSkill
- bevy-audioSkill
- bevy-camerasSkill
- bevy-captureSkill
- bevy-cargo-featuresSkill
- bevy-core-conceptsSkill
- bevy-diagnostics-profilingSkill
- bevy-ecs-componentsSkill
- bevy-ecs-queriesSkill
- bevy-ecs-systemsSkill
- bevy-fluentSkill
- bevy-input-actionsSkill
- bevy-migration-0-17-to-0-18Skill
- bevy-migration-0-18-to-0-19Skill
- bevy-migration-0-19-to-0-20Skill
- bevy-pbr-materialsSkill
- bevy-physicsSkill
- bevy-portingSkill
- bevy-renderingSkill
- bevy-save-loadSkill
- bevySkill
- bevy-testingSkill
- bevy-uiSkill
- bevy-vfxSkill
- bevy-voxel-dataSkill
- bevy-voxel-pipelineSkill
- bevy-voxel-runtimeSkill
- bevy-wasm-webgpuSkill
- similarity-rsSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 177
LoadContext::asset_bytes()is gone: Use reader.read_to_end(&mut bytes).await?
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Bevy 0.20 — Custom asset loaders
When to use this skill
- Loading a custom binary or text format (game data, sidecar configs).
- Producing an asset that depends on other assets (a level that references textures).
- Loading a dependency with settings (
is_srgb = false) or by erased type. - Random access into the file (
reader.seekable()), or writing your ownReader. - Compiler errors: "
MyLoader: TypePathis not implemented", "no methodloader", "missing trait itemseekable".
Canonical pattern
use bevy::asset::io::Reader;
use bevy::asset::{Asset, AssetApp, AssetLoader, LoadContext};
use bevy::image::ImageLoaderSettings;
use bevy::prelude::*;
use bevy::reflect::TypePath;
#[derive(Asset, TypePath, Debug)]
pub struct LevelDef {
pub name: String,
pub gravity: f32,
#[dependency] // counted toward `LoadedWithDependencies`
pub thumbnail: Handle<Image>,
#[dependency]
pub normal_map: Handle<Image>,
}
#[derive(TypePath)] // required on the loader itself since 0.18
pub struct LevelLoader;
impl AssetLoader for LevelLoader {
type Asset = LevelDef;
type Settings = ();
type Error = BevyError; // any `Error` converts via `?`
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let text = String::from_utf8(bytes)?;
let mut level = LevelDef {
name: String::new(),
gravity: 9.81,
thumbnail: Handle::default(),
normal_map: Handle::default(),
};
for line in text.lines() {
let Some((key, value)) = line.split_once('=') else { continue };
match key.trim() {
"name" => level.name = value.trim().to_owned(),
"gravity" => level.gravity = value.trim().parse()?,
// Dependencies go through `LoadContext`, never `AssetServer`.
"thumbnail" => level.thumbnail = load_context.load(value.trim().to_owned()),
// 0.19+: settings, type erasure, untyped loads use `load_builder()`.
"normal_map" => {
level.normal_map = load_context
.load_builder()
.with_settings(|s: &mut ImageLoaderSettings| s.is_srgb = false)
.load(value.trim().to_owned());
}
_ => {}
}
}
Ok(level)
}
fn extensions(&self) -> &[&str] {
&["level"]
}
}
pub struct LevelLoaderPlugin;
impl Plugin for LevelLoaderPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<LevelDef>()
.register_asset_loader(LevelLoader);
}
}Random access
use bevy::asset::io::{Reader, ReaderNotSeekableError};
use bevy::asset::AsyncSeekExt;
async fn read_footer(reader: &mut dyn Reader) -> std::io::Result<[u8; 8]> {
let mut footer = [0u8; 8];
match reader.seekable() {
Ok(seekable) => {
// `SeekableReader: Reader + AsyncSeek`; `AsyncSeekExt` is re-exported by bevy::asset.
seekable.seek(std::io::SeekFrom::End(-8)).await?;
let mut buf = Vec::new();
seekable.read_to_end(&mut buf).await?;
footer.copy_from_slice(&buf);
}
Err(ReaderNotSeekableError) => {
// e.g. HTTP source: stream the whole file instead.
let mut buf = Vec::new();
reader.read_to_end(&mut buf).await?;
footer.copy_from_slice(&buf[buf.len() - 8..]);
}
}
Ok(footer)
}Implementing Reader yourself (0.19+ shape)
use bevy::asset::io::{Reader, ReaderNotSeekableError, SeekableReader};
use bevy::tasks::futures_lite::io::Cursor;
use bevy::tasks::futures_lite::{AsyncRead, AsyncSeek};
use std::pin::Pin;
use std::task::{Context, Poll};
pub struct MemReader(Cursor<Vec<u8>>);
impl AsyncRead for MemReader {
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8])
-> Poll<std::io::Result<usize>> {
Pin::new(&mut self.0).poll_read(cx, buf)
}
}
impl AsyncSeek for MemReader {
fn poll_seek(mut self: Pin<&mut Self>, cx: &mut Context<'_>, pos: std::io::SeekFrom)
-> Poll<std::io::Result<u64>> {
Pin::new(&mut self.0).poll_seek(cx, pos)
}
}
impl Reader for MemReader {
// 0.19+: required. `AsyncSeekForward` is gone.
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
Ok(self) // return `Err(ReaderNotSeekableError)` if you cannot seek
}
}Gotchas
0.19 → 0.20
LoadDirectError::LoadError { error }isBox<AssetLoadError>andAssetLoadError::RequestedHandleTypeMismatchwraps aBox<RequestedHandleTypeMismatchError>. Match through the box.NestedLoadBuilderimmediate loads areasync:load_value(path).await,load_erased_value(type_id, path),load_untyped_value(path),load_value_from_reader(path, reader). They returnResult<LoadedAsset<A>, LoadDirectError>.UntypedHandle::meta_transform()is deleted. Settings travel only throughwith_settingsclosures.
0.18 → 0.19
LoadContext::loader()isload_builder()andNestedLoaderisNestedLoadBuilder. Stale output:ctx.loader().with_settings(..).load(p). Now:ctx.load_builder().with_settings(..).load(p).with_dynamic_type(id).load(p)→load_erased(id, p);with_unknown_type().load(p)→load_untyped(p);immediate().load(p)→load_value(p).await.Readerimpls must implementseekable.AsyncSeekForwardis deleted; implementAsyncSeek(poll_seek) and returnOk(self). Callers of the old trait useAsyncSeekExt::seek(SeekFrom::Current(n)).AssetSaver::savegained a lifetime and a path:asset: SavedAsset<'_, '_, A>, settings: &S, asset_path: AssetPath<'_>.- Resources are components. A loader plugin that inserts a resource holding handles is fine; broad queries can now conflict with it (
bevy-ecs-components).
Still true since 0.18
#[derive(TypePath)]on the loader struct, not only the asset.AssetLoader: TypePath + Send + Sync + 'static.LoadContext::asset_bytes()is gone. Usereader.read_to_end(&mut bytes).await?orload_context.read_asset_bytes(path).await?for a sibling file.- Dependencies must go through
LoadContext.asset_server.loadinside a loader does not register the dependency, soLoadedWithDependenciesfires early. Mark handle fields#[dependency]so#[derive(Asset)]visits them. type Erroronly needsInto<BevyError>.BevyErroritself is the shortest choice;thiserrorenums still work.- No Tokio inside
load. The executor is Bevy's; usebevy::tasks::futures_liteorAsyncComputeTaskPool. Settingsmust beDefault + Serialize + DeserializeOwnedto round-trip through.metafiles.- Extensions match on the full suffix.
"level.ron"matchesfoo.level.ron, notfoo.ron. - Do not import
AsyncReadExtnext todyn Reader. Both traits defineread_to_end; the call becomes ambiguous.Reader::read_to_endis inherent to the trait.
See also
bevy-assets— using theHandle<LevelDef>once loaded,load_builderonAssetServer.bevy-voxel-data— RON-driven asset patterns.bevy-migration-0-18-to-0-19—loader()→load_builder(),Reader::seekable.bevy-migration-0-19-to-0-20— boxed error variants.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

