Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

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

$
0
0
Learn how to create a fun anagrams game!

Learn how to create a fun anagrams game!

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

Welcome back to our 3-part tutorial series that shows you how to make a letter / word game with UIKit – in this case, a game of anagrams!

If you successfully followed through the first part of this series, you should now have your game board showing up onscreen. So far, you’ve learned how to kick-start your UIKit game, how to plan your controllers and how to connect your views, and have also started implementing the game elements.

Now you’ll take on the gameplay itself and grant the player some special powers, like dragging the tiles around and dropping them onto the targets at the top of the screen. A heads-up-display with a timer and score ticker will keep the player’s ego in check.

Time to grab your UIKit and get back to work!

Getting Started: Your Epic Goal

When you run the completed project from Part 1 of this tutorial, you should see something like this:

Part 2 Start

In this second part of the tutorial series, you’ll aim for developing a fully playable version of the game. When you’re finished, the player will be able to drag the tiles and drop them on the correct targets, where they will “stick” to the spot. When all tiles have been placed on the correct targets, the player will win that puzzle.

There will also be a timer – the player will have a limited amount of time to finish each puzzle – and a score display that will increase and decrease in real time, according to the player’s actions.

You’ll achieve these epic goals by taking on these smaller steps:

  • Make the tiles draggable.
  • Make the tiles notify the game controller when they are dropped somewhere.
  • Implement handling of tile drops in the game controller.
  • Check if the player has won after each successful tile placement.
  • Create a separate view layer to contain elements like score readouts and menus. It’s usually best to keep these arranged in their own view, rather than including them in the same view as the actual gameplay elements.
  • Add a level timer and a player score. This is a game, after all!

All right! You’ve been debriefed. Now back to the code!

Drag Those Tiles

You might already be familiar with how to handle touch events with UIKit. If you are, there’s nothing different about handling touches in a UIKit game. You just need to implement the touch delegate methods in TileView. Before your tiles can receive touch events, however, you need to enable them.

Inside TileView.swift, add the following line to the end of init(letter:sideLength:):

self.userInteractionEnabled = true

This instructs iOS to send touch events to this object. By default, this is set to false for a UIImageView (which this class derives from), so you have to set it to true manually.

Next, you need to add the dragging code. To accomplish this, you are going to take the following strategy:

  • When the player touches down on a tile, figure out the offset within the tile of where their finger lies.
  • When the player drags, you’ll set the center of the tile to the new position of their finger – except you’ll shift the tile by the offset you computed earlier to account for where the player’s finger is within the tile.

Let’s try this out. Still in TileView.swift, add new properties to the top of the class:

private var xOffset: CGFloat = 0.0
private var yOffset: CGFloat = 0.0

You’ll use xOffset and yOffset to keep track of the distance between the center of the tile and the initial placement of the player’s finger when the touch begins.

To handle dragging the tiles, add the following touch handling methods to the TileView class:

//1
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
  let point = touches.anyObject()!.locationInView(self.superview)
  xOffset = point.x - self.center.x
  yOffset = point.y - self.center.y
}
 
//2
override func touchesMoved(touches: NSSet, withEvent event: UIEvent)  {
  let point = touches.anyObject()!.locationInView(self.superview)
  self.center = CGPointMake(point.x - xOffset, point.y - yOffset)
}
 
//3
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
  self.touchesMoved(touches, withEvent: event)
}

Here’s what’s going on in the above methods:

  1. When a touch is detected, you fetch its location within the tile’s superview (that is, the view that contains the tile). You calculate and store the distance from the touch to the tile’s center.
  2. When the player moves their finger, you move the tile to that location, but you adjust the location by the offsets you stored in xOffset and yOffset. This keeps the tile from centering itself under the player’s finger as soon as they start moving it – you want it to feel like the player is dragging the tile by a particular point.
  3. When the player lifts their finger, you make one last call to touchesMoved(touches:withEvent:) to make sure the tile’s position is set to the touch’s final location. You could have just typed the same code here as in touchesMoved(touches:withEvent:), but it’s better to avoid repeating code where possible to make maintenance easier.

