diff --git a/README.md b/README.md index e318ef5d..8251ff73 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,15 @@ The best way to get started is to play around with what you find in [`src/demo/` This template comes with a basic project structure that you may find useful: -| Path | Description | -|------------------------------------------|--------------------------------------------------------------------------------| -| [`src/lib.rs`](./src/lib.rs) | App setup | -| [`src/assets.rs`](./src/assets.rs) | Definition of assets that will be preloaded before the game starts | -| [`src/audio/`](./src/audio) | Commands for playing SFX and music | -| [`src/demo/`](./src/demo) | Example game mechanics & content (replace with your own code) | -| [`src/dev_tools.rs`](./src/dev_tools.rs) | Dev tools for dev builds (press \` aka backtick to toggle) | -| [`src/screens/`](./src/screens) | Splash screen, title screen, playing screen, etc. | -| [`src/theme/`](./src/theme) | Reusable UI widgets & theming | +| Path | Description | +| -------------------------------------------------- | -------------------------------------------------------------------------- | +| [`src/lib.rs`](./src/lib.rs) | App setup | +| [`src/asset_tracking.rs`](./src/asset_tracking.rs) | A simple, high-level way to load collections of asset handles as resources | +| [`src/audio/`](./src/audio) | Commands for playing SFX and music | +| [`src/demo/`](./src/demo) | Example game mechanics & content (replace with your own code) | +| [`src/dev_tools.rs`](./src/dev_tools.rs) | Dev tools for dev builds (press \` aka backtick to toggle) | +| [`src/screens/`](./src/screens) | Splash screen, title screen, playing screen, etc. | +| [`src/theme/`](./src/theme) | Reusable UI widgets & theming Feel free to move things around however you want, though. diff --git a/src/asset_tracking.rs b/src/asset_tracking.rs new file mode 100644 index 00000000..a8c17bb2 --- /dev/null +++ b/src/asset_tracking.rs @@ -0,0 +1,59 @@ +//! A simple, high-level way to load collections of asset handles as resources + +use bevy::prelude::*; + +pub(super) fn plugin(app: &mut App) { + app.init_resource::() + .add_systems(PreUpdate, load_resource_assets); +} + +pub trait LoadResource { + /// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies + /// have been loaded, it will be inserted as a resource. This ensures that the resource only + /// exists when the assets are ready. + fn load_resource(&mut self) -> &mut Self; +} + +impl LoadResource for App { + fn load_resource(&mut self) -> &mut Self { + self.init_asset::(); + let world = self.world_mut(); + let value = T::from_world(world); + let assets = world.resource::(); + let handle = assets.add(value); + let mut handles = world.resource_mut::(); + handles.waiting.push((handle.untyped(), |world, handle| { + let assets = world.resource::>(); + if let Some(value) = assets.get(handle.id().typed::()) { + world.insert_resource(value.clone()); + } + })); + self + } +} + +/// A function that inserts a loaded resource. +type InsertLoadedResource = fn(&mut World, &UntypedHandle); + +#[derive(Resource, Default)] +struct ResourceHandles { + waiting: Vec<(UntypedHandle, InsertLoadedResource)>, + #[allow(unused)] + finished: Vec, +} + +fn load_resource_assets(world: &mut World) { + world.resource_scope(|world, mut resource_handles: Mut| { + world.resource_scope(|world, assets: Mut| { + for _ in 0..resource_handles.waiting.len() { + let (handle, insert_fn) = resource_handles.waiting.pop().unwrap(); + if assets.is_loaded_with_dependencies(&handle) { + insert_fn(world, &handle); + resource_handles.finished.push(handle); + } else { + resource_handles.waiting.push((handle, insert_fn)); + } + } + }); + }); +} diff --git a/src/assets.rs b/src/assets.rs deleted file mode 100644 index 0060ba85..00000000 --- a/src/assets.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! This module contains the asset handles used throughout the game. -//! During `Screen::Loading`, the game will load the assets specified here. -//! Your systems can then request the resources defined here to access the -//! loaded assets. - -use bevy::{ - prelude::*, - render::texture::{ImageLoaderSettings, ImageSampler}, - utils::HashMap, -}; - -pub(super) fn plugin(app: &mut App) { - app.register_type::(); - app.init_resource::(); - - app.register_type::(); - app.init_resource::(); - - app.register_type::(); - app.init_resource::(); -} - -#[derive(Resource, Debug, Deref, DerefMut, Reflect)] -#[reflect(Resource)] -pub struct ImageHandles(HashMap>); - -impl ImageHandles { - pub const PATH_DUCKY: &'static str = "images/ducky.png"; -} - -impl FromWorld for ImageHandles { - fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - - let pixel_art_settings = |settings: &mut ImageLoaderSettings| { - // Use `nearest` image sampling to preserve the pixel art style. - settings.sampler = ImageSampler::nearest(); - }; - - let pixel_art_paths = [Self::PATH_DUCKY]; - let map = pixel_art_paths - .into_iter() - .map(|path| { - ( - path.to_string(), - asset_server.load_with_settings(path, pixel_art_settings), - ) - }) - .collect(); - - Self(map) - } -} - -/// Stores the handles for background music, aka soundtracks. -#[derive(Resource, Debug, Deref, DerefMut, Reflect)] -#[reflect(Resource)] -pub struct BgmHandles(HashMap>); - -impl BgmHandles { - pub const PATH_CREDITS: &'static str = "audio/bgm/Monkeys Spinning Monkeys.ogg"; - pub const PATH_GAMEPLAY: &'static str = "audio/bgm/Fluffing A Duck.ogg"; -} - -impl FromWorld for BgmHandles { - fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - - let paths = [Self::PATH_CREDITS, Self::PATH_GAMEPLAY]; - let map = paths - .into_iter() - .map(|path| (path.to_string(), asset_server.load(path))) - .collect(); - - Self(map) - } -} - -/// The values stored here are a `Vec>` because -/// a single sound effect can have multiple variations. -#[derive(Resource, Debug, Deref, DerefMut, Reflect)] -#[reflect(Resource)] -pub struct SfxHandles(HashMap>>); - -impl SfxHandles { - pub const PATH_BUTTON_HOVER: &'static str = "audio/sfx/button_hover.ogg"; - pub const PATH_BUTTON_PRESS: &'static str = "audio/sfx/button_press.ogg"; - pub const PATH_STEP: &'static str = "audio/sfx/step"; -} - -impl FromWorld for SfxHandles { - fn from_world(world: &mut World) -> Self { - let asset_server = world.get_resource::().unwrap(); - - let paths = [Self::PATH_BUTTON_HOVER, Self::PATH_BUTTON_PRESS]; - let mut map: HashMap<_, _> = paths - .into_iter() - .map(|path| (path.to_string(), vec![asset_server.load(path)])) - .collect(); - - // Using string parsing to strip numbered suffixes + `AssetServer::load_folder` - // is a good way to load many sound effects at once, but is not supported on - // Wasm or Android. - const STEP_VARIATIONS: u32 = 4; - let mut step_sfx = Vec::new(); - for i in 1..=STEP_VARIATIONS { - let file = format!("{key}{i}.ogg", key = Self::PATH_STEP); - step_sfx.push(asset_server.load(file)); - } - map.insert(Self::PATH_STEP.to_string(), step_sfx); - - Self(map) - } -} diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 00000000..04b170bc --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,37 @@ +use bevy::prelude::*; + +/// An organizational marker component that should be added to a spawned [`AudioBundle`] if it is in the +/// general "music" category (ex: global background music, soundtrack, etc). +/// +/// This can then be used to query for and operate on sounds in that category. For example: +/// +/// ``` +/// use bevy::prelude::*; +/// use bevy_quickstart::audio::Music; +/// +/// fn set_music_volume(sink_query: Query<&AudioSink, With>) { +/// for sink in &sink_query { +/// sink.set_volume(0.5); +/// } +/// } +/// ``` +#[derive(Component, Default)] +pub struct Music; + +/// An organizational marker component that should be added to a spawned [`AudioBundle`] if it is in the +/// general "sound effect" category (ex: footsteps, the sound of a magic spell, a door opening). +/// +/// This can then be used to query for and operate on sounds in that category. For example: +/// +/// ``` +/// use bevy::prelude::*; +/// use bevy_quickstart::audio::SoundEffect; +/// +/// fn set_sound_effect_volume(sink_query: Query<&AudioSink, With>) { +/// for sink in &sink_query { +/// sink.set_volume(0.5); +/// } +/// } +/// ``` +#[derive(Component, Default)] +pub struct SoundEffect; diff --git a/src/audio/bgm.rs b/src/audio/bgm.rs deleted file mode 100644 index 17deb292..00000000 --- a/src/audio/bgm.rs +++ /dev/null @@ -1,78 +0,0 @@ -use bevy::{ - audio::PlaybackMode, - ecs::{system::RunSystemOnce as _, world::Command}, - prelude::*, -}; - -use crate::assets::BgmHandles; - -pub(super) fn plugin(app: &mut App) { - app.register_type::(); -} - -/// Marker component for the soundtrack entity so we can find it later. -#[derive(Component, Reflect)] -#[reflect(Component)] -struct IsBgm; - -/// A custom command used to play soundtracks. -#[derive(Debug)] -enum PlayBgm { - Key(String), - Disable, -} - -impl Command for PlayBgm { - /// This command will despawn the current soundtrack, then spawn a new one - /// if necessary. - fn apply(self, world: &mut World) { - world.run_system_once_with(self, play_bgm); - } -} - -fn play_bgm( - In(config): In, - mut commands: Commands, - bgm_query: Query>, - bgm_handles: Res, -) { - for entity in bgm_query.iter() { - commands.entity(entity).despawn_recursive(); - } - - let bgm_key = match config { - PlayBgm::Key(key) => key, - PlayBgm::Disable => return, - }; - - commands.spawn(( - AudioSourceBundle { - source: bgm_handles[&bgm_key].clone_weak(), - settings: PlaybackSettings { - mode: PlaybackMode::Loop, - ..default() - }, - }, - IsBgm, - )); -} - -/// An extension trait with convenience methods for soundtrack commands. -pub trait BgmCommands { - /// Play a soundtrack, replacing the current one. - /// Soundtracks will loop. - fn play_bgm(&mut self, name: impl Into); - - /// Stop the current soundtrack. - fn stop_bgm(&mut self); -} - -impl BgmCommands for Commands<'_, '_> { - fn play_bgm(&mut self, name: impl Into) { - self.add(PlayBgm::Key(name.into())); - } - - fn stop_bgm(&mut self) { - self.add(PlayBgm::Disable); - } -} diff --git a/src/audio/mod.rs b/src/audio/mod.rs deleted file mode 100644 index d2bf1c74..00000000 --- a/src/audio/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Functionality relating to playing audio in the game. - -pub mod bgm; -pub mod sfx; - -use bevy::prelude::*; - -pub fn plugin(app: &mut App) { - app.add_plugins((sfx::plugin, bgm::plugin)); -} diff --git a/src/audio/sfx.rs b/src/audio/sfx.rs deleted file mode 100644 index 5a07a1e7..00000000 --- a/src/audio/sfx.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Functions and types for playing sound effects in the game. -//! The main point of interest here is the extension trait `SfxCommands` -//! and its `play_sfx` method. -//! -//! This method accepts any type which implements `Into`. -//! This allows us to pass in `&str`, `String`, or a custom type that can be -//! converted to a string. -//! -//! These custom types can be useful for defining enums that represent specific -//! sound effects. Generally speaking, enum values should be used to represent -//! one-off or special-cased sound effects, while string keys are a better fit -//! for sound effects corresponding to objects loaded from a data file. -//! -//! This pattern is taken from the [Bevy example for sound effects](https://github.com/bevyengine/bevy/pull/14554). - -use bevy::{ecs::world::Command, prelude::*}; -use rand::seq::SliceRandom; - -use crate::assets::SfxHandles; - -pub(super) fn plugin(_app: &mut App) { - // No setup required for this plugin. - // It's still good to have a function here so that we can add some setup - // later if needed. -} - -impl SfxHandles { - /// Plays a random sound effect matching the given name. - /// - /// When defining the settings for this method, we almost always want to use - /// [`PlaybackMode::Despawn`](bevy::audio::PlaybackMode). Every time a - /// sound effect is played, a new entity is generated. Once the sound effect - /// is complete, the entity should be cleaned up, rather than looping or - /// sitting around uselessly. - fn play(&mut self, name: impl AsRef, world: &mut World, settings: PlaybackSettings) { - let name = name.as_ref(); - if let Some(sfx_list) = self.get_mut(name) { - // If we need precise control over the randomization order of our sound effects, - // we can store the RNG as a resource and modify these functions to take it as - // an argument. - let rng = &mut rand::thread_rng(); - let random_sfx = sfx_list.choose(rng).unwrap(); - - // We don't need a (slightly) more expensive strong handle here (which is used - // to keep an asset loaded in memory), because a copy is always - // stored in the `SfxHandles` resource. - let source = random_sfx.clone_weak(); - - world.spawn(AudioBundle { source, settings }); - } else { - warn!("Sound effect not found: {name}"); - } - } -} - -/// A custom command used to play sound effects. -struct PlaySfx { - name: String, - settings: PlaybackSettings, -} - -impl Command for PlaySfx { - fn apply(self, world: &mut World) { - // If you need more complex behavior, use `world.run_system_once_with`, - // as demonstrated with `PlayBgm`. - world.resource_scope(|world, mut sfx: Mut| { - sfx.play(self.name, world, self.settings); - }); - } -} - -/// An extension trait with convenience methods for sound effect commands. -pub trait SfxCommands { - fn play_sfx_with_settings(&mut self, name: impl Into, settings: PlaybackSettings); - - fn play_sfx(&mut self, name: impl Into) { - self.play_sfx_with_settings(name, PlaybackSettings::DESPAWN); - } -} - -impl SfxCommands for Commands<'_, '_> { - // By accepting an `Into` here, we can be flexible about what we want to - // accept: &str literals are better for prototyping and data-driven sound - // effects, but enums are nicer for special-cased effects - fn play_sfx_with_settings(&mut self, name: impl Into, settings: PlaybackSettings) { - let name = name.into(); - self.add(PlaySfx { name, settings }); - } -} diff --git a/src/demo/animation.rs b/src/demo/animation.rs index ea52e99b..7894474c 100644 --- a/src/demo/animation.rs +++ b/src/demo/animation.rs @@ -4,12 +4,12 @@ //! - [Sprite animation](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs) //! - [Timers](https://github.com/bevyengine/bevy/blob/latest/examples/time/timers.rs) -use std::time::Duration; - use bevy::prelude::*; +use rand::prelude::*; +use std::time::Duration; use super::movement::MovementController; -use crate::{assets::SfxHandles, audio::sfx::SfxCommands as _, AppSet}; +use crate::{audio::SoundEffect, demo::player::PlayerAssets, AppSet}; pub(super) fn plugin(app: &mut App) { // Animate and play sound effects based on controls. @@ -24,6 +24,7 @@ pub(super) fn plugin(app: &mut App) { trigger_step_sfx, ) .chain() + .run_if(resource_exists::) .in_set(AppSet::Update), ), ); @@ -66,13 +67,25 @@ fn update_animation_atlas(mut query: Query<(&PlayerAnimation, &mut TextureAtlas) /// If the player is moving, play a step sound effect synchronized with the /// animation. -fn trigger_step_sfx(mut commands: Commands, mut step_query: Query<&PlayerAnimation>) { +fn trigger_step_sfx( + mut commands: Commands, + player_assets: Res, + mut step_query: Query<&PlayerAnimation>, +) { for animation in &mut step_query { if animation.state == PlayerAnimationState::Walking && animation.changed() && (animation.frame == 2 || animation.frame == 5) { - commands.play_sfx(SfxHandles::PATH_STEP); + let rng = &mut rand::thread_rng(); + let random_step = player_assets.steps.choose(rng).unwrap(); + commands.spawn(( + AudioBundle { + source: random_step.clone(), + settings: PlaybackSettings::DESPAWN, + }, + SoundEffect, + )); } } } diff --git a/src/demo/mod.rs b/src/demo/mod.rs index 1ded31d0..5128fe49 100644 --- a/src/demo/mod.rs +++ b/src/demo/mod.rs @@ -8,7 +8,7 @@ use bevy::prelude::*; mod animation; pub mod level; mod movement; -mod player; +pub mod player; pub(super) fn plugin(app: &mut App) { app.add_plugins(( diff --git a/src/demo/player.rs b/src/demo/player.rs index 1d437a24..ef3ac701 100644 --- a/src/demo/player.rs +++ b/src/demo/player.rs @@ -5,10 +5,11 @@ use bevy::{ ecs::{system::RunSystemOnce as _, world::Command}, prelude::*, + render::texture::{ImageLoaderSettings, ImageSampler}, }; use crate::{ - assets::ImageHandles, + asset_tracking::LoadResource, demo::{ animation::PlayerAnimation, movement::{MovementController, ScreenWrap}, @@ -19,6 +20,7 @@ use crate::{ pub(super) fn plugin(app: &mut App) { app.register_type::(); + app.load_resource::(); // Record directional input as movement controls. app.add_systems( @@ -47,7 +49,7 @@ impl Command for SpawnPlayer { fn spawn_player( In(config): In, mut commands: Commands, - image_handles: Res, + player_assets: Res, mut texture_atlas_layouts: ResMut>, ) { // A texture atlas is a way to split one image with a grid into multiple @@ -63,7 +65,7 @@ fn spawn_player( Name::new("Player"), Player, SpriteBundle { - texture: image_handles[ImageHandles::PATH_DUCKY].clone_weak(), + texture: player_assets.ducky.clone(), transform: Transform::from_scale(Vec2::splat(8.0).extend(1.0)), ..Default::default() }, @@ -110,3 +112,42 @@ fn record_player_directional_input( controller.intent = intent; } } + +#[derive(Resource, Asset, Reflect, Clone)] +pub struct PlayerAssets { + // This #[dependency] attribute marks the field as a dependency of the Asset. + // This means that it will not finish loading until the labeled asset is also loaded. + #[dependency] + pub ducky: Handle, + #[dependency] + pub steps: Vec>, +} + +impl PlayerAssets { + pub const PATH_DUCKY: &'static str = "images/ducky.png"; + pub const PATH_STEP_1: &'static str = "audio/sfx/step1.ogg"; + pub const PATH_STEP_2: &'static str = "audio/sfx/step2.ogg"; + pub const PATH_STEP_3: &'static str = "audio/sfx/step3.ogg"; + pub const PATH_STEP_4: &'static str = "audio/sfx/step4.ogg"; +} + +impl FromWorld for PlayerAssets { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + ducky: assets.load_with_settings( + PlayerAssets::PATH_DUCKY, + |settings: &mut ImageLoaderSettings| { + // Use `nearest` image sampling to preserve the pixel art style. + settings.sampler = ImageSampler::nearest(); + }, + ), + steps: vec![ + assets.load(PlayerAssets::PATH_STEP_1), + assets.load(PlayerAssets::PATH_STEP_2), + assets.load(PlayerAssets::PATH_STEP_3), + assets.load(PlayerAssets::PATH_STEP_4), + ], + } + } +} diff --git a/src/lib.rs b/src/lib.rs index ce72811b..354edb68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ -mod assets; -mod audio; +mod asset_tracking; +pub mod audio; mod demo; #[cfg(feature = "dev")] mod dev_tools; @@ -56,11 +56,10 @@ impl Plugin for AppPlugin { // Add other plugins. app.add_plugins(( + asset_tracking::plugin, demo::plugin, screens::plugin, theme::plugin, - assets::plugin, - audio::plugin, )); // Enable dev tools for dev builds. diff --git a/src/screens/credits.rs b/src/screens/credits.rs index acf4f5e1..3ed2e955 100644 --- a/src/screens/credits.rs +++ b/src/screens/credits.rs @@ -3,14 +3,32 @@ use bevy::prelude::*; use super::Screen; -use crate::{assets::BgmHandles, audio::bgm::BgmCommands as _, theme::prelude::*}; +use crate::{asset_tracking::LoadResource, audio::Music, theme::prelude::*}; pub(super) fn plugin(app: &mut App) { + app.load_resource::(); app.add_systems(OnEnter(Screen::Credits), show_credits_screen); app.add_systems(OnExit(Screen::Credits), stop_bgm); } -fn show_credits_screen(mut commands: Commands) { +#[derive(Resource, Asset, Reflect, Clone)] +pub struct CreditsMusic { + #[dependency] + music: Handle, + entity: Option, +} + +impl FromWorld for CreditsMusic { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + music: assets.load("audio/bgm/Monkeys Spinning Monkeys.ogg"), + entity: None, + } + } +} + +fn show_credits_screen(mut commands: Commands, mut music: ResMut) { commands .ui_root() .insert(StateScoped(Screen::Credits)) @@ -28,11 +46,23 @@ fn show_credits_screen(mut commands: Commands) { children.button("Back").observe(enter_title); }); - commands.play_bgm(BgmHandles::PATH_CREDITS); + music.entity = Some( + commands + .spawn(( + AudioBundle { + source: music.music.clone(), + settings: PlaybackSettings::LOOP, + }, + Music, + )) + .id(), + ); } -fn stop_bgm(mut commands: Commands) { - commands.stop_bgm(); +fn stop_bgm(mut commands: Commands, mut music: ResMut) { + if let Some(entity) = music.entity.take() { + commands.entity(entity).despawn_recursive(); + } } fn enter_title(_trigger: Trigger, mut next_screen: ResMut>) { diff --git a/src/screens/loading.rs b/src/screens/loading.rs index 75aa4d2e..29bc45be 100644 --- a/src/screens/loading.rs +++ b/src/screens/loading.rs @@ -1,12 +1,13 @@ //! A loading screen during which game assets are loaded. //! This reduces stuttering, especially for audio on WASM. -use bevy::{prelude::*, utils::HashMap}; +use bevy::prelude::*; use super::Screen; use crate::{ - assets::{BgmHandles, ImageHandles, SfxHandles}, - theme::prelude::*, + demo::player::PlayerAssets, + screens::{credits::CreditsMusic, playing::GameplayMusic}, + theme::{interaction::InteractionAssets, prelude::*}, }; pub(super) fn plugin(app: &mut App) { @@ -30,37 +31,17 @@ fn show_loading_screen(mut commands: Commands) { } fn all_assets_loaded( - asset_server: Res, - image_handles: Res, - sfx_handles: Res, - bgm_handles: Res, + player_assets: Option>, + interaction_assets: Option>, + credits_music: Option>, + gameplay_music: Option>, ) -> bool { - image_handles.all_loaded(&asset_server) - && sfx_handles.all_loaded(&asset_server) - && bgm_handles.all_loaded(&asset_server) + player_assets.is_some() + && interaction_assets.is_some() + && credits_music.is_some() + && gameplay_music.is_some() } fn continue_to_title(mut next_screen: ResMut>) { next_screen.set(Screen::Title); } - -/// An extension trait to check if all the assets in an asset collection are -/// loaded. -trait AllLoaded { - fn all_loaded(&self, asset_server: &AssetServer) -> bool; -} - -impl AllLoaded for HashMap> { - fn all_loaded(&self, asset_server: &AssetServer) -> bool { - self.values() - .all(|x| asset_server.is_loaded_with_dependencies(x)) - } -} - -impl AllLoaded for HashMap>> { - fn all_loaded(&self, asset_server: &AssetServer) -> bool { - self.values() - .flatten() - .all(|x| asset_server.is_loaded_with_dependencies(x)) - } -} diff --git a/src/screens/mod.rs b/src/screens/mod.rs index c5e4b58f..a63828b9 100644 --- a/src/screens/mod.rs +++ b/src/screens/mod.rs @@ -1,8 +1,8 @@ //! The game's main screen states and transitions between them. -mod credits; +pub mod credits; mod loading; -mod playing; +pub mod playing; mod splash; mod title; diff --git a/src/screens/playing.rs b/src/screens/playing.rs index 0afd7b8a..2cef3324 100644 --- a/src/screens/playing.rs +++ b/src/screens/playing.rs @@ -3,9 +3,10 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*}; use super::Screen; -use crate::{assets::BgmHandles, audio::bgm::BgmCommands as _, demo::level::SpawnLevel}; +use crate::{asset_tracking::LoadResource, audio::Music, demo::level::SpawnLevel}; pub(super) fn plugin(app: &mut App) { + app.load_resource::(); app.add_systems(OnEnter(Screen::Playing), spawn_level); app.add_systems(OnExit(Screen::Playing), stop_bgm); @@ -16,13 +17,42 @@ pub(super) fn plugin(app: &mut App) { ); } -fn spawn_level(mut commands: Commands) { +#[derive(Resource, Asset, Reflect, Clone)] +pub struct GameplayMusic { + #[dependency] + handle: Handle, + entity: Option, +} + +impl FromWorld for GameplayMusic { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + handle: assets.load("audio/bgm/Fluffing A Duck.ogg"), + entity: None, + } + } +} + +fn spawn_level(mut commands: Commands, mut music: ResMut) { commands.add(SpawnLevel); - commands.play_bgm(BgmHandles::PATH_GAMEPLAY); + music.entity = Some( + commands + .spawn(( + AudioBundle { + source: music.handle.clone(), + settings: PlaybackSettings::LOOP, + }, + Music, + )) + .id(), + ); } -fn stop_bgm(mut commands: Commands) { - commands.stop_bgm(); +fn stop_bgm(mut commands: Commands, mut music: ResMut) { + if let Some(entity) = music.entity.take() { + commands.entity(entity).despawn_recursive(); + } } fn return_to_title_screen(mut next_screen: ResMut>) { diff --git a/src/theme/interaction.rs b/src/theme/interaction.rs index ff5a7018..a51890a8 100644 --- a/src/theme/interaction.rs +++ b/src/theme/interaction.rs @@ -1,16 +1,21 @@ -use bevy::{ecs::system::SystemId, prelude::*}; - -use crate::{assets::SfxHandles, audio::sfx::SfxCommands as _}; +use crate::{asset_tracking::LoadResource, audio::SoundEffect}; +use bevy::{ + ecs::{system::SystemId, world::Command}, + prelude::*, +}; +use std::{collections::VecDeque, marker::PhantomData}; pub(super) fn plugin(app: &mut App) { app.register_type::(); + app.load_resource::(); app.add_systems( Update, ( trigger_on_press, apply_interaction_palette, trigger_interaction_sfx, - ), + ) + .run_if(resource_exists::), ); } @@ -57,15 +62,46 @@ fn apply_interaction_palette( } } +#[derive(Resource, Asset, Reflect, Clone)] +pub struct InteractionAssets { + #[dependency] + hover: Handle, + #[dependency] + press: Handle, +} + +impl InteractionAssets { + pub const PATH_BUTTON_HOVER: &'static str = "audio/sfx/button_hover.ogg"; + pub const PATH_BUTTON_PRESS: &'static str = "audio/sfx/button_press.ogg"; +} + +impl FromWorld for InteractionAssets { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + hover: assets.load(Self::PATH_BUTTON_HOVER), + press: assets.load(Self::PATH_BUTTON_PRESS), + } + } +} + fn trigger_interaction_sfx( interaction_query: Query<&Interaction, Changed>, + interaction_assets: Res, mut commands: Commands, ) { for interaction in &interaction_query { - match interaction { - Interaction::Hovered => commands.play_sfx(SfxHandles::PATH_BUTTON_HOVER), - Interaction::Pressed => commands.play_sfx(SfxHandles::PATH_BUTTON_PRESS), - _ => (), - } + let source = match interaction { + Interaction::Hovered => interaction_assets.hover.clone(), + Interaction::Pressed => interaction_assets.press.clone(), + _ => continue, + }; + commands.spawn(( + AudioBundle { + source, + settings: PlaybackSettings::DESPAWN, + }, + SoundEffect, + )); } }