Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AssetPack syntax sugar for static assets #13819

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,17 @@ description = "Embed an asset in the application binary and load it"
category = "Assets"
wasm = true

[[example]]
name = "asset_pack"
path = "examples/asset/asset_pack.rs"
doc-scrape-examples = true

[package.metadata.example.asset_pack]
name = "Asset Bundle"
description = "Load an asset from an AssetPack"
category = "Assets"
wasm = true

[[example]]
name = "extra_asset_source"
path = "examples/asset/extra_source.rs"
Expand Down
123 changes: 121 additions & 2 deletions crates/bevy_asset/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
#![allow(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

use bevy_macro_utils::BevyManifest;
use bevy_macro_utils::{get_struct_fields, BevyManifest};
use proc_macro::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Path};
use syn::{parse_macro_input, Data, DeriveInput, Field, Path};

pub(crate) fn bevy_app_path() -> Path {
BevyManifest::default().get_path("bevy_app")
}

pub(crate) fn bevy_asset_path() -> Path {
BevyManifest::default().get_path("bevy_asset")
Expand Down Expand Up @@ -123,3 +127,118 @@ fn derive_dependency_visitor_internal(
}
})
}

const EMBEDDED_ATTRIBUTE: &str = "embedded";
const LOAD_ATTRIBUTE: &str = "load";

#[proc_macro_derive(AssetPack, attributes(embedded, load))]
pub fn derive_asset_pack(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let bevy_app_path: Path = bevy_app_path();
let bevy_asset_path: Path = bevy_asset_path();
match derive_asset_pack_internal(&ast, &bevy_app_path, &bevy_asset_path) {
Ok(tokens) => TokenStream::from(tokens),
Err(err) => err.into_compile_error().into(),
}
}

enum FieldLoadMethod {
Embedded(syn::Ident, syn::Result<syn::LitStr>),
Load(syn::Ident, syn::Result<syn::Expr>),
Unknown(syn::Error),
}

impl FieldLoadMethod {
fn new(field: &Field) -> Self {
let ident = field.ident.as_ref().unwrap(); //get_struct_fields rejects tuple structs
field
.attrs
.iter()
.find_map(|attr| {
if attr.path().is_ident(EMBEDDED_ATTRIBUTE) {
Some(Self::Embedded(
ident.clone(),
attr.parse_args::<syn::LitStr>(),
))
} else if attr.path().is_ident(LOAD_ATTRIBUTE) {
Some(Self::Load(ident.clone(), attr.parse_args::<syn::Expr>()))
} else {
None
}
})
.unwrap_or_else(|| {
Self::Unknown(syn::Error::new_spanned(
field,
"missing attribute: use #[embedded(\"...\")] or #[load(\"...\")]",
))
})
}

fn error(&self) -> proc_macro2::TokenStream {
match self {
FieldLoadMethod::Unknown(err) => err.to_compile_error(),
_ => proc_macro2::TokenStream::new(),
}
}

fn init(&self, bevy_asset_path: &Path) -> proc_macro2::TokenStream {
match self {
FieldLoadMethod::Embedded(_, path) => match path {
Ok(path) => quote!(#bevy_asset_path::embedded_asset!(app, #path);),
Err(err) => err.to_compile_error(),
},
_ => proc_macro2::TokenStream::new(),
}
}

fn load(&self, bevy_asset_path: &Path) -> proc_macro2::TokenStream {
match self {
FieldLoadMethod::Embedded(ident, path) => match path {
Ok(path) => quote!(#ident: {
let embedded_path = #bevy_asset_path::embedded_path!(#path);
let embedded_source_id = #bevy_asset_path::io::AssetSourceId::from("embedded");
let asset_path = #bevy_asset_path::AssetPath::from_path(embedded_path.as_path()).with_source(embedded_source_id);
asset_server.load(asset_path)
},),
Err(err) => err.to_compile_error(),
},
FieldLoadMethod::Load(ident, path) => match path {
Ok(path) => quote!(#ident: asset_server.load(#path),),
Err(err) => err.to_compile_error(),
},
_ => proc_macro2::TokenStream::new(),
}
}
}

fn derive_asset_pack_internal(
ast: &DeriveInput,
bevy_app_path: &Path,
bevy_asset_path: &Path,
) -> syn::Result<proc_macro2::TokenStream> {
let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();

let fields = get_struct_fields(&ast.data)?;

let load_methods = fields.iter().map(FieldLoadMethod::new).collect::<Vec<_>>();
let error = load_methods.iter().map(|field| field.error());
let init = load_methods.iter().map(|field| field.init(bevy_asset_path));
let load = load_methods.iter().map(|field| field.load(bevy_asset_path));

Ok(quote! {
#(#error)*

impl #impl_generics #bevy_asset_path::io::pack::AssetPack for #struct_name #type_generics #where_clause {
fn init(app: &mut #bevy_app_path::App) {
#(#init)*
}

fn load(asset_server: &#bevy_asset_path::AssetServer) -> Self {
Self {
#(#load)*
}
}
}
})
}
2 changes: 1 addition & 1 deletion crates/bevy_asset/src/io/embedded/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl EmbeddedAssetRegistry {
#[macro_export]
macro_rules! embedded_path {
($path_str: expr) => {{
embedded_path!("src", $path_str)
$crate::embedded_path!("src", $path_str)
}};

($source_path: expr, $path_str: expr) => {{
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_asset/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod embedded;
pub mod file;
pub mod gated;
pub mod memory;
pub mod pack;
pub mod processor_gated;
#[cfg(target_arch = "wasm32")]
pub mod wasm;
Expand Down
69 changes: 69 additions & 0 deletions crates/bevy_asset/src/io/pack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use std::{marker::PhantomData, sync::OnceLock};

use bevy_app::{App, Plugin};
use bevy_ecs::system::{Res, Resource, SystemParam};

pub use bevy_asset_macros::AssetPack;

use crate::AssetServer;

/// This trait (and associated derive macro) provides syntax sugar for loading related assets
/// whose sources are known at compile time. When using the derive macro, all fields must have
/// either an `#[embedded("...")]` attribute or a `#[load(...)]` attribute.
///
/// `embedded` takes as argument a relative path to the embedded asset, while
/// `load` takes as argument an expression that implements `Into<AssetPath<'_>>`. This could be a string
/// literal, or something else depending on the use-case.
///
/// For accessing an `AssetPack`, see `AssetPackPlugin`, `Pack` and `GetPack`
/// For a usage example, see the `asset_pack` example.

pub trait AssetPack: Send + Sync + 'static {
fn init(app: &mut App);
fn load(asset_server: &AssetServer) -> Self;
}

/// Provides setup for loading an asset pack of type `T`
pub struct AssetPackPlugin<T: AssetPack>(PhantomData<T>);

impl<T: AssetPack> Default for AssetPackPlugin<T> {
fn default() -> Self {
Self(PhantomData)
}
}

impl<T: AssetPack> Plugin for AssetPackPlugin<T> {
fn build(&self, app: &mut App) {
T::init(app);
app.init_resource::<Pack<T>>();
}
}

/// A `Resource` that wraps access to an `AssetPack`
#[derive(Resource)]
struct Pack<T: AssetPack>(OnceLock<T>);

impl<T: AssetPack> Pack<T> {
fn get(&self, asset_server: &AssetServer) -> &T {
self.0.get_or_init(|| T::load(asset_server))
}
}

impl<T: AssetPack> Default for Pack<T> {
fn default() -> Self {
Self(Default::default())
}
}

/// A `SystemParam` that wraps `Pack<T>` and `AssetServer` for simple access
#[derive(SystemParam)]
pub struct GetPack<'w, T: AssetPack> {
handles: Res<'w, Pack<T>>,
asset_server: Res<'w, AssetServer>,
}

impl<'w, T: AssetPack> GetPack<'w, T> {
pub fn get(&self) -> &T {
self.handles.get(&self.asset_server)
}
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Example | Description

Example | Description
--- | ---
[Asset Bundle](../examples/asset/asset_pack.rs) | Load an asset from an AssetPack
[Asset Decompression](../examples/asset/asset_decompression.rs) | Demonstrates loading a compressed asset
[Asset Loading](../examples/asset/asset_loading.rs) | Demonstrates various methods to load assets
[Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets
Expand Down
34 changes: 34 additions & 0 deletions examples/asset/asset_pack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Example of a custom asset bundle

use bevy::asset::io::pack::{AssetPack, AssetPackPlugin, GetPack};
use bevy::prelude::*;

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

struct AssetPackExamplePlugin;

#[derive(AssetPack)]
struct ExampleAssetPack {
#[embedded("files/bevy_pixel_dark.png")]
sprite: Handle<Image>,
}

impl Plugin for AssetPackExamplePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(AssetPackPlugin::<ExampleAssetPack>::default());
}
}
ecoskey marked this conversation as resolved.
Show resolved Hide resolved

fn setup(mut commands: Commands, assets: GetPack<ExampleAssetPack>) {
commands.spawn(Camera2dBundle::default());

commands.spawn(SpriteBundle {
texture: assets.get().sprite.clone(),
..default()
});
}