All right! Build and run the project and have fun dragging the game tiles around.

Moving the Tiles

Cool – you’re already one step closer to your goal.

Hey, Game Controller! A Tile Is Dropping!

In order to make the game controller accept notifications from tiles when they are dropped somewhere on the board, you’ll make the game controller a delegate to all tiles. The tiles will then invoke a method on their delegate when the player drops them. Therefore, get ready to get some experience with the delegation pattern!

At the top of the TileView.swift file (after the import lines), declare the protocol with a single method:

protocol TileDragDelegateProtocol {
  func tileView(tileView: TileView, didDragToPoint: CGPoint)
}

TileDragDelegateProtocol requires one method – the one that handles a finished drag and drop operation.

Now inside the class, add a property to store the delegate:

var dragDelegate: TileDragDelegateProtocol?

All right! That should be enough declarations – time to add the code. Still in TileView.swift and at the end of touchesEnded(touches:withEvent:) add:

dragDelegate?.tileView(self, didDragToPoint: self.center)

Totally easy, right? If the dragDelegate property is not nil, you just call the delegate’s tileView(_:didDragToPoint:) method, passing it a reference to the tile (self) and self.center.

Now you need to make GameController conform to your new protocol and actually do something with this information.

Handling Tile Drops

Open up GameController.swift and change the class declaration to conform to the TileDragDelegateProtocol protocol:

class GameController: TileDragDelegateProtocol {

Add the initial implementation of the delegate method at the bottom of the GameController class:

//a tile was dragged, check if matches a target
func tileView(tileView: TileView, didDragToPoint point: CGPoint) {
  var targetView: TargetView?
  for tv in targets {
    if CGRectContainsPoint(tv.frame, point) && !tv.isMatched {
      targetView = tv
      break
    }
  }
}

This code loops over all objects in the targets array and, for each of the target views, checks whether the given drag point is within the target’s frame.

Ha! The simple if statement effectively checks if the tile’s center point was dropped within the target – that is, whether the tile was dropped on a target. And because the point passed to this method is the tile’s center, the function will not succeed if only a small portion of the tile is within the target.

If a tile is found to be within a target, the matching target is saved to targetView.

Now go on and check whether the letter has been dragged to the correct target to make the anagram. Add the following code to the end of the same method:

//1 check if target was found
if let foundTargetView = targetView {
 
  //2 check if letter matches
  if foundTargetView.letter == tileView.letter {
 
    //3
      println("Success! You should place the tile here!")
 
      //more stuff to do on success here
 
      println("Check if the player has completed the phrase")
    } else {
 
    //4
    println("Failure. Let the player know this tile doesn't belong here")
 
    //more stuff to do on failure here
  }
}

Now to break this down step-by-step:

  1. If the tile was not dropped onto a target, then targetView will still be nil, and the rest of the code will not be performed
  2. Compare the tile’s letter with the target’s letter to see if they match.
  3. If the letters do match, you will do various bits of processing. For now, you just print some messages to the console.
  4. If the letters do not match, you want to indicate that to the player. Again, for now just use a log statement until you’ve verified that the logic is working correctly.

OK, build and run the project and try dragging some tiles onto targets.

If everything went well, there shouldn’t be any difference compared to what you saw last time you ran the project. But why no print statements? That’s a good question! Can you figure out why the tiles don’t react to being dropped on the targets?

Solution Inside: Answer SelectShow>

In GameController.swift, just after tile.randomize() in dealRandomAnagram(), add the following line:

tile.dragDelegate = self

Run the project again and you will see your success/failure statements printed to the console when you drag a tile onto a target. Now that you know the right things are happening at the right times, you can make it do something more fun than printing to the console!

Inside GameController.swift, add the following method:

func placeTile(tileView: TileView, targetView: TargetView) {
  //1
  targetView.isMatched = true
  tileView.isMatched = true
 
  //2
  tileView.userInteractionEnabled = false
 
  //3
  UIView.animateWithDuration(0.35,
                      delay:0.00,
                    options:UIViewAnimationOptions.CurveEaseOut,
                 //4
                 animations: {
                    tileView.center = targetView.center
                    tileView.transform = CGAffineTransformIdentity
                  },
                  //5
                  completion: {
                    (value:Bool) in
                    targetView.hidden = true
                  })
}

You’ll call this method when the player successfully places a tile. Here’s what’s happening above:

  1. You set the isMatched property on both the targetView and tileView. This will help you later when you check to see if the player has completed the phrase.
  2. Disable user interactions for this tile. The player will not be able to move a tile once it’s been successfully placed.
  3. Create an animation that will last for 35 hundredths of a second. By passing in .CurveEaseOut, UIAnimation will automatically calculate an ease-out animation. Rather than making changes in even increments across the given time, it will do larger changes in the beginning and smaller changes toward the end, slowing down the animation.
  4. This closure defines the changes that should occur during the animation. In this case, you move the tile so its center is on the target’s center, and you set the tile’s transform to CGAffineTransformIdentity. The identity transform is basically no transform. This effectively undoes any changes you’ve made to scale and rotation. In this case, it straightens out the tile.
  5. When the animation is complete, you hide targetView. This isn’t absolutely necessary, since targetView will be completely behind tileView. But in general, if you know you have a view object that will not be visible, it’s more efficient to hide it so that its parent view knows it’s safe to skip it for certain types of processing.

Inside tileView(_:didDragToPoint:) in GameController.swift, replace the println() statement that begins with “Success!” with the following:

self.placeTile(tileView, targetView: foundTargetView)

Now handle what happens when the player puts a tile on an incorrect target. Still inside tileView(_:didDragToPoint:) in GameController.swift, replace the println() statement that begins with “Failure” with the following code:

//1
tileView.randomize()
 
//2
UIView.animateWithDuration(0.35,
    delay:0.00,
    options:UIViewAnimationOptions.CurveEaseOut,
    animations: {
      tileView.center = CGPointMake(tileView.center.x + CGFloat(randomNumber(minX:0, maxX:40)-20),
                                    tileView.center.y + CGFloat(randomNumber(minX:20, maxX:30)))
    },
    completion: nil)

There are only two major things happening here:

  1. You randomize the tile’s rotation again.
  2. You create an animation that slightly offsets the tile’s position.

Taken together, these two effects demonstrate to the player that the tile does not “fit” into the dropped target.

Build and run the project. This time, instead of just statements printed to your console, you will see the tiles slide onto the target positions when dropped in the correct place, and sliding away when placed in the wrong place. Cool!

Tiles sticking

You’re ready to check if the player has finished the current game. Inside GameController.swift, add the following:

func checkForSuccess() {
  for targetView in targets {
    //no success, bail out
    if !targetView.isMatched {
      return
    }
  }
  println("Game Over!")
}

The above code loops through the targets array and checks to see if ANY TargetView is not yet matched. If it finds an unmatched target, then the game is still not over and it bails out of the method.

If all the TargetViews are matched, then your log statement “Game Over!” appears in the console. Later, you will do some more interesting things here.

Inside tileView(_:didDragToPoint:) in GameController.swift, replace the println() statement that begins with “Check if” with the following code:

//check for finished game
self.checkForSuccess()

Build and run the app and complete a puzzle. You should see your log statement printed to the console when you are finished. Great!

Creating a HUD

HUD’s up!

Your game could use some controls and readouts. You might ask, “Should I add these elements to the gameView?”

No. The gameView is for all gameplay elements. You generally want to put elements like the score, time readouts and player controls onto their own view layer. This allows you to do many types of things more easily, like hide them when the game is paused, or maintain fixed sizes and locations for these controls while simultaneously zooming in on some area of the game board.

This separate layer is often called a heads-up-display, or HUD. It will be easier to test your HUD layer if you have something to display in it, so first you will make a label to display the game timer.

Create a new Swift file in Anagrams/Classes/Views named StopwatchView. This will be a subclass of UILabel that displays the remaining time in the format “[minutes]:[seconds]“.

Open up StopwatchView.swift and replace its contents with the following:

import UIKit
 
class StopwatchView: UILabel {
 
  //this should never be called
  required init(coder aDecoder:NSCoder) {
    fatalError("use init(frame:")
  }
 
