NiklasEi / bevy_asset_loader

Bevy plugin helping with asset loading and organization
Apache License 2.0
482 stars 53 forks source link

Hooking into `AssetCollection::create` #118

Closed deifactor closed 1 year ago

deifactor commented 1 year ago

I have a weapon system where the shotgun needs both some assets loaded off disk and some assets that are created in code via explicit calls to Assets::add. RIght now I'm doing this:

#[derive(AssetCollection, Resource)]
pub(super) struct ShotgunAssets {
    bullet: BulletAssets,
    #[asset(path = "shotgun.wav")]
    shotgun_sound: Handle<AudioSource>,
}

pub(super) struct BulletAssets {
    mesh: Mesh2dHandle,
    material: Handle<BulletTrail>,
}

impl FromWorld for BulletAssets {
    fn from_world(world: &mut World) -> Self {
        let cell = world.cell();
        let mut meshes = cell.get_resource_mut::<Assets<Mesh>>().unwrap();
        let mut materials = cell.get_resource_mut::<Assets<BulletTrail>>().unwrap();
        let mesh = meshes
            .add(shape::Quad::new(Vec2::new(BULLET_TRAIL_LENGTH, 1.)).into())
            .into();
        let material = materials.add(BulletTrail {
            color: Color::WHITE,
        });
        Self { mesh, material }
    }
}

And it works, but this relies on the fact that all my 'internally-created' assets happen to make sense to put together in a struct. It'd be nice if I could do something like this (I'm not tied to this API design):

#[derive(AssetCollection, Resource)]
#[asset_collection(has_post_create)]
pub(super) struct ShotgunAssets {
    bullet_mesh: Mesh2dHandle,
    bullet_material: Handle<BulletTrail>,
    #[asset(path = "shotgun.wav")]
    shotgun_sound: Handle<AudioSource>,
}

impl ShotgunAssets {
    fn post_create(&mut self, world: &mut World) {
        // ...
    }
}

where the has_post_create attribute inserts a call to ShotgunAssets::post_create at the end of AssetCollection::create.

deifactor commented 1 year ago

BTW, thanks a ton for this library. :)

NiklasEi commented 1 year ago

Adding a single hook that takes &mut self to the collection means that we need to build the asset collection before calling the hook. All fields that you want to update in your hook would need a default implementation and already have a value when the hook gets called.

Your current solution has a single FromWorld impl for the BulletAssets. You could instead do something like this:

#[derive(AssetCollection, Resource)]
#[asset_collection(has_post_create)]
pub(super) struct ShotgunAssets {
    bullet_mesh: BulletMesh,
    bullet_material: BulletMaterial,
    #[asset(path = "shotgun.wav")]
    shotgun_sound: Handle<AudioSource>,
}

struct BulletMesh(Mesh2dHandle);
impl FromWorld for BulletMesh {
    fn from_world(world: &mut World) -> Self {
        let mut meshes = world.get_resource_mut::<Assets<Mesh>>().unwrap();
        BulletMesh(Mesh2dHandle::from(meshes.add(Quad::new(Vec2::splat(5.)).into())))
    }
}

struct BulletMaterial(Handle<BulletTrail>);
impl FromWorld for BulletMaterial {
    fn from_world(world: &mut World) -> Self {
        // here you can also use the asset server 
        // Assets loaded from files as part of the asset collection are already in their respective `Assets` resources
    }
}
deifactor commented 1 year ago

Yeah, that does work; the reason I didn't want to is that now I have these extra boilerplate types and I either have to derive/implement Deref or write .0.

That's a fair point about needing a default implementation; an #[asset(initializer = "my_function)] where fn my_function(world: &mut World) -> WhateverType would work, but then you couldn't get at other assets unless you did something excessively clever like passing in a tuple of mutable references to all fields without initializers.

NiklasEi commented 1 year ago

I think wrapper types with FromWorld implementations are the way to go here. At least for now I don't see a nicer API that would allow usage of already loaded parts of the collection.

The .0s aren't beautiful, but acceptable in my opinion.

NiklasEi commented 1 year ago

I think has been resolved with the code example above. Feel free to re-open if you don't agree :slightly_smiling_face: