generated from NiklasEi/bevy_game_template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
619 additions
and
471 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[package] | ||
name = "turtle_time" | ||
version = "0.9.0" | ||
version = "0.10.0" | ||
publish = false | ||
authors = ["Mike Eder <[email protected]>"] | ||
edition = "2021" | ||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
File renamed without changes.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,186 @@ | ||
use crate::actions::{set_movement_actions, Actions}; | ||
use crate::loading::AudioAssets; | ||
use crate::AppState; | ||
use std::time::Duration; | ||
|
||
use crate::player::components::Expired; | ||
use crate::{AppState, FPS}; | ||
use bevy::core::FrameCount; | ||
use bevy::prelude::*; | ||
use bevy::utils::{HashMap, HashSet}; | ||
use bevy_ggrs::Rollback; | ||
use bevy_kira_audio::prelude::*; | ||
use bevy_kira_audio::{Audio, AudioPlugin}; | ||
|
||
const MAX_SOUND_DELAY_FRAMES: u32 = 10; | ||
|
||
pub struct InternalAudioPlugin; | ||
|
||
// This plugin is responsible to control the game audio | ||
impl Plugin for InternalAudioPlugin { | ||
fn build(&self, app: &mut App) { | ||
app.add_plugin(AudioPlugin) | ||
.add_system(start_audio.in_schedule(OnEnter(AppState::MenuMain))) | ||
.add_system( | ||
control_flying_sound | ||
.after(set_movement_actions) | ||
.in_set(OnUpdate(AppState::RoundOnline)), | ||
) | ||
.add_system( | ||
control_flying_sound | ||
.after(set_movement_actions) | ||
.in_set(OnUpdate(AppState::RoundLocal)), | ||
); | ||
.init_resource::<PlaybackStates>() | ||
.add_system(init_audio.in_schedule(OnExit(AppState::Loading))) | ||
.add_system(sync_rollback_sounds) | ||
.add_system(remove_finished_sounds) | ||
.add_system(update_looped_sounds); | ||
} | ||
} | ||
|
||
#[derive(Resource)] | ||
struct FlyingAudio(Handle<AudioInstance>); | ||
/// Rollback Audio | ||
/// https://johanhelsing.studio/posts/cargo-space-devlog-4 | ||
#[derive(Component)] | ||
pub struct RollbackSound { | ||
/// the actual sound effect to play | ||
pub clip: Handle<AudioSource>, | ||
/// when the sound effect should have started playing | ||
pub start_frame: u32, | ||
/// differentiates several unique instances of the same sound playing at once. | ||
/// for example, two players shooting at the same time | ||
pub sub_key: u32, | ||
} | ||
|
||
impl RollbackSound { | ||
pub fn key(&self) -> (Handle<AudioSource>, usize) { | ||
(self.clip.clone(), self.sub_key as usize) | ||
} | ||
} | ||
|
||
#[derive(Bundle)] | ||
pub struct RollbackSoundBundle { | ||
pub sound: RollbackSound, | ||
/// an id to make sure that the entity will be removed in case of rollbacks | ||
pub rollback: Rollback, | ||
} | ||
|
||
fn start_audio(mut commands: Commands, audio_assets: Res<AudioAssets>, audio: Res<Audio>) { | ||
audio.pause(); | ||
let handle = audio | ||
.play(audio_assets.flying.clone()) | ||
.looped() | ||
.with_volume(0.3) | ||
.handle(); | ||
commands.insert_resource(FlyingAudio(handle)); | ||
fn init_audio(audio: Res<Audio>) { | ||
// todo: volume control in options | ||
let volume = 0.3; | ||
audio.set_volume(volume); | ||
} | ||
|
||
fn control_flying_sound( | ||
actions: Res<Actions>, | ||
audio: Res<FlyingAudio>, | ||
/// Actual playback states, managed by sync_rollback_sounds system below. | ||
#[derive(Resource, Reflect, Default)] | ||
struct PlaybackStates { | ||
playing: HashMap<(Handle<AudioSource>, usize), Handle<AudioInstance>>, | ||
} | ||
|
||
fn sync_rollback_sounds( | ||
mut current_state: ResMut<PlaybackStates>, | ||
mut audio_instances: ResMut<Assets<AudioInstance>>, | ||
desired_query: Query<&RollbackSound>, | ||
audio: Res<Audio>, | ||
frame: Res<FrameCount>, | ||
) { | ||
if let Some(instance) = audio_instances.get_mut(&audio.0) { | ||
match instance.state() { | ||
PlaybackState::Paused { .. } => { | ||
if actions.player_movement.is_some() { | ||
instance.resume(AudioTween::default()); | ||
// remove any finished sound effects | ||
current_state.playing.retain(|_, handle| { | ||
!matches!( | ||
audio_instances.state(handle), | ||
PlaybackState::Stopped | PlaybackState::Stopping { .. } | ||
) | ||
}); | ||
|
||
let mut live = HashSet::new(); | ||
|
||
// start/update sound effects | ||
for rollback_sound in desired_query.iter() { | ||
let key = rollback_sound.key(); | ||
if current_state.playing.contains_key(&key) { | ||
// already playing | ||
// todo: compare frames and seek if time critical | ||
} else { | ||
let frames_late = frame.0 - rollback_sound.start_frame; | ||
// ignore any sound effects that are *really* late | ||
// todo: make configurable | ||
if frames_late <= MAX_SOUND_DELAY_FRAMES { | ||
if frames_late > 0 { | ||
// todo: seek if time critical | ||
info!( | ||
"playing sound effect {} frames late", | ||
frame.0 - rollback_sound.start_frame | ||
); | ||
} | ||
let instance_handle = audio.play(rollback_sound.clip.clone()).handle(); | ||
current_state | ||
.playing | ||
.insert(key.to_owned(), instance_handle); | ||
} | ||
PlaybackState::Playing { .. } => { | ||
if actions.player_movement.is_none() { | ||
instance.pause(AudioTween::default()); | ||
} | ||
} | ||
|
||
// we keep track of `RollbackSound`s still existing, | ||
// so we can remove any sound effects not present later | ||
live.insert(rollback_sound.key().to_owned()); | ||
} | ||
|
||
// stop interrupted sound effects | ||
for (_, instance_handle) in current_state | ||
.playing | ||
.drain_filter(|key, _| !live.contains(key)) | ||
{ | ||
if let Some(instance) = audio_instances.get_mut(&instance_handle) { | ||
// todo: add config to use linear tweening, stop or keep playing as appropriate | ||
// instance.stop(default()); // immediate | ||
instance.stop(AudioTween::linear(Duration::from_millis(100))); | ||
} else { | ||
error!("Audio instance not found"); | ||
} | ||
} | ||
} | ||
|
||
fn remove_finished_sounds( | ||
frame: Res<FrameCount>, | ||
query: Query<(Entity, &RollbackSound)>, | ||
mut commands: Commands, | ||
audio_sources: Res<Assets<AudioSource>>, | ||
) { | ||
for (entity, sfx) in query.iter() { | ||
// perf: cache frames_to_play instead of checking audio_sources every frame? | ||
if let Some(audio_source) = audio_sources.get(&sfx.clip) { | ||
let frames_played = frame.0 - sfx.start_frame; | ||
let seconds_to_play = audio_source.sound.duration().as_secs_f64(); | ||
let frames_to_play = (seconds_to_play * FPS as f64) as u32; | ||
|
||
if frames_played >= frames_to_play { | ||
commands.entity(entity).insert(Expired); | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
|
||
#[derive(Component, Default, Reflect)] | ||
#[reflect(Component)] | ||
pub struct FadedLoopSound { | ||
/// The actual sound playing, if any | ||
pub audio_instance: Option<Handle<AudioInstance>>, | ||
/// The sound to play | ||
pub clip: Handle<AudioSource>, | ||
/// number of seconds to fade in | ||
pub fade_in: f32, | ||
/// number of seconds to fade out | ||
pub fade_out: f32, | ||
/// whether the sound effect should be playing or not | ||
pub should_play: bool, | ||
} | ||
|
||
fn update_looped_sounds( | ||
mut sounds: Query<&mut FadedLoopSound>, | ||
mut audio_instances: ResMut<Assets<AudioInstance>>, | ||
audio: Res<Audio>, | ||
) { | ||
for mut sound in sounds.iter_mut() { | ||
if sound.should_play { | ||
if sound.audio_instance.is_none() { | ||
sound.audio_instance = Some( | ||
audio | ||
.play(sound.clip.clone()) | ||
.looped() | ||
.linear_fade_in(Duration::from_secs_f32(sound.fade_in)) | ||
.handle(), | ||
); | ||
} | ||
} else if let Some(instance_handle) = sound.audio_instance.take() { | ||
if let Some(instance) = audio_instances.get_mut(&instance_handle) { | ||
instance.stop(AudioTween::linear(Duration::from_secs_f32(sound.fade_out))); | ||
} | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.