steps to asset loader

retrofit bevy_quickstart with bevy_asset_loader

2024-09-21
#bevy

bevy_quickstart (soon to be bevy_new_2d) exposes some great basic patterns for game development. However, many folks want to use bevy_asset_loader rather than the built-in asset management, whether out of habit or personal preference.

Herewith the steps for doing a manual conversion.

What you should really do instead

Note that by far the smarter way is to yoink demo in favour of your own custom game plugin, which might well be inspired by demo but doesn't necessarily adhere to every last skerrick of its goodness. What follows is more for the curious, the tinkerers, the "what if?"-ers... y'know, my people.

Setup

cargo add bevy_asset_loader

Replace the contents of src/asset_tracking.rs with (for example):

use bevy::prelude::*;
use bevy_asset_loader::prelude::*;

use crate::screens::Screen;

pub(super) fn plugin(app: &mut App) {
    app.add_loading_state(
        LoadingState::new(Screen::Loading)
            .continue_to_state(Screen::Title)
            .load_collection::<AudioAssets>()
            .load_collection::<ImageAssets>(),
    );
}

#[derive(AssetCollection, Resource)]
pub struct AudioAssets {
    #[asset(path = "audio/sound_effects/button_hover.ogg")]
    button_hover: Handle<AudioSource>,

    #[asset(path = "audio/sound_effects/button_press.ogg")]
    button_press: Handle<AudioSource>,

    #[asset(path = "audio/music/Fluffing A Duck.ogg")]
    fluffing_a_duck: Handle<AudioSource>,

    #[asset(path = "audio/music/Monkeys Spinning Monkeys.ogg")]
    monkeys_spinning_monkeys: Handle<AudioSource>,

    #[asset(path = "audio/sound_effects/step1.ogg")]
    path_step_1: Handle<AudioSource>,

    #[asset(path = "audio/sound_effects/step2.ogg")]
    path_step_2: Handle<AudioSource>,

    #[asset(path = "audio/sound_effects/step3.ogg")]
    path_step_3: Handle<AudioSource>,

    #[asset(path = "audio/sound_effects/step4.ogg")]
    path_step_4: Handle<AudioSource>,
}

#[derive(AssetCollection, Resource)]
pub struct ImageAssets {
    #[asset(path = "images/ducky.png")]
    pub ducky: Handle<Image>,

    #[asset(path = "images/splash.png")]
    pub splash: Handle<Image>,
}

Obviously, this is a 1:1 replacement of the quickstart resources, but you could omit most of 'em if you're planning to delete anyway.

Surgically remove asset management

Next, there's a bit of a pattern to replacing the quickstart's asset code. We'll use player.rs as an example, but something similar should be done for each part of the demo that does asset management:

  • remove PlayerAssets
  • replace player_assets in spawn_player with
    image_assets: Res<ImageAssets>,
  • replace any references to player_assets with image_assets
  • remove app.load_resource::<PlayerAssets>() from fn plugin
  • remove any unused imports

When it comes to music, we might use credits.rs as an example:

  • remove CreditsMusic
  • replace play and stop functions with something akin to this:
fn play_credits_music(mut commands: Commands, audio_assets: Res<AudioAssets>) {
    commands
        .spawn((
            AudioBundle {
                source: audio_assets.monkeys_spinning_monkeys.clone(),
                settings: PlaybackSettings::LOOP,
            },
            Music,
        ));
}

fn stop_music(mut commands: Commands, music: Query<Entity, With<Music>>) {
    if let Ok(entity) = music.get_single() {
        commands.entity(entity).despawn_recursive();
    }
}

i.e. just query for the entity and despawn, instead of saving it onto CreditsMusic. (You could also state scope the audio, if you're not planning to do anything particularly fancy with it. State-scoping is a lot like magic.)

In loading.rs, remove any non-existent asset resources, and the functions continue_to_title_screen and all_assets_loaded, as bevy_asset_loader is taking care of this for us.

A note on scheduling

Some systems are configured to run against the general Update schedule, when we really want to be more precise about when they are called. For example, those in AppSet::Update probably should be divided into AppSet::UpdateAnytime and AppSet::UpdateGameplay or some such. This is also true of AppSet::RecordInput, which currently is only used for player movement. We might do something like:

    app.configure_sets(Update, (AppSet::TickTimers, AppSet::Update).chain());
    app.configure_sets(
        Update,
        (AppSet::RecordInput, AppSet::GameplayUpdate)
            .chain()
            .run_if(in_state(Screen::Gameplay)),
    );

    // ...

    #[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
    enum AppSet {
        TickTimers,
        RecordInput,
        Update,
        /// Updates only during gameplay (when resources etc. should already exist)
        GameplayUpdate,
    }

The goal is just to prevent unnecessary Update system runs from attempting to access a resource before it exists. Here, we're delegating responsibility for asset loading, so we really just need to know which states are safe to assume the resource will exist in!

Move your systems to the appropriate AppSet, e.g.:

    app.add_systems(
        Update,
        (
            update_animation_timer.in_set(AppSet::TickTimers),
            (
                update_animation_movement,
                update_animation_atlas,
                trigger_step_sound_effect,
            )
                .chain()
                .in_set(AppSet::GameplayUpdate),
        ),
    );