NiklasEi / bevy_asset_loader

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

Allow adding assets to states from plugins #81

Closed uzytkownik closed 1 year ago

uzytkownik commented 1 year ago

Currently the only (official) way to describe the loading transitions is from fn main function. For bigger projects, from what I understand, plugins are often used.

I wrote very quick'n'dirty proof-of-concept:

use bevy::{prelude::*, utils::*, ecs::schedule::StateData, asset::Asset};
use bevy_asset_loader::prelude::*;

enum AssetLoader<T> {
    BeingBuilt {
        hash: HashMap<T, Option<LoadingState<T>>>,
    },
    Completed
}

impl<T> Default for AssetLoader<T> {
    fn default() -> Self {
        Self::BeingBuilt {
            hash: Default::default()
        }
    }
}

pub trait AssetLoaderExt {
    fn continue_to_state<T: StateData>(&mut self, in_state: T, next: T) -> &mut Self;
    fn init_resource_in_state<T: StateData, R: FromWorld + Send + Sync + 'static>(&mut self, in_state: T) -> &mut Self;
    fn with_collection<T: StateData, A: AssetCollection>(&mut self, in_state: T) -> &mut Self;
    fn with_dynamic_collection<T: StateData, C: DynamicAssetCollection + Asset>(&mut self, in_state: T, files: Vec<&str>) -> &mut Self;
    fn with_loading_state<T: StateData>(&mut self, in_state: T, run: impl FnOnce(LoadingState<T>) -> LoadingState<T>) -> &mut Self;
    fn complete_loading<T: StateData>(&mut self) -> &mut Self;
}

impl AssetLoaderExt for App {

    fn continue_to_state<T: StateData>(&mut self, in_state: T, next: T) -> &mut Self {
        self.with_loading_state(in_state, |ls| {
            ls.continue_to_state(next)
        })
    }

    fn init_resource_in_state<T: StateData, R: FromWorld + Send + Sync + 'static>(&mut self, in_state: T) -> &mut Self {
        self.with_loading_state(in_state, |ls| {
            ls.init_resource::<R>()
        })
    }

    fn with_collection<T: StateData, A: AssetCollection>(&mut self, in_state: T) -> &mut Self {
        self.with_loading_state(in_state, |ls| {
            ls.with_collection::<A>()
        })
    }

    fn with_dynamic_collection<T: StateData, C: DynamicAssetCollection + Asset>(&mut self, in_state: T, files: Vec<&str>) -> &mut Self {
        self.with_loading_state(in_state, |ls| {
            ls.with_dynamic_collections::<C>(files)
        })
    }

    fn with_loading_state<T: StateData>(&mut self, in_state: T, run: impl FnOnce(LoadingState<T>) -> LoadingState<T>) -> &mut Self {
        let mut al = self.world.get_resource_or_insert_with(|| AssetLoader::<T>::default());
        match &mut *al {
            AssetLoader::BeingBuilt { hash } => {
                let ols = hash.entry(in_state).or_insert(Some(LoadingState::new(in_state)));
                *ols = Some(run(ols.take().unwrap()));
            },
            AssetLoader::Completed => unreachable!()
        }
        self
    }

    fn complete_loading<T: StateData>(&mut self) -> &mut Self {
        if let Some(al) = self.world.remove_resource::<AssetLoader<T>>() {
            match al {
                AssetLoader::BeingBuilt { mut hash } => {
                    for (_state, ls) in hash.drain() {
                        self.add_loading_state(ls.unwrap());
                    }
                },
                AssetLoader::Completed => unreachable!()
            }
        }
        self.world.insert_resource(AssetLoader::<T>::Completed);
        self
    }
}

(It would probably be able to expose simpler interface if LoadingState changed from fn foo(self) -> Self to fn foo(&mut self) -> &mut Self).

This way the loading can be constructed as:

// ... 
impl Plugin for PlayerPlugin {
    fn build(&self, app: &mut App) {
        // ...
        app.with_collection::<_, PlayerImages>(&State::Game);
    }
}

// ...
fn main() {
    let mut app = App::new();
    // ...
    app.continue_to_state(State::Init, State::Game);
    // ...
    app.complete_loading::<State>();
    app.run();
}

(I suspect that complete_loading can be avoided with 'native' loading)

NiklasEi commented 1 year ago

I have not seen a use case for this so far. Can you explain what you want the possibility to configure a loading state across multiple plugins for? In my own code bases, I would like them to be configured in one plugin to make it easier to follow.

The biggest pain point I would have with this kind of API is that the user needs to remember to call complete_loading and that the actual content of the loading state then depends on the order of user plugins (since they can all try to add new collections to a state).

uzytkownik commented 1 year ago

I divided my game (toy but I'm trying to do it properly) into files and treat each component as plugin. So player is a plugin, mob is a plugin, board is a plugin etc.

NiklasEi commented 1 year ago

So you want to add the collections concerning the player in the player plugin, the board in the board plugin and so on? This should already be possible. You can add identical loading states with different collections and they will get combined.

uzytkownik commented 1 year ago

I missed this from documentation. Can it be treated as documentation bug then?

NiklasEi commented 1 year ago

Yes, I can add that to the readme and consider this issue closed as soon as the functionality is documented.