2011-02-21

Game Sounds Using HTML5 and RX-JS

With HTML5 and Javascript, it's pretty easy to play sounds in the browser. And when you're using Reactive Extensions for Javascript, it's also pretty easy to "bind" sound effects to events. I created a simple "sound engine" to my Worzone game experiment and used RX to bind sounds to events. The source code is of course available in GitHub at https://github.com/raimohanska/worzone and you can have a quick demo at http://juhajasatu.com/worzone/. Just click on the "sound" checkbox before pressing any key to hear the stuff you're looking for.


Anyway, I'll briefly show you the code here too. Let's start with the "sound engine", which looks like this:


function Audio() {   
  var on = false

  var sounds = {}


  function loadSound(soundName) {
    var audioElement = document.createElement('audio')
    audioElement
      .setAttribute('src', "audio/" + soundName + ".ogg")
    return audioElement
  }


  function getSound(soundName) {
    if (!sounds[soundName]) {
      sounds[soundName] = loadSound(soundName)
    }                                         
    return sounds[soundName]          
  }              
  function play(soundName) {   
    if (on) getSound(soundName).play()
  }
  return {
    // String -> (Unit -> Unit)
    playSound : function(soundName) { 
                  return function() { play(soundName) }
                },
    // Unit -> Unit
    toggle : function() { on = !on; }
  } 
}


The Audio object exposes two methods, one of which is used to toggle sound on/off and the other returning a function that will play a given sound when invoked. The comments above the methods use the Haskell syntax for describing what are the input and output types of the functions. 





One funny thing about HTML5 audio is that .ogg format is supported while .mp3 is not. Some patent issues there, I guess. The reason why playSound returns a function instead of playing a sound is that this way it's easier to plug it into RX streams, as in the following piece of code which constitutes the whole of game sound related code in Worzone:



function GameSounds(messageQueue, audio) {
  function sequence(delay, count) {
    return ticker(delay)
      .Scan(1, function(counter) {return counter % count + 1} )    
  }
  sequence(500, 3)
    .SkipUntil(messageQueue.ofType("level-started"))
    .TakeUntil(messageQueue.ofType("level-finished"))
    .Repeat()
    .Subscribe( function(counter) { 
       audio.playSound("move" + counter)() 
    })
  messageQueue.ofType("start")
    .Where(function (start) { return start.object.player })
    .Select(function(start) { return start.object.player.id })
    .Subscribe(function(id) { audio.playSound("join" + id)() })    
  messageQueue.ofType("fire")
    .Subscribe(audio.playSound("fire"))
  messageQueue.ofType("hit")
    .Subscribe(audio.playSound("explosion"))
  messageQueue.ofType("level-starting")
    .Subscribe(audio.playSound("intro1"))



The messageQueue thing there is an RX Observable that I described in one of my previous postings. It will provide all game events, each containing a message property that can be used for filtering events. The ofType method actually provides an Observable containing only messages of the given type.


The actual sound effects are

  • An alternating sequence of move[1..3] sounds played each 500 milliseconds, starting when a game level is started and ending when a level ends. So, no creepy beeping when the monsters aren't moving.
  • Two sounds join[1..2] that are when player 1 or 2 starts the game.
  • Fire, explosion and intro sounds that are played when somebody fires or dies and when a new game level is about to start.
There seems to be some problem with my formatting. Extra line breaks appear. Not in the editor though. What's up?

2 comments: