Custom Asset Format
Level Format: The Quick and Dirty Way
Let's go with a basic format that you can manually edit with a good idea of how it should render: emojis to the rescue!
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜🙂⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜🟩🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
Asset Type
To match the basic level format, we'll use a basic type that will just be a vec of vecs of tiles. The struct must derive the Asset
trait.
#![allow(unused)] fn main() { extern crate bevy; use bevy::prelude::*; #[derive(Asset, TypePath)] struct Level { pub tiles: Vec<Vec<Tile>>, } enum Tile { Empty, Ground, } }
Asset Loader
To load this format, we'll read the file character by character, then choose the right tile depending on the character. Bevy expects custom asset loader to implement the trait AssetLoader
.
#![allow(unused)] fn main() { extern crate bevy; extern crate thiserror; use bevy::{asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, prelude::*}; use thiserror::Error; #[derive(Asset, TypePath)] struct Level {pub tiles: Vec<Vec<Tile>>} enum Tile {Empty, Ground} #[derive(Default)] struct LevelLoader; #[derive(Debug, Error)] enum LevelLoaderError { #[error("Could not load asset: {0}")] Io(#[from] std::io::Error), #[error("Unknown tile: {0}")] UnknownTile(char), } impl AssetLoader for LevelLoader { type Asset = Level; type Settings = (); type Error = LevelLoaderError; async fn load( &self, reader: &mut dyn Reader, _settings: &(), _load_context: &mut LoadContext<'_>, ) -> Result<Self::Asset, Self::Error> { let mut buf = String::new(); reader.read_to_string(&mut buf).await?; let mut tiles = vec![]; let mut line = vec![]; for char in buf.chars() { match char { '⬜' => line.push(Tile::Empty), '🟩' => line.push(Tile::Ground), '🙂' => (), '\n' => { tiles.push(line); line = vec![]; } char => Err(LevelLoaderError::UnknownTile(char))?, } } Ok(Level { tiles }) } fn extensions(&self) -> &[&str] { &["bw"] } } }
Loading the Level
Custom asset formats and loaders must be initiated in the application with App::init_asset
and App::init_asset_loader
. We can wrap that in a plugin.
#![allow(unused)] fn main() { extern crate bevy; extern crate thiserror; use bevy::{asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, prelude::*}; use thiserror::Error; #[derive(Asset, TypePath)] struct Level {pub tiles: Vec<Vec<Tile>>} enum Tile {Empty, Ground} #[derive(Default)] struct LevelLoader; #[derive(Debug, Error)] enum LevelLoaderError {} impl AssetLoader for LevelLoader { type Asset = Level; type Settings = (); type Error = LevelLoaderError; async fn load(&self, reader: &mut dyn Reader, _settings: &(), _load_context: &mut LoadContext<'_>) -> Result<Self::Asset, Self::Error> { unimplemented!() } fn extensions(&self) -> &[&str] { &["bw"] } } fn level_loader_plugin(app: &mut App) { app.init_asset::<Level>().init_asset_loader::<LevelLoader>(); } }
Don't forget to add the new level_loader_plugin
to the app in the main.rs
file.
Now we can load the asset file like the sprites we're already using, and keeping the handle to the loaded level in a resource.
#![allow(unused)] fn main() { extern crate bevy; use bevy::{asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, prelude::*}; #[derive(Asset, TypePath)] struct Level {pub tiles: Vec<Vec<Tile>>} enum Tile {Empty, Ground} #[derive(Resource)] pub struct LoadedLevel { pub level: Handle<Level>, } fn load_assets( mut commands: Commands, asset_server: Res<AssetServer>, // ... ) { commands.insert_resource(LoadedLevel { level: asset_server.load("level.bw"), }); // ... } }