Compare commits

...

2 Commits

@ -15,7 +15,6 @@ impl Plugin for GameActionsPlugin {
.bind(ZoomAction::Zoom, [Binding::Scroll]) .bind(ZoomAction::Zoom, [Binding::Scroll])
.bind(ZoomAction::ZoomIn, [Binding::Key(KeyCode::Equal)]) .bind(ZoomAction::ZoomIn, [Binding::Key(KeyCode::Equal)])
.bind(ZoomAction::ZoomOut, [Binding::Key(KeyCode::Minus)]), .bind(ZoomAction::ZoomOut, [Binding::Key(KeyCode::Minus)]),
ActionsPlugin::<Action>::new().bind(Action::Quit, [Binding::Key(KeyCode::Escape)]),
)); ));
} }
} }
@ -101,14 +100,6 @@ pub(crate) enum ZoomAction {
None, None,
} }
#[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)]
pub(crate) enum Action {
#[action_handler(Action::quit)]
Quit,
#[default]
None,
}
impl ZoomAction { impl ZoomAction {
pub(crate) fn zoom_by(factor: f32, mut projection: Query<&mut Projection>) { pub(crate) fn zoom_by(factor: f32, mut projection: Query<&mut Projection>) {
projection.iter_mut().for_each(|mut p| { projection.iter_mut().for_each(|mut p| {
@ -134,12 +125,6 @@ impl ZoomAction {
} }
} }
impl Action {
fn quit(mut writer: MessageWriter<AppExit>) {
writer.write(AppExit::Success);
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)] #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)]
pub(crate) enum AudioAction { pub(crate) enum AudioAction {
#[action_handler(AudioAction::toggle)] #[action_handler(AudioAction::toggle)]

@ -1,8 +1,4 @@
// TODO: // TODO:
// * Performance: have a limit on per-note+octave sounds
// * Use a finite set of N audio entities and scale volume instead of
// spawning a bunch of audio entities which is causing issues at >100
// cells
// * Volume slider // * Volume slider
// * Invert mute/unmute icons // * Invert mute/unmute icons
// * Only move camera when cells move out of view // * Only move camera when cells move out of view
@ -28,10 +24,16 @@ const SIM_FRAME_DURATION: f32 = 0.5;
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin { .add_plugins(
file_path: "assets".into(), DefaultPlugins
..default() .set(AssetPlugin {
})) file_path: "assets".into(),
..default()
})
.set(TaskPoolPlugin {
task_pool_options: TaskPoolOptions::with_num_threads(1),
}),
)
.add_plugins(FeathersPlugins) .add_plugins(FeathersPlugins)
.add_plugins(GameActionsPlugin) .add_plugins(GameActionsPlugin)
.init_resource::<NextCoordinates>() .init_resource::<NextCoordinates>()
@ -54,7 +56,6 @@ fn main() {
load_audio_tones, load_audio_tones,
load_materials, load_materials,
load_meshes, load_meshes,
spawn_audio_players.after(load_audio_tones),
), ),
) )
.configure_sets( .configure_sets(
@ -109,7 +110,14 @@ fn main() {
.add_systems( .add_systems(
Update, Update,
( (
control_audio_players.in_set(GameSet::React), control_audio_players
.run_if(
on_message::<Lifecycle>
.or_else(state_changed::<SimulationPlayback>)
.or_else(state_changed::<AudioPlayback>),
)
.in_set(GameSet::React),
manage_audio_players.run_if(resource_changed::<PitchMap>),
update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>), update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>),
), ),
) )
@ -120,6 +128,7 @@ fn main() {
grid_gizmo, grid_gizmo,
update_debug_info_coordinates, update_debug_info_coordinates,
( (
#[cfg(debug_assertions)]
assert_cell_uniqueness assert_cell_uniqueness
.run_if(any_with_component::<Cell>) .run_if(any_with_component::<Cell>)
.after(update_material), .after(update_material),
@ -193,7 +202,6 @@ enum AudioPlayback {
#[derive(Debug, Component, PartialEq, Default, Clone, FromTemplate)] #[derive(Debug, Component, PartialEq, Default, Clone, FromTemplate)]
#[require(Visibility, Transform)] #[require(Visibility, Transform)]
enum UiButton { enum UiButton {
Power,
Pause, Pause,
Step, Step,
Play, Play,
@ -210,14 +218,13 @@ enum UiButton {
impl UiButton { impl UiButton {
fn icon(&self) -> &'static str { fn icon(&self) -> &'static str {
match self { match self {
Self::Power => "power.png",
Self::Pause => "pause.png", Self::Pause => "pause.png",
Self::Step => "next.png", Self::Step => "next.png",
Self::Play => "forward.png", Self::Play => "forward.png",
Self::ZoomOut => "zoomOut.png", Self::ZoomOut => "zoomOut.png",
Self::ZoomIn => "zoomIn.png", Self::ZoomIn => "zoomIn.png",
Self::PlayAudio => "audioOn.png", Self::PlayAudio => "audioOff.png",
Self::MuteAudio => "audioOff.png", Self::MuteAudio => "audioOn.png",
Self::Reset => "return.png", Self::Reset => "return.png",
Self::Wipe => "trashcan.png", Self::Wipe => "trashcan.png",
Self::None => todo!(), Self::None => todo!(),
@ -483,7 +490,6 @@ fn primary_ui() -> impl Scene {
Text("Welcome to a little experiment I made: adding sound to Conways Game of Life") ThemedText, Text("Welcome to a little experiment I made: adding sound to Conways Game of Life") ThemedText,
Text("---") ThemedText, Text("---") ThemedText,
Text("The buttons above control (in order)") ThemedText, Text("The buttons above control (in order)") ThemedText,
Text("Quit the game") ThemedText,
Text("Start/Pause the simulation") ThemedText, Text("Start/Pause the simulation") ThemedText,
Text("Step the simulation") ThemedText, Text("Step the simulation") ThemedText,
Text("Reset the world state") ThemedText, Text("Reset the world state") ThemedText,
@ -530,8 +536,6 @@ fn primary_ui() -> impl Scene {
justify_content: JustifyContent::Start, justify_content: JustifyContent::Start,
} }
Children [ Children [
ui_button(UiButton::Power)
ActionSource<Action>(Action::Quit),
ui_toggle_button((UiButton::Pause, bsn! { SimulationPlayback::Pause }), (UiButton::Play, bsn! { SimulationPlayback::Run })) ui_toggle_button((UiButton::Pause, bsn! { SimulationPlayback::Pause }), (UiButton::Play, bsn! { SimulationPlayback::Run }))
ActionSource<SimAction>(SimAction::Toggle), ActionSource<SimAction>(SimAction::Toggle),
ui_button(UiButton::Step) ui_button(UiButton::Step)
@ -544,8 +548,10 @@ fn primary_ui() -> impl Scene {
ActionSource<ZoomAction>(ZoomAction::ZoomOut), ActionSource<ZoomAction>(ZoomAction::ZoomOut),
ui_button(UiButton::ZoomIn) ui_button(UiButton::ZoomIn)
ActionSource<ZoomAction>(ZoomAction::ZoomIn), ActionSource<ZoomAction>(ZoomAction::ZoomIn),
ui_toggle_button((UiButton::MuteAudio, bsn! { AudioPlayback::Mute }), (UiButton::PlayAudio, bsn! { AudioPlayback::Play })) ui_toggle_button(
ActionSource<AudioAction>(AudioAction::Toggle), (UiButton::MuteAudio, bsn! { AudioPlayback::Mute }),
(UiButton::PlayAudio, bsn! { AudioPlayback::Play })
) ActionSource<AudioAction>(AudioAction::Toggle),
] ]
] ]
} }
@ -785,6 +791,7 @@ fn is_ui_hovered(hovered: Query<&Hovered>) -> bool {
hovered.iter().any(|Hovered(h)| *h) hovered.iter().any(|Hovered(h)| *h)
} }
#[cfg(debug_assertions)]
fn assert_cell_uniqueness(q: Query<(Entity, &Coordinates), With<Cell>>) { fn assert_cell_uniqueness(q: Query<(Entity, &Coordinates), With<Cell>>) {
q.iter().for_each(|(e1, c1)| { q.iter().for_each(|(e1, c1)| {
q.iter().for_each(|(e2, c2)| { q.iter().for_each(|(e2, c2)| {
@ -893,26 +900,11 @@ enum Lifecycle {
fn new_cell( fn new_cell(
Coordinates { x, y }: Coordinates, Coordinates { x, y }: Coordinates,
cell_assets: &Res<CellAssets>, cell_assets: &Res<CellAssets>,
sinks: &Query<Entity, With<AudioSink>>,
sim_state: &Res<State<SimulationPlayback>>,
audio_state: &Res<State<AudioPlayback>>,
pitch_map: &Res<PitchMap>, pitch_map: &Res<PitchMap>,
) -> impl Scene { ) -> impl Scene {
let c = Coordinates { x, y }; let c = Coordinates { x, y };
let volume = {
if *sim_state.get() == SimulationPlayback::Run && *audio_state.get() == AudioPlayback::Play
{
let ratio = 1.0 / (sinks.count() + 1) as f32;
Volume::Linear(ratio)
} else {
Volume::SILENT
}
};
let playback_settings = PlaybackSettings::ONCE.with_volume(volume);
let cell_pitch = c.to_cell_pitch(&(*pitch_map)); let cell_pitch = c.to_cell_pitch(&(*pitch_map));
let translation = c.as_translation() + Vec3::new(0.0, 0.0, 1.0);
debug!("cell pitch: {:#?}", cell_pitch); debug!("cell pitch: {:#?}", cell_pitch);
debug!("materials: {:#?}", cell_assets.materials); debug!("materials: {:#?}", cell_assets.materials);
@ -921,8 +913,7 @@ fn new_cell(
Cell Cell
Coordinates { x, y } Coordinates { x, y }
template_value(cell_pitch.clone()) template_value(cell_pitch.clone())
template_value(playback_settings) template_value(Transform::from_translation(translation))
template_value(Transform::from_translation(c.as_translation() + Vec3::new(0.0, 0.0, 1.0)))
template_value(Mesh2dTemplate(cell_assets.mesh.clone().into())) template_value(Mesh2dTemplate(cell_assets.mesh.clone().into()))
// MeshMaterial2d added by update_material // MeshMaterial2d added by update_material
template_value(MeshMaterial2dTemplate(cell_assets.materials.get(&cell_pitch).unwrap().clone().into())) template_value(MeshMaterial2dTemplate(cell_assets.materials.get(&cell_pitch).unwrap().clone().into()))
@ -933,23 +924,13 @@ fn cell_lifecycle(
mut reader: MessageReader<Lifecycle>, mut reader: MessageReader<Lifecycle>,
mut commands: Commands, mut commands: Commands,
cell_assets: Res<CellAssets>, cell_assets: Res<CellAssets>,
query: Query<Entity, With<AudioSink>>,
sim_state: Res<State<SimulationPlayback>>,
audio_state: Res<State<AudioPlayback>>,
pitch_map: Res<PitchMap>, pitch_map: Res<PitchMap>,
) { ) {
reader.read().for_each(|msg| match msg { reader.read().for_each(|msg| match msg {
Lifecycle::Alive(c) => { Lifecycle::Alive(c) => {
debug!("creating {:?}", c); debug!("creating {:?}", c);
// TODO: Safeguard: ensure no other cell there? // TODO: Safeguard: ensure no other cell there?
commands.spawn_scene(new_cell( commands.spawn_scene(new_cell(c.clone(), &cell_assets, &pitch_map));
c.clone(),
&cell_assets,
&query,
&sim_state,
&audio_state,
&pitch_map,
));
} }
Lifecycle::Dead(e) => { Lifecycle::Dead(e) => {
commands.entity(*e).despawn(); commands.entity(*e).despawn();
@ -1258,6 +1239,18 @@ impl PitchMap {
// always ensure the notes list is sorted // always ensure the notes list is sorted
self.notes.sort(); self.notes.sort();
} }
// Return an iterator over all enabled CellPitch values
fn iter_enabled_cell_pitches(&self) -> impl Iterator<Item = CellPitch> {
self.notes.iter().flat_map(|n| {
(0..=self.octaves)
.map(|o| o as isize)
.map(|o| o - (o / 2))
.map(|o| {
CellPitch { note: n.clone(), octave: o }
})
})
}
} }
#[test] #[test]
@ -1410,69 +1403,105 @@ fn update_camera_position(mut query: Query<(&Coordinates, &mut Transform), With<
}); });
} }
// Ordering: first, once, after(load_audio_tones) // Run when PitchMap changes to ensure we have just the audio sinks we need
fn spawn_audio_players(mut commands: Commands, cell_assets: Res<CellAssets>) { fn manage_audio_players(
mut commands: Commands,
cell_assets: Res<CellAssets>,
pitch_map: Res<PitchMap>,
current: Query<(Entity, &CellPitch), With<AudioSink>>,
mut active: Local<HashSet<CellPitch>>,
) {
*active = pitch_map.iter_enabled_cell_pitches().collect();
CellPitch::iter_all().for_each(|cp| { CellPitch::iter_all().for_each(|cp| {
let h = cell_assets.pitches.get(&cp).unwrap().clone(); // The cell is active, ensure it has an audio entity
commands.spawn((AudioPlayer(h), cp, PlaybackSettings::default().with_volume(Volume::Linear(0.0)))); if active.contains(&cp) {
}); if !current.iter().any(|(_this_e, this_cp)| *this_cp == cp) {
let h = cell_assets.pitches.get(&cp).unwrap().clone();
commands.spawn((AudioPlayer(h), cp, PlaybackSettings::default().muted()));
}
}
// The cell is inactive, ensure it does not have an audio entity
else {
if let Some(e) = current
.iter()
.find_map(|(this_e, this_cp)| (*this_cp == cp).then_some(this_e))
{
commands.entity(e).despawn();
}
}
})
} }
// Ordering: React
fn control_audio_players( fn control_audio_players(
audio_state: Res<State<AudioPlayback>>, audio_state: Res<State<AudioPlayback>>,
simulation_state: Res<State<SimulationPlayback>>, simulation_state: Res<State<SimulationPlayback>>,
mut players: Query<(&mut AudioSink, &CellPitch)>, mut players: Query<(&mut AudioSink, &CellPitch)>,
cells: Query<&CellPitch, With<Cell>>, cells: Query<&CellPitch, With<Cell>>,
mut active_cells: Local<HashMap<CellPitch, usize>>,
) { ) {
fn turn_off(sink: &mut AudioSink) {
if !sink.is_paused() {
sink.pause();
}
if !sink.is_muted() {
sink.mute();
}
}
fn turn_on(sink: &mut AudioSink, vol: f32) {
// Set the volume of the sink to the proportional volume
sink.set_volume(Volume::Linear(vol));
// Play a paused sink
if sink.is_paused() {
sink.play();
}
// Unmute a muted sink
if sink.is_muted() {
sink.unmute();
}
}
match audio_state.get() { match audio_state.get() {
AudioPlayback::Play => { AudioPlayback::Play => {
match simulation_state.get() { match simulation_state.get() {
SimulationPlayback::Run => { SimulationPlayback::Run => {
let mut s: HashMap<CellPitch, usize> = HashMap::new(); // Count number of each type of cell for audio
cells.iter().for_each(|cp| { cells.iter().for_each(|cp| {
let this = s.entry(cp.clone()).or_insert(0); let this = active_cells.entry(cp.clone()).or_insert(0);
*this += 1; *this += 1;
}); });
let total: usize = s.values().sum(); // Get the total to compute per-cell ratio
let total: usize = active_cells.values().sum();
players.iter_mut().for_each(|(mut sink, cp)| { // Update each type of cell with the correct volume
match s.get(cp) { players
.iter_mut()
.for_each(|(mut sink, cp)| match active_cells.get(cp) {
Some(v) => { Some(v) => {
let new_volume: f32 = (*v as f64 / total as f64) as f32; let new_volume: f32 = (*v as f64 / total as f64) as f32;
debug!("Setting volume for {:?} to {:?}", cp, new_volume);
if sink.is_paused() { turn_on(sink.as_mut(), new_volume);
sink.play(); }
}
sink.set_volume(Volume::Linear(new_volume));
},
None => { None => {
sink.pause(); turn_off(sink.as_mut());
sink.set_volume(Volume::Linear(0.0));
} }
} });
}); active_cells.clear();
// create distribution of cell pitches }
// then go back and adjust volume based on % with this CellPitch
// if volume is 0, pause
// if volume > 0, play
},
// If the sim is paused or stepping, mute an dpause audios // If the sim is paused or stepping, mute an dpause audios
_ => { _ => {
players.iter_mut().for_each(|(mut sink, _)| { players.iter_mut().for_each(|(mut sink, _)| {
sink.pause(); turn_off(sink.as_mut());
sink.set_volume(Volume::Linear(0.0));
}); });
} }
} }
} }
AudioPlayback::Mute => { AudioPlayback::Mute => {
players.iter_mut().for_each(|(mut sink, _)| { players.iter_mut().for_each(|(mut sink, _)| {
sink.pause(); turn_off(sink.as_mut());
sink.set_volume(Volume::Linear(0.0));
}); });
}, }
} }
} }

Loading…
Cancel
Save