  override init(frame: CGRect) {
    super.init(frame: frame)
    self.backgroundColor = UIColor.clearColor()
  }
 
  //helper method that implements time formatting
  //to an int parameter (eg the seconds left)
  func setSeconds(seconds:Int) {
    self.text = String(format: " %02i : %02i", seconds/60, seconds % 60)
  }
}

The above code isn’t fancy. It just sets the label’s background to be transparent (the “clear” color), and creates a method to set the label’s text property to the mm:ss format.

Now that you have a label to display, you need a HUD layer to hold it. Create a new Swift file in Anagrams/Classes/Views named HUDView and replace its contents with the following:

import UIKit
 
class HUDView: UIView {
 
  var stopwatch: StopwatchView
 
  //this should never be called
  required init(coder aDecoder:NSCoder) {
    fatalError("use init(frame:")
  }
 
  override init(frame:CGRect) {
    self.stopwatch = StopwatchView(frame:CGRectMake(ScreenWidth/2-150, 0, 300, 100))
    self.stopwatch.setSeconds(0)
 
    super.init(frame:frame)
    self.addSubview(self.stopwatch)
  }
}

For now, you’ll have a single stopwatch property that holds a timer initialized to 0 seconds remaining.

Now to get that timer on the screen! You haven’t checked your progress in a while.

First add a HUDView property to the game controller. Inside GameController.swift at the top of the class, add:

var hud: HUDView!

Next you’ll create the HUD view in your ViewController – the same spot where you create the game view. Switch to ViewController.swift and inside viewDidLoad(), find the line controller.gameView = gameView and just below it, add the HUD to the window’s view and the game controller:

//add one view for all hud and controls
let hudView = HUDView(frame: CGRectMake(0, 0, ScreenWidth, ScreenHeight))
self.view.addSubview(hudView)
controller.hud = hudView

You create a new HUD view as big as the screen and you add it to the view controller’s view. Then you also assign it to the game controller’s hud property.

OK, build and run again and have a look at your new HUD layer!

Ugly Label!

Well, that’s some progress. Before you move on with the functionality, now is probably a good time to talk about….

The Art of Gamification

The fact you are using UIKit to create a game does not mean that you also have to use vanilla UIKit components. To make a game more “game-like”, you will want to use fonts, buttons and other controls that look like they belong in a game and not in a spreadsheet program.

For example, compare the two fonts below:

Compare fonts

If you think the first font rendered above is more exciting, in a game context at least, perhaps you should hire a graphic designer. :] Long story short, choose an appropriate font for your game. You’re not likely to excite players with a game menu rendered in Arial or Verdana.

Note: Then why did you go for good old Verdana when you created the tile view? On tiles, it makes sense to have maximum readability. Players shouldn’t get confused about which letters they’re seeing.

I’ve included a fun font for you in the project. To use it, first open up Config.swift and add these font constants:

let FontHUD = UIFont(name:"comic andy", size: 62.0)!
let FontHUDBig = UIFont(name:"comic andy", size:120.0)!

Some of you probably will ask, “Isn’t it faster to put the font definition in the code? Why should it be in a separate file?”

The answer is convenience. Imagine your graphic designer finds a much cooler font for your game. If you already have font assignments sprinkled all over the game code, replacing them will be a pain.

What’s more, abstracting the font size is a very good idea. That’s why you have two definitions – one for a big font and one for normal-sized font. This makes it a lot easier to change the font size in all HUDs throughout your game with a single line change.

Open StopwatchView.swift and add the following line to the end of init(frame:):

self.font = FontHUDBig

This sets the label’s font to FontHUDBig, which you just defined in Config.swift.

Build and run again to behold your new, game-ready label!

Note: If you want to include your own font, as well as importing it into the project, you need to add the font to the project’s Info.plist using the key Fonts provided by application.

Beautiful timer!

Nice! However… if you try to drag the tiles around you will notice they don’t drag anymore…

Well… actually, that’s fine. It’s all because of how you normally have views ordered in a game setup.

