Game
I finally added sound support to Factor Y
. Both looping sounds (for music and ambience) and one time sounds (for actions) are supported. There’s also support for ‘visibility’ checks. Only Structure
s that are visible will cause sounds to be emitted.
For now I added freely available sounds to some actions, Structure
s and views. I’ll certainly add more sounds in the future and might replace the ones you’re hearing in the clip below. I’m quite happy with some of them already, tho.
Currently sounds aren’t directional and individual volumes can’t be changed. Also for example having multiple Assembler
s present won’t increase the volume.
Code
For sounds I’m handling looped and ‘once’ sounds separately:
pub enum Sound {
Loop(Loop),
Once(Once),
}
...
pub enum Loop {
Planet,
Space,
Assembler,
}
...
pub enum Once {
PlaceStructure,
RemoveStructure,
HotbarClick,
}
For every sound the audio data is directly available embedded in the binary via include_bytes!
:
pub fn all_sounds() -> &'static [(Sound, &'static [u8])] {
...
}
To potentially switch the used audio library in the future or depending on the platform I introduced a slim trait for generic audio players:
pub trait AudioPlayer {
fn play(&self, x: Once) -> AudioPlayerResult<()>;
fn start(&self, x: Loop) -> AudioPlayerResult<()>;
fn stop(&self, x: Loop) -> AudioPlayerResult<()>;
}
with rodio the AudioPlayer
implementation is less than 150
LOC.
For managing the playback of sounds I introduced SoundScape
.
It ensures that newly added Once
sounds are played and Loop
sounds are correctly started and stopped depending on presence.
pub struct SoundScape {
player: Box<dyn AudioPlayer>,
onces: Cell<BTreeSet<Once>>,
loops: Cell<BTreeSet<Loop>>,
loops_prev: Cell<BTreeSet<Loop>>,
}
impl SoundScape {
pub fn new(player: Box<dyn AudioPlayer>) -> Self {
Self {
player,
onces: Default::default(),
loops: Default::default(),
loops_prev: Default::default(),
}
}
pub fn register<T>(&self, x: T)
where
T: Into<Sound>,
{
let sound = x.into();
match sound {
Sound::Once(x) => {
let mut onces = self.onces.take();
onces.insert(x);
self.onces.set(onces);
}
Sound::Loop(x) => {
let mut loops = self.loops.take();
loops.insert(x);
self.loops.set(loops);
}
};
}
pub fn render(&self) -> AudioPlayerResult<()> {
let mut any_failed = false;
// play once sounds once
{
let mut onces = self.onces.take();
for once in onces.iter() {
self.player
.play(*once)
.unwrap_or_else(|_| any_failed = true);
}
onces.clear();
self.onces.set(onces);
}
{
let mut loops = self.loops.take();
let mut loops_prev = self.loops_prev.take();
// stop sounds that aren't part of loops anymore
for sound in loops_prev.iter() {
if !loops.contains(sound) {
self.player
.stop(*sound)
.unwrap_or_else(|_| any_failed = true);
}
}
// start sounds that are newly added
for sound in loops.iter() {
if !loops_prev.contains(sound) {
self.player
.start(*sound)
.unwrap_or_else(|_| any_failed = true);
}
}
std::mem::swap(&mut loops, &mut loops_prev);
loops.clear();
self.loops.set(loops);
self.loops_prev.set(loops_prev);
}
if any_failed {
Err(AudioPlayerError::Output)
} else {
Ok(())
}
}
}
The core game logic uses SoundScape
to play sounds and maps certain game events to Once
sounds.
...
fn play_sound<T>(&self, sound: T)
where
T: Into<Sound>,
{
self.sound_scape.register(sound)
}
...
fn sound_of_structure_action(action: &StructureAction) -> Option<Once> {
match action {
StructureAction::WantAdd(_) => Some(Once::PlaceStructure),
StructureAction::Remove(_) => Some(Once::RemoveStructure),
_ => None,
}
}
...
fn play_sound_of_event(&self, event: &ClientEvent) {
type CE = ClientEvent;
match event {
CE::Player(x) => match x {
PlayerEvent::Local(x) => match x {
PlayerLocalEvent::StructureBP(x) => {
Self::sound_of_structure_action(x).map(|x| self.play_sound(x));
}
_ => (),
},
...
},
...
_ => (),
}
}
...
if handled_by_hotbar {
self.play_sound(Once::HotbarClick)
}
Every ‘frame’ SoundScape::render
is then called to ensure the sounds are played.
To track the presence of Structure
s that emit sounds I added:
(for now only supporting Assembler
s)
pub struct StructureSoundScene {
// counts of ACTIVE / hearable structures
pub n_assemblers: usize,
}
Anything that previously returned render Node
s of Structure
s now also returns that sound scene:
impl PlanetUI {
pub fn render_data<'a>(&self, ...) -> (Node, StructureSoundScene) {
...
The main game logic then plays sounds accordingly:
...
fn play_sound_scene(&self, sss: &StructureSoundScene) {
if sss.n_assemblers > 0 {
self.play_sound(Loop::Assembler)
}
}
In the future I’d like to adjust the volume depending on count / zoom etc.