Let It Sound Wide
Mini Game Tutorial > Let It Sound

To be a game, it must have some sounds. Let's get started. In this chapter, you will learn how to:

  • play a sound
  • play sound loops (BGM)

You also learn what class should play a sound, as well as what class shouldn't.

Playing sounds with Game Framework

There are two different ways to play a sound in Game Framework. One is using NSSound, and another is using BGM that is included in the game framework.

NSSound - for one-shot sound

Cocoa provides an easy way to play sounds - NSSound. Playing a sound with NSSound is very simple.

file = "path/to/soundfile.m4a"
@sound = NSSound.alloc.initWithContentsOfFile(file, :byReference, true)
@sound.play if (@sound)

As NSSound.initWithContentsOfFile returns nil if a sound file cannot be opened. you should add an error handling code if needed.

to stop the sound, simply write a code like this:

@sound.stop if (@sound)

NSSound supports various music file format such as mp3, AAC(*.m4a), wav, and aiff. This is the easiest and effective way to play sounds in your game.

Using a sound file that is stored at the Resource folder in your application, you can specify a sound file like this:

file = NSBundle.mainBundle.resourcePath.to_s + "/soundfile.aiff"

BGM - for music loops

Unfortunately, NSSound doesn't provide a means of playing a sound loop seamlessly. You can try the code below but you will be suffering from hearing a little gap between loops.

file = "path/to/soundfile.m4a"
@sound = NSSound.alloc.initWithContentsOfFile(file, :byReference, true)
@sound.play if (@sound)
@timer = NSTimer.scheduledTimerWithTimeInterval(interval, :target, 
             self, :selector, :tick, :userInfo, userInfo, :repeats, repeated)

def someTimerDrivenMethod()
  @sound.play if (!@sound.isPlaying)
 end

It is critical when you want to play a sound loop. But don't worry, the BGM class in the game framework provides an easy way out for playing such sound loops.

file = "path/to/soundfile.m4a"
begin
  @sound = BGM.alloc.initWithFile(file)
  @sound.play(true) if (@sound)
rescue
  # BGM.initWithFile raises an exception if it cannot open a given file.
  puts "Cannot open file: #{file}"
end

The argument true of @sound.play designates that the sound will be played continuously. To play one-shot sound, designate false as an argument of BGM.play (you can also use NSSound).

To stop a sound, simply write the following line.

 @sound.stop() if (@sound)

BGM class has similar methods that are in NSSound such as play, pause, resume, isPlaying, and stop. See BGM class reference for farther information.

Let Cannon Balls sound

Let's make an exploding sound when a cannon ball reaches its target, as well as it hits an enemy.

First, add the init method to the beginning of the View part of CannonBall.

def init()
  soundFile = NSBundle.mainBundle.resourcePath.to_s + "/Explosion.m4a"
  # class variable is used to share this sound object with all the instances.
  @@sound = NSSound.alloc.initWithContentsOfFile(soundFile, :byReference, true)
end

Second, add playSound to the View part of CannonBall so it can make a sound when it reaches its target.

def playSound()
  if (@opacity == 1.0)
    @@sound.stop if (@@sound.isPlaying.to_i == 1)
    @@sound.play
  end
end

Finally, update the draw method to start playing a sound when @exploding is set to true

def draw()
  init() if (!defined?(@@sound)) # <------ added
  size = SIZE * (3.0 - @opacity * 2)
  if (@active)
    if (!@exploding)
      NSColor.blueColor.set
    else
      NSColor.redColor.colorWithAlphaComponent(@opacity).set
      playSound() # <------ added
    end
    point = NSPoint.new(@point.x - size / 2, @point.y - size / 2)
    rect = NSRect.new(point, NSSize.new(size, size))
    path = NSBezierPath.bezierPathWithOvalInRect(rect)
    path.fill
  end
end

Let the Tank sound

The tank also make a sound when it launches a cannon ball. There are two modifications here. One is adding two lines of code below to the end of Tank.init:

 if (!defined?(@sound))
   soundFile = NSBundle.mainBundle.resourcePath.to_s + "/CannonLaunch.m4a"
   @sound = NSSound.alloc.initWithContentsOfFile(soundFile, :byReference, true)
   @aiming.init()
 end

You don't need any explanation here because it is pretty much similar to the code for Cannon.

Another one is overriding draw at the View part of Tank. Take a look at the code below:

 def draw()
   super()
   @sound.play if (@launchTimer == 10)
 end

This code is also simple. it draws the image by invoking super. It, then, plays a sound when it launches a cannon ball. The if closure means that it plays a sound only when the space key is pressed since @launchTimer is set to 10 when the key is pressed.

Let Your Game have a BGM

Playing a BGM in a game is not that hard. Add the following methods to TankGameView.

 # invoked when the current state is changed to TankGamePlayingState
 def notifyStateDidChange()
   @model.tank.init() 
   playSound()
 end
 # invoked when the current state is about to transit to another.
 def notifyStateWillChange(state)
   @sound.stop if (@sound)
   super(state)
 end
 
 # starts playing a BGM at the beginning of TankGamePlayingState
 def playSound()
   if (!defined?(@sound) || !@sound)
     soundFile = NSBundle.mainBundle.resourcePath.to_s + "/TankGame.aif" 
     begin
       @sound = BGM.alloc.initWithFile(soundFile)
     rescue
       @sound = nil
     end
   end
   @sound.play(true) if (@sound)
 end

As the notifyStateDidChange method is invoked when the current state is changed to TankGamePlayingState, it is a good place to start playing a BGM at this method.

When leaving TankGamePlayingState, notifyStateWillChange is invoked. Overriding this method gives you a chance to stop the BGM.

That's it.

Check if you can hear sounds

Run the application again. You can hear a BGM. It also makes a sound when you launch a cannon ball, as well as when the cannon ball reaches its target.

Inside the game framework - What class should play a sound?

There are lots of classes in TankGame so far and you might wonder where you should put a code for playing a sound. The answer should be:

  • character (e.g. Tank, Cannon, Enemies, etc..)
  • TankView (or other subclass of View class if any)
  • XXXXState objects (not recommended)

From a software engineering perspective, XXXModel must not play a sound because playing a sound is generally considered as a role of View. So playing sounds in a subclass of View is a good way. However, it is not that convenient to play sounds only in subclasses of View because these classes need to know everything about playing sounds (e.g. when to play, what sound to play), which often makes these classes very fat when there are lots of sounds in your game.

There are two solutions to avoid making View classes fat. One is to let characters play sounds. As a character has roles of both Model and View, playing sounds in its View part is a good way. It often makes the code very simple.

Another solution (or I should say workaround) is to play sounds in subclasses of State (e.g. TankGamePlayingState). This is not beautiful at all since State doesn't have a role of playing sounds, so you should not do this unless it is very hard to play sounds in View classes. The best way to separate the role of M, V, and C in terms of playing a sound is that:

  • a Model object has sound file names (sound objects would be OK)
  • a View object or a View part of a character object plays sounds
  • The Controller triggers either Model or View to play sounds

What we've created so far

LetItSound.tar.gz SIZE:x(?KB) is the file that we made so far. Here are the steps to run the game.

  1. Untar the tar.gz file at the project folder
  2. Add a group named "TankGame" to the "Resources" group at the left pane of the Xcode project window. (if not exists)
  3. Right click the group and select "Add" -> "Existing Files" to add all the sound files (*.m4a and *.aif)
  4. Run the application by pressing the "Build & Run" button

Let Them Fight<Prev|Top:Mini Game Tutorial|Next>Brush It Up

Leave a comment

Begin the comment with //pukiwiki if you want to write a comment in PukiWiki format.

You must be logged in to post a comment.