How To Make a Letter / Word Game with UIKit and Swift: Part 3/3

This third and final part of the series will be the most fun of them all! In this part, you’re going to be adding a lot of cool and fun features By Caroline Begbie.

Leave a rating/review
Save for later
Share

Update note: This tutorial was updated for Swift and iOS 8 by Caroline Begbie. Original series by Tutorial Team member Marin Todorov.

Update 04/12/15: Updated for Xcode 6.3 and Swift 1.2

Welcome to the our tutorial series about creating a letter / word game with UIKit for the iPad.

If you successfully followed through the first and second parts of this tutorial, your game should be pretty functional. But it still lacks some key game elements and could use a bit more eye candy.

This third and final part of the series will be the most fun of them all! In this part, you’re going to be adding a lot of cool and fun features:

  • Visual effects to make tile dragging more satisfying
  • Gratuitous sound effects
  • In-game help for players
  • A game menu for choosing the difficulty level
  • And yes… explosions! :]

That’s quite a lot, so let’s once again get cracking!

Enhanced Tile Dragging: Not a Drag

When you run the completed project from Part 2 of this tutorial, you have this:

Counting Points

Your players can drag tiles around on the screen just fine, but the effect is very two-dimensional. In this section you’ll see how some simple visual effects can make the dragging experience much more satisfying.

When the player begins dragging a tile, it would be nice if the tile got a little bigger and showed a drop shadow below itself. These effects will make it look like the player is actually lifting the tile from the board while moving it.

To accomplish this effect, you’ll be using some features of the 2D graphics Quartz framework.

Open TileView.swift and add the following code at the end of init(letter:sideLength:), just after the line where you enabled user interactions:

//create the tile shadow
self.layer.shadowColor = UIColor.blackColor().CGColor
self.layer.shadowOpacity = 0
self.layer.shadowOffset = CGSizeMake(10.0, 10.0)
self.layer.shadowRadius = 15.0
self.layer.masksToBounds = false
    
let path = UIBezierPath(rect: self.bounds)
self.layer.shadowPath = path.CGPath

Every UIView has a property called layer – this is the CALayer instance for the lower-level drawing layer used to draw the view. Luckily CALayer has a set of properties that you can use to create a drop shadow. The property names mostly speak for themselves, but a few probably need some extra explanation.

  • masksToBounds is set to true by default, which means the layer will never render anything outside its bounds. Because a drop shadow is rendered outside of the view’s bounds, you have to set this property to false to see the shadow.
  • shadowPath is a UIBezierPath that describes the shadow shape. Use this whenever possible to speed up the shadow rendering. In the code above, you use a rectangle path (same as the tile’s bounds), but you also apply rounding to the rectangle’s corners via shadowRadius, and you effectively have a shadow with the form of the tile itself. Nice!
  • Note that you’re setting the shadowOpacity to 0, which makes the shadow invisible. So you create the shadow when the tile is initialized, but you’ll later show and hide it when the player drags the tile.

Add the following code at the end of touchesBegan(_:withEvent:):

//show the drop shadow
self.layer.shadowOpacity = 0.8

This turns on the shadow when the player starts dragging. Setting the opacity to 0.8 gives it a bit of transparency, which looks nice.

Add the following code at the end of touchesEnded(_:withEvent:):

self.layer.shadowOpacity = 0.0

This turns off the shadow. The effect will look even better when you change the tile size, but build and run the app now and drag some tiles around to see it working:

Tile with shadows

It’s a nice subtle touch that the user will appreciate – even if subconsciously.

Now to handle resizing the tile so that it looks like it’s being lifted off the board. First add the following property to the TileView class:

private var tempTransform: CGAffineTransform = CGAffineTransformIdentity

This will store the original transform for when the player stops dragging the tile. Remember, each tile has a small random rotation to begin with, so that’s the state you’ll be storing here.

Next, add this code to the end of touchesBegan(_:withEvent:):

//save the current transform
tempTransform = self.transform
//enlarge the tile
self.transform = CGAffineTransformScale(self.transform, 1.2, 1.2)

This saves the current transform and then sets the size of the tile to be 120% of the current tile size.

Now find touchesEnded(_:withEvent:) and add the following line just before the call to dragDelegate:

//restore the original transform
self.transform = tempTransform

This restores the tile’s size to its pre-drag state. While you’re at it, add the following method right after touchesEnded:

//reset the view transform in case drag is cancelled
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
  self.transform = tempTransform
  self.layer.shadowOpacity = 0.0
}

iOS calls touchesCancelled(_:withEvent:) in certain special situations, like when the app receives a low memory warning or if a notification brings up a modal alert. Your method will ensure that the tile’s display is properly restored.

Build and run and move some tiles around!

You may have already noticed this issue while developing the app, but what’s wrong with the following screenshot?

Bad Layering

As you can see, the tile the player is dragging is displayed below another tile! That really destroys the illusion of lifting the tile.

