Friday, December 20, 2013

Adding Music to Your Game (Game Programming)

The last several posts have been all about audio in Firefox OS. I've learned a lot about what works, and what doesn't work, on my trusty little ZTE Open. Go back and read: HTML5 Audio, HTML5 Audio Followup, Son of HTML5 Audio Followup, and HTML5 Audio Triumphant. Whew! There were surprises for me and technical blind alleys, but now no one has any excuse for not adding music and sound effects to their HTML5 Firefox OS game! And this post will show you how!


What I did is to add two sound effects and one background song to the SVG Collision Detection example.  That example had an innocent ball, a hungry box, and impending doom for the ball. But the ball can jump out of the way, and the point there was to show you how SVG has built-in collision detection, but there's just enough of a game in it to justify adding music and SFX (sound effects).

Here's the game screen:


Here's the code (and you can go to the SVG Collision Detection post for details on how the non-audio code works. You may also want to refer to my previous audio posts for reasons why I chose the way I do audio in this post.

<!DOCTYPE HTML>
<html>  
  <head>
      <meta charset="utf-8"> 
    <title>
    Game Sounds
    </title>

    <script>
       
          // Global variables
      var boardWidth = 320;
      var boardHeight = 460;
      var bwPixels = boardWidth + "px";
      var bhPixels = boardHeight + "px";
        var redBallHor = 60;
      var redBallVer = 160;
      var redBallRad = 10;
      var howFast = 3;
        var blueBoxHor = 270;
      var blueBoxVer = 150;
      var audMove;
      var audJump;
      var audEnd;
      var flagJump = 0;
      var flagEnd = 0;
   
          // Namespace for SVG.
          svgNS = "http://www.w3.org/2000/svg";
     
      // Covers all bases for various browser support.
        var requestAnimationFrame =
          window.requestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.msRequestAnimationFrame; 

      // Event listeners
      // Page load event listener
      window.addEventListener("load", getLoaded, false);
      // Mouse down event listener
      window.addEventListener(
          "mousedown", redBallJump, false);
         
      // Run this when the page loads.
      function getLoaded(){

        // Make sure we are loaded.
        console.log("Page loaded!");    
       
        // Load audio.
        audMove = new Audio("move.ogg");
        audJump = new Audio("jump.ogg");
        audEnd = new Audio("end.ogg");
       
        // Set up audio event listeners.
        audMove.addEventListener("canplaythrough",
          moveAudioOK, false);
        audJump.addEventListener("canplaythrough",
          jumpAudioOK, false); 
        audEnd.addEventListener("canplaythrough",
          endAudioOK, false);        
    
        // Create SVG parent element.
        myBoard = document.createElementNS(svgNS, "svg");
        myBoard.style.setProperty("width",bwPixels);
        myBoard.style.setProperty("height",bhPixels);
        myBoard.style.setProperty("top","0px");
        myBoard.style.setProperty("left","0px");
        myBoard.style.setProperty("position","absolute");
        
        // You must append the board to the body.
        document.getElementById("pageBody").
             appendChild(myBoard);
            
        // Create blue box.
        blueBox =
          document.createElementNS(svgNS, "rect");
         
        // Width,  height, radius, and color of box.
        blueBox.x.baseVal.valueAsString =
          blueBoxHor + "px";
        blueBox.y.baseVal.valueAsString =
          blueBoxVer + "px";
        blueBox.width.baseVal.valueAsString =
          "20px";
        blueBox.height.baseVal.valueAsString =
          "20px";       
        blueBox.style.
          setProperty("fill","blue","");
         
        // Attach the box to the game board.
        myBoard.appendChild(blueBox);

        // Create red ball.
        redBall =
          document.createElementNS(svgNS, "circle");
         
        // Width,  height, radius, and color of ball.
        redBall.cx.baseVal.valueAsString =
          redBallHor + "px";
        redBall.cy.baseVal.valueAsString =
          redBallVer + "px";
        redBall.r.baseVal.valueAsString =
          redBallRad + "px";
        redBall.style.
          setProperty("fill","red","");
         
        // Attach the ball to the game board.
        myBoard.appendChild(redBall);
       
        // Create ground.
        myGround =
          document.createElementNS(svgNS, "rect");
         
        // Width,  height, radius, and color of box.
        myGround.x.baseVal.valueAsString =
          "0px";
        myGround.y.baseVal.valueAsString =
          "170px";
        myGround.width.baseVal.valueAsString =
          "320px";
        myGround.height.baseVal.valueAsString =
          "230px";       
        myGround.style.
          setProperty("fill","chocolate","");
         
        // Attach the box to the game board.
        myBoard.appendChild(myGround);

        // Start the main loop.
        doMainLoop();
      }

      // Game loop
      function doMainLoop() {
               
        // Loop within the loop.
        // Outer loop
        // Runs every second.
            loopTimer = setTimeout(function() {
       
          // Inner loop
          // Runs as fast as it can.
          animTimer = requestAnimationFrame(doMainLoop);         
      
          // Drawing code goes here
              console.log("Moving the box.");
         
              // Move the box
              moveBlueBox();
         
        }, 1000 / howFast); // Delay / how fast     
      }
           
      // Move the blue box here.
      function moveBlueBox() {
     
          // Subtract 10 from box horizontal value.
          blueBoxHor = blueBoxHor - 10;
     
      // Draw the new blue box.
      blueBox.x.baseVal.valueAsString =
          blueBoxHor + "px";
             
          // If the blue box hits the left edge, restart.
          if (blueBoxHor < 10) blueBoxHor = 270;   

          // Get bounding box of box.
      bbBox = blueBox.getBBox();
      console.log(bbBox.x);

      // Get bounding box of ball.
      bbBall = redBall.getBBox();
      console.log(bbBall.x);

      // Is there an x collision?
      if (bbBall.x + 20 == bbBox.x) {
     
        // Is there a y collision?
        if (bbBall.y == bbBox.y) {
       
          // Collision!
          blueBoxHor = 270;
         
          // Make the end sound.
          if(flagEnd == 1) {
       
            // Play the end sound.
            audEnd.play();    
          }

          alert("Bang");
        }
      }  
      }

    // Make the red ball jump up.
    function redBallJump() {

      // Calculate red ball jump and move it.
      redBallVer = redBallVer - 50;  

      // Draw the new red ball.
      redBall.cy.baseVal.valueAsString =
          redBallVer + "px"; 

      // Make the jump sound.
      if(flagJump == 1) {
       
        // Play the jump sound.
        audJump.play();    
      }
 
      console.log("Ball up.");
   
      // Make the red ball fall after one second.
      redBallTimer = setTimeout(redBallFall, 1000);     
    } 

    // Make the red ball fall down.
    function redBallFall() {
   
      // Calculate the redBox fall and move it.
      redBallVer = redBallVer + 50; 
     
      // Draw the new red box.
      redBall.cy.baseVal.valueAsString =
          redBallVer + "px";    
      console.log("Ball down.");  
    }
   
    // Process the move audio loaded event.
    function moveAudioOK() {
   
      console.log("Move sound loaded.");
     
      // Loop and play the audio.
      audMove.loop = true;
      audMove.play(); 
    }
   
    // Process the jump audio loaded event.
    function jumpAudioOK() {
   
      console.log("Jump sound loaded.");
     
      // Set the jump audio flag.
      flagJump = 1;    
    }

    // Process the end audio loaded event.
    function endAudioOK() {
   
      console.log("End sound loaded.");
     
      // Set the end audio flag.
      flagEnd = 1;      
    }

    </script>
  </head>
   
  <body id="pageBody">
  </body>

