# Waitables API (aka Loading Screen API) ## Background Loading assets is a deferred operation. You call `AssetServer::load`, and then at some point in the future, the asset loads and can be used. For end users, this stinks. Currently, users need to create a system that reads `MessageReader<AssetEvent<A>>`, listens for the loaded event for that asset ID, and in response runs whatever code you need. Assets-as-entities will likely make this a bit less painful, in that users can instead create an observer to handle the loaded event instead of needing a system to poll. But even that is over-simplified. In practice, you don't just need to wait for one asset. A single scene in a game likely has all sorts of assets: the level geometry, explosive barrels, enemies, the player, etc. Just calling `AssetServer::load` for each of these and starting the game can result in the player falling through the floor if the level doesn't load in time! Or your explosive barrels might also not load in fast enough. Or the enemies might only be partially loaded in. What we really want is to start loading all of these assets, wait for all of them to load, and then trigger an event of some kind to kick off the game (e.g., changing states). ## Goals Provide an API that allows you to: 1. Request to load a set of assets. 2. Wait for the assets to load. 3. Allow reacting to when all assets have loaded. Some desired features: - Not limited to assets. While we should have nice utilities for assets, anything you can wait on should be supported. For example, it would be nice if we could wait for a render pipeline to be ready. This is just an operation we have to wait on. - Current progress tracking. Some loading screens like to show a bar indicating how far along loading is. We should provide a way for tracking this (and ideally with a way to apply a weight to each item). - Not global. Users should be allowed to have multiple waitables in progress at once. While waitables are expected to be used for loading screens, they could also be used to ensure streaming assets are ready when you need them. - "Sworn to carry your burdens" (we love Skyrim). The waitable should persist and keep any asset handles loaded until a user explicitly unloads the loading screen. This allows users to create a loading screen, fill it with a bunch of asset handles, wait for the loading screen to load, use the assets during the game (assuming they are all loaded), and then drop all the assets at once by despawning the loading screen when transitioning to a new scene. In other words, the loading screen should act like an "asset scope" - keep these assets alive while this scene is running. ### Naming Feel free to bikeshed the name. "Waitables" is provided as a placeholder because it evokes the right idea of waiting for a bunch of stuff to happen, but doesn't have the baggage of "loading screen" which some users don't want something covering the whole screen saying "loading" - that's not what this is! Possible alternatives: - Barrier: it basically acts the same as [Rust's Barrier](https://doc.rust-lang.org/std/sync/struct.Barrier.html#method.new). ## Non-goals - An actual full-screen "Loading" effect. We should not be defining what the in-progress load "looks like". A user may want to show a loading splash screen, or display a video, or wait completely in the background until its ready (by making the `Next` button gray for example). ## Guidelines 1. Waitables as entities. We always end up here, so let's just design with this in mind. This immediately allows us to support multiple waitables. This also allows using observers to handle the "Ready" event. ## [Rejected] Proposal 1: Just use an async Future Async is just fancy polling. So why not just make a waitable be one big task? Once the task completes we say loading is done! Here's what this could look like: ```rust let asset_server = asset_server.clone(); commands.spawn( Waitable::new(async move { futures::join!( // Maybe we move to a future where `AssetServer::load` returns a future? asset_server.load::<Scene>("level.gltf#Scene0"), asset_server.load::<Scene>("explosive_barrel.gltf#Scene0"), asset_server.load::<Scene>("piranha_plant.gltf#Scene0"), asset_server.load::<Image>("minimap.png"), ) }) ).observe(|event: On<Loaded>| { // There's probably a prettier way to fetch this value. For example, we could just // call `AssetServer::load` in a sync way to get the handle and just assume it // remains loaded. let ( level, explosive_barrel, piranha_plant, image ): &( Handle<Scene>, Handle<Scene>, Handle<Scene>, Handle<Image>, ) = event.value.as_ref().downcast().unwrap(); // Do stuff like spawn the level. }); ``` ### Pros - Async makes the code look pretty! Especially if you need to "chain" operations (e.g., do some network requests, then load assets based on that). - With async systems, this is just a natural extension. We can even do ECS operations as things load. ### Cons - There's no way to count how many "internal" tasks there are! The waitable only sees one big `Future`. So we can't put a max on our progress bar. Similarly, we don't know how far through the load we are - have we loaded 2 / 4 assets? - We need to return the handles from the task, else they will be dropped and won't stay loaded. - This turns declarative ideas ("wait for all this junk to load") into an imperative function ("this is how you load all the assets"). ## Proposal 2: Vec of `trait WaitableItem` Let's take the previous proposal and sacrifice a little flexibility for a lot of simplicity. Instead of making one big `Future`, each thing we can wait on becomes its own little `Future` (with some special sauce). We can start by making a trait: ```rust trait WaitableItem { type Output; /// This is our version of Future::poll. fn poll( self: Pin<&mut Self>, cx: &mut Context<'_>, // This allows us to send progress updates to the loading screen. load_state: &mut LoadState, ) -> Poll<Self::Output>; /// Gets the overall cost of the Loadable. This lets us set a max to our fn get_total_cost(&self) -> f32; } struct WaitableFuture<F: Future> { future: F, cost: f32, } impl <F: Future> WaitableItem for WaitableFuture<F> { type Output = F::Output; fn poll( self: Pin<&mut Self>, cx: &mut Context<'_>, load_state: &mut LoadState, ) -> Poll<Self::Output> { Future::poll(do_pin_magic(self.future), cx) } fn get_total_cost(&self) -> f32 { self.cost } } ``` `WaitableFuture` provides a simpler interface for people who just want to wait on a future. More complex waitable things could provide status updates through the `LoadState`. Then our API looks something like: ```rust let asset_server = asset_server.clone(); commands.spawn( Waitable::new() // `load_asset` returns a future. .with("Level", load_asset::<Scene>("level.gltf#Scene0")) .with("Barrel", load_asset::<Scene>("explosive_barrel.gltf#Scene0")) .with("Plant", load_asset::<Scene>("piranha_plant.gltf#Scene0")) .with("Minimap", load_asset::<Image>("minimap.png")) ).observe(|event: On<Loaded>| { let level = event.get_value::<Handle<Scene>>("Level").unwrap(); let barrel = event.get_value::<Handle<Scene>>("Barrel").unwrap(); let plant = event.get_value::<Handle<Scene>>("Plant").unwrap(); let minimap = event.get_value::<Handle<Image>>("Minimap").unwrap(); // We could also just use the regular `AssetServer::load` calls instead. // Do stuff like spawn the level. }); ``` TODO: Pros/Cons