This occurs because a parent view always displays its subviews in the order that they are added to it. That means any given tile may be displayed above some tiles and below others. Not good.

To fix this, add the following code to the end of touchesBegan(_:withEvent:):

self.superview?.bringSubviewToFront(self)

This tells the view that contains the tile (self.superview) to display the tile’s view above all other views. Dragging should feel a lot better now.

Good layering

Challenge: If you want to make tile dragging even cooler, how about if touching a tile that’s underneath other tiles animated those tiles out of the way, so it looks like the pile of tiles is disturbed by lifting the bottom tile? How would you implement that?

Adding Audio: From Mute to Cute

Tile dragging is now more realistic, but there’s only so far realism can go without audio. Sound is omnipresent and interactive in our daily lives, and your game can’t go without it.

The Xcode starter project includes some Creative Commons-licensed sound effects for your game. There are three sound files, which will correspond with the following game actions:

  • ding.mp3: Played when a tile matches a target.
  • wrong.m4a: Played when a tile is dropped on a wrong target.
  • win.mp3: Played when an anagram is solved.

For convenience, you will pre-define these file names in Config.swift. Add them after the constants you added there earlier:

// Sound effects
let SoundDing = "ding.mp3"
let SoundWrong = "wrong.m4a"
let SoundWin = "win.mp3"
let AudioEffectFiles = [SoundDing, SoundWrong, SoundWin]

You have each file name separately, to use when you want to play a single file. The array of all the sound effect file names is to use when preloading the sounds.

Now create a new Swift file in Anagrams/Classes/Controllers and call it AudioController.

Then inside AudioController.swift, add the following code:

import AVFoundation

class AudioController {
  private var audio = [String:AVAudioPlayer]()

}

Here you import the framework that you need to play audio or video, and set up a private dictionary audio to hold all preloaded sound effects. The dictionary will map String keys (the name of the sound effect) to AVAudioPlayer instances.

Next add the following method to preload all sound files:

func preloadAudioEffects(effectFileNames:[String]) {
  for effect in AudioEffectFiles {
    //1 get the file path URL
    let soundPath = NSBundle.mainBundle().resourcePath!.stringByAppendingPathComponent(effect)
    let soundURL = NSURL.fileURLWithPath(soundPath)
      
    //2 load the file contents
    var loadError:NSError?
    let player = AVAudioPlayer(contentsOfURL: soundURL, error: &loadError)
    assert(loadError == nil, "Load sound failed")

    //3 prepare the play
    player.numberOfLoops = 0
    player.prepareToPlay()
      
    //4 add to the audio dictionary
    audio[effect] = player
  }
}

Here you loop over the provided list of names from Config.swift and load the sounds. This process consist of a few steps:

  1. Get the full path to the sound file and convert it to a URL by using NSURL.fileURLWithPath().
  2. Call AVAudioPlayer(contentsOfURL:error:) to load a sound file in an audio player.
  3. Set the numberOfLoops to zero so that the sound won’t loop at all. Call prepareToPlay() to preload the audio buffer for that sound.
  4. Finally, save the player object in the audio dictionary, using the name of the file as the dictionary key.

That effectively loads all sounds, prepares them for a fast audio start and makes them accessible by their file name. Now you just need to add one method, which plays a given sound:

func playEffect(name:String) {
  if let player = audio[name] {
    if player.playing {
      player.currentTime = 0
    } else {
      player.play()
    }
  }
}

And voila! You have it!

This method takes in a file name, then looks it up in audio. If the sound exists, you first check if the sound is currently playing. If so, you just need to “rewind” the sound by setting its currentTime to 0; otherwise, you simply call play().

Hooray! You now have a simple audio controller for your game. Using it is extremely simple, too. First you need to preload all audio files when the game starts.

Switch to GameController.swift and add this property to the class:

private var audioController: AudioController

The audioController property stores the audio controller the GameController will use to play sounds.

Add the following lines to the end of init():

self.audioController = AudioController()
self.audioController.preloadAudioEffects(AudioEffectFiles)

This makes a new instance of the audio controller and uses the method you just wrote to preload the sounds listed in the config file. Nice! You can now go straight to playing some awesome sound effects!

Inside tileView(_:didDragToPoint:), add the following line after the comment that reads “//more stuff to do on success here”:

audioController.playEffect(SoundDing)

Now the player will see and hear when they’ve made a valid move!

Inside the same method, add the following line after the comment that reads “//more stuff to do on failure here”:

audioController.playEffect(SoundWrong)

Now the player will hear when they’ve made a mistake, too.

Build and run, and check out whether you can hear the sounds being played at the right moment by dragging tiles to the empty spaces.

As long as you’re taking care of the sound effects, you can also add the “winning” sound to the game. Just play the win.mp3 file at the end of checkForSuccess():

//the anagram is completed!
audioController.playEffect(SoundWin)

Sweet!

Contributors

Over 300 content creators. Join our team.