</html>


I use three OGG audio files for this example:

     move.ogg - plays a song in the background

     jump.ogg - plays a sound effect when the ball jumps

     end.ogg - plays an explosion when the box and ball collide

If you want to play along at home, you can get these song files from my page on SoundCloud.

     move.ogg - https://soundcloud.com/thulfram/move

     jump.ogg -  https://soundcloud.com/thulfram/jump

     end.ogg - https://soundcloud.com/thulfram/end

SoundCloud is simply wonderful for musicians (and me).

Adding the Music and Sound Effects to your Game Code

I've evolved a fairly simple and fool-proof way to work with HTML5 Audio in Firefox OS. Here's what I have found works.

  1. Create a global name for every different sound. This makes sure that the sound will be available in every function. (But don't tell Douglas Crockford. He hates globals). Just put in placeholders. You will attach them later.
  2. After you make sure the page is loaded (with an event listener for the page load event), create a unique audio object for every sound you want to use. Use the new Audio() command and specify the name of the song when you do so. This will load the audio file. All your audio files should be in the OGG file format.
  3. Create an event listener for every audio object and use the canplaythrough event (not the canplay event). Audio files may take a while to load. With JavaScript, you can never assume that anything will load instantly, and sometimes code will run faster than things that you load. This applies to audio files for music and image files for canvas. Remember: trust no one! Especially not things you load into a web page.
  4. The event listener you created for each audio object should point to a unique function. In that function, you can do different things. One might be to kick off a song that plays in the background constantly. But another use would be for a specific sound effect that you don't want to play right away, but play later when something happens. If this is the case, use the unique function for that audio object to set a flag. Flags also should be global (so they can be used in any function anywhere) and are initially set to 0 (zero). When the audio object is loaded in its unique function, set the flag to 1.
     
  5. Then, when you want a sound to play, for example, when the red ball jumps, you first check to see if the flag is set (equal to 1). If it is, you play the song. But maybe the song is still loading, so if the flag is not set, don't try to play it. Sometimes code runs faster than loading (memorize this: code is faster than load).
     
  6. You can mute sounds using the flag. First kill the onplaythrough event, then call the pause() method of that sound, and finally,  set the flag to 0 (zero).
A little complicated, but following these steps will make sure you can play music and sound when you want to. And you want to, because music and sound can make a good game great!

Here's the lines that were added to the original code of the SVG Collision Detection file.

Setting the Global Audio Object Placeholders

These were put in the Globals section.

      var audMove;
      var audJump;
      var audEnd;


Creating the Audio Objects

These were put in the function that is called when the page loads.

        audMove = new Audio("move.ogg");
        audJump = new Audio("jump.ogg");
        audEnd = new Audio("end.ogg");


These create a new audio element with a specific name and use a specific audio file. OGG is your friend.

Setting Up Event Listeners for Each Audio Object

Just creating an audio object doesn't guarantee it will happen instantly, or at all. You don't want your game to crash, trying to call a non-existent file you forgot to load. Here are the event listeners.

        audMove.addEventListener("canplaythrough",
          moveAudioOK, false);
        audJump.addEventListener("canplaythrough",
          jumpAudioOK, false); 
        audEnd.addEventListener("canplaythrough",
          endAudioOK, false);   
     

They create an event listener for each audio object and use the canplaythrough event. A unique function is created that will be called when the canplaythrough event is triggered.

Filling in the Functions that will be Called by the Audio Events

If you use flags, specify them first in the Globals section. Here are the two flags I used for jumping and ending the game.

      var flagJump = 0;
      var flagEnd = 0;


Here is the function that is called when the canplaythrough event is triggered on the jump audio object.

   function jumpAudioOK() {
   
      console.log("Jump sound loaded.");
     
      // Set the jump audio flag.
      flagJump = 1;    
    }


It's always a good idea to have a console.log for loading. Then you can look in the console and see if your audio actually loaded. Beats an alert every time! Next, for this sound, you only set the jump flag to 1. That's it. The end audio object is the same, with only the names changed to protect the guilty.

For the background music move audio object, you don't need no stinkin' flags, you just want to play the music. So the function looks like this:

    function moveAudioOK() {
   
      console.log("Move sound loaded.");
     
      // Loop and play the audio.
      audMove.loop = true;
      audMove.play(); 
    }


You first set the audio move object so that it loops, and then you play it! How cool is that?

Playing Sound Effects

If it's not background music, you only want to play a sound effect when some event happens, like the ball jumping or the game ending.

For the ball jumping, I just inserted these lines in the code where the ball actually jumps, right after the ball is drawn.

      // Make the jump sound.
      if(flagJump == 1) {
       
        // Play the jump sound.
        audJump.play();    
      }


Here's where the flag earns its dinner. You want to test to see if the flag has been set. If the flag is 1, you know that it is fully loaded and can play. Once you've determined this, you just ... play it!

For the explosion sound when the box and ball collide, I just added these lines right after the code determines that the ball and box have collided and the box has moved out of the way.

          if(flagEnd == 1) {
       
            // Play the end sound.
            audEnd.play();    
          }


Again, I make sure that the sound is loaded and can play, and then I play it. Boom!

That's all there is to it:
  1. Global placeholders.
  2. Create the Audio after the page is loaded.
  3. Make sure the Audio is loaded by using the canplaythrough event.
  4. Process the Audio when loaded by playing (if background music).
  5. Use flags to make sure you can play Audio at a particular time.
  6. Use OGG.
  7. Have fun!
How did I Make this Music?

If you're a programmer or artist, making music can be hard. But there are lots of people who can make music and are eager to put it in your game. Ask around on Twitter or go to a Game Jam!

Speaking of Game Jams, as I've been learning about gaming in Firefox OS, I've been seeing a lot of talk about Game Jams. There are lots of them and people take part to ee if they can make a game in 48 hours. It reminds me of science fiction conventions, small groups of people, high energy, no sleep!

So I went to Kindle and read two books:

Game Jam Survival Guide by Christer Kaitila 
 Global Game Jam - 48 hours of Persistence, Programming, and Pizza at Scottish Game Jam by Jon Brady.
Both books were fascinating and I recommend them, for different reasons.

The first is a good guide to what goes on and how to survive one. If you're going to one and wonder how they work and what you need to think about before you go, get it!

The second was a long but entertaining ramble on how a game jam feels from the inside. Definitely gives the flavor of a game jam, and reminds me of Hunter S. Thompson (minus the drugs and sex).

But tbe Game Jam Survival Guide, as well as being entertaining, had some resources that were very helpful to this post.

Christer mentioned something called SFXR that was used in game jams a lot. I looked it up and it is a cool tool for making quick sound effects. Here's what it looks like:


You click on the buttons on the left until you get a sound you like (for example, a Jump sound). You can click on the button in the middle to modify the sounds, and there are even two buttons on the bottom left to mutate and randomize.  When you get something you like, click on Export .WAV and you've got a WAV file. Then use Audacity (or some other audio software) to convert it to OGG and use it in your game. Get it at http://www.drpetter.se/project_sfxr.html.

Kotaku also had a great article about using a tracker called Pulseboy that makes it easy to create chiptune music like the old 8-bit computers. Read the article here http://kotaku.com/5949117/make-chiptunes-in-your-browser-with-this-awesome-simple-sequencer. And get Pulseboy here: http://www.pulseboy.com/. Trackers are an old-school way to make music that have been replaced by Digital Audio Workstations (Sonar, Acid, Reason, Cubase, Logic Pro, FL Studio, etc.) which are big and expensive. I use FL Studio (formerly known as Fruity Loops until the cereal company made them stop) and Acid. The only tracker I used was Octamed and I don't like trackers because they are more complicated. If you want to know more about trackers, start here: http://woolyss.com/tracking-trackers.php.

Pulseboy is cool because it recreates the sounds of the Gameboy and looks like one too!


Pulseboy consists of 16 tracks, running vertically. You click on a square on the track and supply note information. When Pulseboy runs, it plays all 16 tracks and music comes out. Primitive, but at the same time simple. If you want chip music, use Pulseboy. Get it at: http://www.pulseboy.com/. It's pretty easy to learn and simple.

Between Pulseboy and SFXR, you can make your own music. Chip tunes belong with pixel art and both are very cool, so you don't have to be an artist or musician to make great game art and music.

As usual, all of this was tested and run on a Firefox OS ZTE Open phone.

Now back to games!


No comments:

Post a Comment