As illustrated in the schema below, you will always have the game controls, score, time, etc. on top, so that they are always visible. Under the controls you will have all the objects that move – heroes, word tiles, and so on. And usually at the very back you will have the background – whether static or parallax, it does not matter.

You have exactly the same setup so far, but since the HUD view is on top of everything it swallows all touches. Fear not! You can easily turn off the user’s interaction with the HUD view, thus allowing the user to touch the tiles in the underlying game objects view.

Switch to HUDView.swift and at the end of the init(frame:), add the code to disable touches:

self.userInteractionEnabled = false

Now it’s fixed forever!

Build and run the game again, and you’ll see that you can have your timer and drag tiles, too! Sweet victory!

HUD works

Adding a Level Timer

Implementing the game timer is incredibly simple. Remember that each level already has a corresponding maximum time in seconds in the level config file. You haven’t used this data yet, but now you will.

Open to GameController.swift and add the following two properties to the class:

//stopwatch variables
private var secondsLeft: Int = 0
private var timer: NSTimer?

secondsLeft will store the number of seconds left to complete the level. It will be decreased every second by timer.

Now you need three new helper methods:

  1. One to start the timer when the tiles are displayed on the board.
  2. One to stop the timer in case the player solves the puzzle or the time is up.
  3. One to fire each second and update the HUD.

Add them one at a time to the game controller. First add the following method to initialize the new variables and start the timer:

func startStopwatch() {
  //initialize the timer HUD
  secondsLeft = level.timeToSolve
  hud.stopwatch.setSeconds(secondsLeft)
 
  //schedule a new timer
  timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector:"tick:", userInfo: nil, repeats: true)
}

You initialize secondsLeft with the initial remaining time stored in the level, and update the stopwatch label to display this time. You also schedule a new NSTimer that will invoke tick() each second (you haven’t added this function yet).

Next, add stopStopwatch to stop the timer:

func stopStopwatch() {
  timer?.invalidate()
  timer = nil
}

Calling invalidate on the timer stops it, and then you set it to nil because it is no longer needed.

Now define tick(), which will be called once a second while timer is running.

@objc func tick(timer: NSTimer) {  
  secondsLeft--
  hud.stopwatch.setSeconds(secondsLeft)
  if secondsLeft == 0 {
    self.stopStopwatch()
  }
}

Note the @objc attribute. The GameController class does not inherit from NSObject, so tick() would normally not be visible from Objective-C. Since NSTimer is an Objective-C class, you need to mark tick() with @objc to make it usable in Objective-C land.

This method does nothing more than decrease the amount of seconds left by one, then update the stopwatch label with the new value. If the number of seconds remaining hits 0, it calls stopStopwatch(). That’s all!

Now you need to actually start the timer when the game starts! At the end of dealRandomAnagram(), add:

//start the timer
self.startStopwatch()

And of course you need to stop the timer if the player completes the phrase. At the end of checkForSuccess(), add the following call:

//stop the stopwatch
self.stopStopwatch()

All right, another feature is in! Build and run again to see the timer ticking down when the game starts:

Clock is ticking

Hmm, what else can you add to your fancy new HUD?

Keeping Score, Fair and Square

When making a game you want to have a separate class just to keep the game data for the current player session. This class will store such things as the player’s current score, remaining number of lives and progress through the game. In a more complicated game, you could have this data also sorted per user, which might require a simple database of some sort.

For this tutorial, your game will have a simple class to keep the score between games, but not between app restarts.

Create a new file in Anagrams/Classes/Models named GameData.swift. Add the following code:

class GameData {
  //store the user's game achievement
  var points:Int = 0 {
		didSet {
			//custom setter - keep the score positive
			points = max(points, 0)
		}
  }
}

The points property will store the player’s current score.

The didSet property observer on the points variable ensures the value is never negative. You don’t want the player to have fewer than zero points, right? So you pass points and 0 to the max function, which returns the greater value of its two arguments. Thus, if you try to store a negative number into points, it will assign 0 to points.

That’s all you need for your simple GameData class. But if your game evolves, this class will also become more complicated.

OK, it’s time to decide where in the game the player will receive or lose points.

There are two point-events so far in the gameplay:

  • When a tile is dropped on a correct target, award the player with points.
  • When a tile is dropped on a wrong target, subtract some points from the current score.

This way, to get the most points possible out of the current anagram, the player will have to solve it without making any mistakes.

Challenge: You can also award points upon puzzle completion, and subtract points when the player fails to complete a puzzle in time. How would you implement that yourself?

Add the following property to the GameController class in GameController.swift:

private var data: GameData

Find init() and add the following line to it:

self.data = GameData()

This gives you access to a GameData object to store the score.

In the same file, add the following code inside tileView(_:didDragToPoint:), just below the comment that reads “//more stuff to do on success here”:

//give points
data.points += level.pointsPerTile

Luckily the level config file already includes the points per title for every level of difficulty, so all you do here is increment the current score by that number.

To handle errors, add the following code in the same method, just below the comment that reads “//more stuff to do on failure here”:

//take out points
data.points -= level.pointsPerTile/2

Here, you do the opposite and subtract points. But there’s no point (ha, ha) in being too tough on the player – if they drop the tile on the wrong target, only half of what they receive for a matching one is subtracted from the current score.

All right! Keeping score – check.

Note: The GameData class is quite naïve as it is. Your own UIKit game will likely have some other game data you need to track, requiring you to adjust the class as you wish. A few possible features deserve an honorable mention:

  • You might want to add Game Center integration to your game, which will forward the score from GameData to Apple’s Game Center. More on Game Center.
  • You might want to persist the score by saving it to the NSUserDefaults or a keychain. More on secure storage of data with a keychain.
  • If you would like to store the game data to a keychain or persist it to a file, you might find using the JSONModel library super useful. It allows you to convert your model data to a JSON string, which is easily storable in a keychain or a text file.

Adding the Score to the HUD

Games are all about achievements and score, so there’s no sense in keeping the player’s score behind the scenes. You need to put it upfront, right on the HUD layer.

In Anagrams/Classes/Views, create a new Swift file called CounterLabelView. This will be another subclass of UILabel.

Open CounterLabelView.swift and replace its contents with the following:

import UIKit
 
class CounterLabelView: UILabel {
  //1
  var value:Int = 0 {
    //2
    didSet {
      self.text = " \(value)"
    }
  }
 
  required init(coder aDecoder:NSCoder) {
    fatalError("use init(font:frame:")
  }
 
  //3
  init(font:UIFont, frame:CGRect) {
    super.init(frame:frame)
    self.font = font
    self.backgroundColor = UIColor.clearColor()
  }
}

Here’s what you need to know about the above:

  1. value is a property that will hold the score currently shown on the label.
  2. Whenever the value is changed, update the label
  3. Set the label’s font and set the background transparent

Inside HUDView.swift, add the following property to the HUDView class:

var gamePoints: CounterLabelView

In the HUDView‘s init(frame:) initializer, add the following before the super.init() call:

//the dynamic points label
self.gamePoints = CounterLabelView(font: FontHUD, frame: CGRectMake(ScreenWidth-200, 30, 200, 70))
gamePoints.textColor = UIColor(red: 0.38, green: 0.098, blue: 0.035, alpha: 1)
gamePoints.value = 0

Then add the following after the super.init()) call:

self.addSubview(gamePoints)
 
//"points" label
var pointsLabel = UILabel(frame: CGRectMake(ScreenWidth-340, 30, 140, 70))
pointsLabel.backgroundColor = UIColor.clearColor()
pointsLabel.font = FontHUD
pointsLabel.text = " Points:"
self.addSubview(pointsLabel)

When initializing the class, the class’s own properties must be initialized before calling the super class’s initialization. However, the super class’s properties can’t be referenced until after it has been initialized.

Now to update the score. Remember the two places where you change the score? In GameController.swift in the tileView(_:didDragToPoint:) method, find data.points += level.pointsPerTile and add the following just below it:

hud.gamePoints.value = data.points

Then look for the line data.points -= level.pointsPerTile/2 and add the following just below it:

hud.gamePoints.value = data.points

These lines updates the score label on the HUD to the same value as the game’s data points.

Build and run to see your score updating.

Points updating

It’s a bit boring though, that the score label instantly changes from, say 500 to 750, as it will on the harder levels. It’s a lot more fun to have the score label rapidly cycle through 500, 501, 502, etc. all the way up to 750 – it just adds a little polish to an awesome game experience.

Inside the CounterLabelView class in CounterLabelView.swift, add new private variables:

private var endValue: Int = 0
private var timer: NSTimer? = nil

Basically, you will set up a timer to repeatedly update the HUD, incrementing (or decrementing) the label one point, until you reach endValue.

Now add the following helper method:

func updateValue(timer:NSTimer) {
  //1 update the value
  if (endValue < value) {
    value -= 1
  } else {
    value += 1
  }
 
  //2 stop and clear the timer
  if (endValue == value) {
    timer.invalidate()
    self.timer = nil
  }
}

There are two things happening in this method:

  1. Depending on whether endValue is higher than the current value, you increment or decrement the value. The didSet observer on value will update the label’s text every time value is set.
  2. When endValue is equal to the current value, you have finished, so stop the timer, and clear it.

Now it’s time to implement setValue(_:duration:), which is the method that initially calls updateValue() to get the counter rolling. Add the following method:

//count to a given value
func setValue(newValue:Int, duration:Float) {
  //1 set the end value
  endValue = newValue
 
  //2 cancel previous timer
  if timer != nil {
    timer?.invalidate()
    timer = nil
  }
 
  //3 calculate the interval to fire each timer
  let deltaValue = abs(endValue - value)
  if (deltaValue != 0) {
    var interval = Double(duration / Float(deltaValue))
    if interval < 0.01 {
      interval = 0.01
    }
 
    //4 set the timer to update the value
    timer = NSTimer.scheduledTimerWithTimeInterval(interval, target: self, selector:"updateValue:", userInfo: nil, repeats: true)
  }
}

This function is the most complicated one you’ve added thus far, so let’s go over it carefully.

  1. Set endValue to the value that you are counting to.
  2. In the event that the player is really fast at adding letters, cancel any existing timers, because you don’t want them overlapping.
  3. Here you calculate the interval between calls to updateValue(timer:). To do so, you do the following:
    • You find how many values the label needs to count through. For example, if the current score is 50 and you want it to go to 65, you’ll get the delta value 15. If the current value is 65 and you want to go to 50, you’ll get the same delta value of 15.
    • Only do the update if the value needs to be changed.
    • You divide the total duration for the animation, duration, by the number of values you need to count through. This gives you the time interval for firing of the timer.
    • If interval is less than 0.01 seconds, you increase it to 0.01. This just keeps the animation from moving too quickly, which wouldn’t look as nice.
  4. Finally, you schedule a NSTimer to call the update.

Nice – and you’re just about done!

Now you need to call the new update functions from the game controller to see the animation. Switch to GameController.swift and replace the success line hud.gamePoints.value = data.points with:

hud.gamePoints.setValue(data.points, duration: 0.5)

This will make the score label count to the just-updated score value, and it’ll do that in half a second.

Change the failure line hud.gamePoints.value = data.points to:

hud.gamePoints.setValue(data.points, duration: 0.25)

This updates the label, but does it in 0.25 seconds. Since the penalty values are half the success values, making the duration half as long will make the counter appear to change values at the same pace, whether it’s going up or down. Feel free to tweak these values to your liking.

Build and run the project to enjoy your new and shiny HUD:

Counting Points

Oh, sweet joy! The score label counts up and down as you drop tiles on the targets. Your game is really coming together!

Where to Go from Here?

You can download all of the source code up to this point by clicking this link

Woot, your game is starting to take its final shape and is almost fully-playable. You’ve done well to make it this far.

If you think you’ve progressed a lot in this part of the tutorial, just wait for Part 3, where you’ll add even more cool effects and interactivity, including those promised explosions!

While you’re waiting, let us know how it’s going for you so far in the forums.

How To Make a Letter / Word Game with UIKit and Swift: Part 2/3 is a post from: Ray Wenderlich

The post How To Make a Letter / Word Game with UIKit and Swift: Part 2/3 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles