amethyst / evoli

An ecosystem-simulation game made with Amethyst
https://community.amethyst.rs/t/evoli-introduction/770
Other
217 stars 33 forks source link

Controllable audio system #81

Open QbieShay opened 5 years ago

QbieShay commented 5 years ago

At the moment, the system used by evoli to play audio is amethyst's AudioBundle/DjSystem.

Changing the music that is being played is currently not possible, a new, controllable system is required.

I'm not entirely sure what would be a good implementation for this.

Related to #76 ( and what's currently blocking me on #80 :< )

Sorry in advance if i mix up some concepts, I'm new!

azriel91 commented 5 years ago

This should help:

  1. Have a system (let's call it BgmSystem) which subscribes to day / night cycle change events.

    #[derive(Debug)]
    pub enum DayCycleEvent {
        DayBegin,
        NightBegin,
    }
    
    type BgmSystemData<'s> = (
        Read<'s, EventChannel<DayCycleEvent>>,
        // ..
    );
  2. The BgmSystem needs to be able to play sounds.

    Because we need to stop the sound when the day/night cycle changes, we need to hold onto a Sink:

    use std::io::Cursor;
    
    use rodio;
    
    let device = rodio::default_output_device().expect("No default output device");
    let source = music.musics.get("day/night soundtrack").expect("not found").clone();
    let reader = Cursor::new(source);
    let sink = rodio::play_once(device, reader);
    
    // On change events, we can go:
    sink.stop();

    Note: Amethyst's amethyst::audio::Output.play_once(..) does not return you the Sink, so you can't use that as is -- you can if this is changed, I think it's a good idea.

  3. We need to stop the sound on an event, so we'll insert it into the world.

    type BgmSystemData<'s> = (
        Read<'s, EventChannel<DayCycleEvent>>,
        Read<'s, Music>,
        // Inserted by `AudioSystem.setup()`, so you need that in your dispatcher.
        Read<'s, Option<Device>>,
        Write<'s, Option<Sink>>,
    );
    
    impl System<'s> for BgmSystem {
        type SystemData = BgmSystemData<'s>;
    
        fn run(&mut self, (channel, music, device, sink_resource): Self::SystemData) {
            // On event:
            // Play soundtrack.
            let source = music.musics.get("day/night soundtrack").expect("not found").clone();
            let reader = Cursor::new(source);
            let sink = rodio::play_once(device, reader);
    
            *sink_resource = Some(sink);
        }
    }
  4. rodio's Sink type does not allow you to re-use a sink after invoking sink.stop(). So to switch audio, we need a new Sink every time.

    impl System<'s> for BgmSystem {
        type SystemData = BgmSystemData<'s>;
    
        fn run(&mut self, (channel, music, device, sink_resource): Self::SystemData) {
            // On event:
            // Stop previous soundtrack if any.
            if let Some(sink) = *sink_resource {
                sink.stop();
            }
    
            // Play soundtrack.
            // ..
        }
    }
  5. Listen to events:

    #[derive(Debug, Default)]
    pub struct BgmSystem {
        /// Reader ID for the `DayCycleEvent` event channel.
        day_cycle_event_rid: Option<ReaderId<DayCycleEvent>>,
    }
    
    impl System<'s> for BgmSystem {
        type SystemData = BgmSystemData<'s>;
    
        fn run(&mut self, (channel, music, device, sink_resource): Self::SystemData) {
            channel
                .read(
                    self.day_cycle_event_rid
                        .as_mut()
                        .expect("Expected reader ID to exist for HitDetectionSystem."),
                )
                .for_each(|ev| {
                    // Stop previous soundtrack if any.
                    if let Some(sink) = *sink_resource {
                        sink.stop();
                    }
    
                    // Play soundtrack.
                    let source = match ev {
                        DayCycleEvent::DayBegin => "day.wav",
                        DayCycleEvent::NightBegin => "night.wav",
                    }
                    let source = music.musics.get().expect("soundtrack not loaded.").clone();
                    // ..
                });
        }
    
        fn setup(&mut self, res: &mut Resources) {
            Self::SystemData::setup(res);
            self.day_cycle_event_rid = Some(
                res.fetch_mut::<EventChannel<DayCycleEvent>>()
                    .register_reader(),
            );
        }
    }
  6. When the day / night cycle changes, send a new DayCycleEvent.

    // In `DayNightCycle`, when night time happens:
    day_cycle_event_channel.write(DayCycleEvent::NightBegin);
  7. When the game starts, we need to send the first event to start the first soundtrack.

    // In `SomethingState.on_start()`
    day_cycle_event_channel.write(DayCycleEvent::DayBegin);
QbieShay commented 5 years ago

Wow that's a lot of information! Thank you so much! I'll try to process and understand all :) in the meantime, if this becomes urgent, please feel free to implement it, I don't want to hold back anyone