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

How to Make a Game Like Candy Crush with SpriteKit and Swift: Part 2

$
0
0
In this epic tutorial you'll learn how to make a tasty match-3 game like Candy Crush with SpriteKit and Swift. Now updated for Xcode 7.3 and Swift 2.2.

In this epic tutorial you’ll learn how to make a tasty match-3 game like Candy Crush with SpriteKit and Swift. Now updated for Xcode 7.3 and Swift 2.2.

Update note: This SpriteKit tutorial has been updated for Xcode 7.3 and Swift 2.2 by Morten Faarkrog. The original tutorial was written by Matthijs Hollemans.

Welcome back to our “How to Make a Game Like Candy Crush” tutorial with SpriteKit and Swift series. Your game is called Cookie Crunch Adventure and it’s delicious!

  • In the first part, you put some of the foundation in place. You setup the gameplay view, the sprites, and the logic for loading levels.
  • (You’re here) The second part will continue expanding on the foundation of the game. You’ll focus on detecting swipes and swapping cookies, and you’ll also create some nice visual effects in the process.
  • In the third part, you’ll work on finding and removing chains and refilling the level with new yummy cookies after successful swipes.
  • Finally, in the fourth part, you’ll complete the gameplay by adding support for scoring points, winning and losing, shuffling the cookies, and more.

This Swift tutorial picks up where you left off in the last part. If you don’t have it already, here is the project with all of the source code up to this point. You also need a copy of the resources zip (this is the same file from Part One).

Let’s get cookie-ing!

Add Swipe Gestures

In Cookie Crunch Adventure, you want the player to be able to swap two cookies by swiping left, right, up or down.

Detecting swipes is a job for GameScene. If the player touches a cookie on the screen, then this might be the start of a valid swipe motion. Which cookie to swap with the touched cookie depends on the direction of the swipe.

To recognize the swipe motion, you’ll use the touchesBegan, touchesMoved and touchesEnded methods from GameScene. Even though iOS has very handy pan and swipe gesture recognizers, these don’t provide the level of accuracy and control that this game needs.

Go to GameScene.swift and add two private properties to the class:

var swipeFromColumn: Int?
var swipeFromRow: Int?

These properties record the column and row numbers of the cookie that the player first touched when she started her swipe movement.

Initialize these two properties at the bottom of init(size:):

swipeFromColumn = nil
swipeFromRow = nil

The value nil means that these properties have invalid values. In other words, they don’t yet point at any of the cookies. This is why they are declared as optionals — Int? instead of just Int — because they need to be nil when the player is not swiping.

You first need to add a new convertPoint() method. It’s the opposite of pointForColumn(column:, row:), so you may want to add this method right below pointForColumn() so the two methods are nearby.

func convertPoint(point: CGPoint) -> (success: Bool, column: Int, row: Int) {
  if point.x >= 0 && point.x < CGFloat(NumColumns)*TileWidth &&
     point.y >= 0 && point.y < CGFloat(NumRows)*TileHeight {
    return (true, Int(point.x / TileWidth), Int(point.y / TileHeight))
  } else {
    return (false, 0, 0)  // invalid location
  }
}

This method takes a CGPoint that is relative to the cookiesLayer and converts it into column and row numbers. The return value of this method is a tuple with three values: 1) the boolean that indicates success or failure; 2) the column number; and 3) the row number. If the point falls outside the grid, this method returns false for success.

Now add the touchesBegan() method:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
  // 1
  guard let touch = touches.first else { return }
  let location = touch.locationInNode(cookiesLayer)
  // 2
  let (success, column, row) = convertPoint(location)
  if success {
    // 3
    if let cookie = level.cookieAtColumn(column, row: row) {
      // 4
      swipeFromColumn = column
      swipeFromRow = row
    }
  }
}

Note: This method needs to be marked override because the base class SKScene already contains a version of touchesBegan. This is how you tell Swift that you want it to use your own version.

The game will call touchesBegan() whenever the user puts her finger on the screen. Here’s what the method does, step by step:

  1. It converts the touch location, if any, to a point relative to the cookiesLayer.
  2. Then, it finds out if the touch is inside a square on the level grid by calling a method you’ll write in a moment. If so, then this might be the start of a swipe motion. At this point, you don’t know yet whether that square contains a cookie, but at least the player put her finger somewhere inside the 9×9 grid.
  3. Next, the method verifies that the touch is on a cookie rather than on an empty square.
  4. Finally, it records the column and row where the swipe started so you can compare them later to find the direction of the swipe.

So far, you have detected the start of a possible swipe motion. To perform a valid swipe, the player also has to move her finger out of the current square. It doesn’t really matter where the finger ends up—you’re only interested in the general direction of the swipe, not the exact destination.

The logic for detecting the swipe direction goes into touchesMoved(), so add this method next:

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
  // 1
  guard swipeFromColumn != nil else { return }
 
  // 2
  guard let touch = touches.first else { return }
  let location = touch.locationInNode(cookiesLayer)
 
  let (success, column, row) = convertPoint(location)
  if success {
 
    // 3
    var horzDelta = 0, vertDelta = 0
    if column < swipeFromColumn! {          // swipe left
      horzDelta = -1
    } else if column > swipeFromColumn! {   // swipe right
      horzDelta = 1
    } else if row < swipeFromRow! {         // swipe down
      vertDelta = -1
    } else if row > swipeFromRow! {         // swipe up
      vertDelta = 1
    }
 
    // 4
    if horzDelta != 0 || vertDelta != 0 {
      trySwapHorizontal(horzDelta, vertical: vertDelta)
 
      // 5
      swipeFromColumn = nil
    }
  }
}

Here is what this does step by step:

  1. If swipeFromColumn is nil, then either the swipe began outside the valid area or the game has already swapped the cookies and you need to ignore the rest of the motion. You could keep track of this in a separate boolean but using swipeFromColumn is just as easy — that’s why you made it an optional.
  2. This is similar to what touchesBegan() does to calculate the row and column numbers currently under the player’s finger.
  3. Here the method figures out the direction of the player’s swipe by simply comparing the new column and row numbers to the previous ones. Note that you’re not allowing diagonal swipes (since you’re using else if statements, only one of horzDelta or vertDelta will be set).
  4. The method only performs the swap if the player swiped out of the old square.
  5. By setting swipeFromColumn back to nil, the game will ignore the rest of this swipe motion.

Note: To read the actual values from swipeFromColumn and swipeFromRow, you have to use the exclamation point. These are optional variables, and using the ! will “unwrap” the optional. Normally you’d use optional binding to read the value of an optional but here you’re guaranteed that swipeFromRow is not nil (you checked for that at the top of the method), so using ! is perfectly safe.

The hard work of cookie-swapping goes into a new method:

func trySwapHorizontal(horzDelta: Int, vertical vertDelta: Int) {
  // 1
  let toColumn = swipeFromColumn! + horzDelta
  let toRow = swipeFromRow! + vertDelta
  // 2
  guard toColumn >= 0 && toColumn < NumColumns else { return }
  guard toRow >= 0 && toRow < NumRows else { return }
  // 3
  if let toCookie = level.cookieAtColumn(toColumn, row: toRow),
     let fromCookie = level.cookieAtColumn(swipeFromColumn!, row: swipeFromRow!) {
       // 4
       print("*** swapping \(fromCookie) with \(toCookie)")
  }
}

This is called “try swap” for a reason. At this point, you only know that the player swiped up, down, left or right, but you don’t yet know if there are two cookies to swap in that direction.

  1. You calculate the column and row numbers of the cookie to swap with.
  2. It is possible that the toColumn or toRow is outside the 9×9 grid. This can occur when the user swipes from a cookie near the edge of the grid. The game should ignore such swipes.
  3. The final check is to make sure that there is actually a cookie at the new position. You can’t swap if there’s no second cookie. This happens when the user swipes into a gap where there is no tile.
  4. When you get here, it means everything is OK and this is a valid swap! For now, you log both cookies to the Xcode debug pane.

For completeness’s sake, you should also implement touchesEnded(), which is called when the user lifts her finger from the screen, and touchesCancelled(), which happens when iOS decides that it must interrupt the touch (for example, because of an incoming phone call).

Add the following:

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
  swipeFromColumn = nil
  swipeFromRow = nil
}
 
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
  if let touches = touches {
    touchesEnded(touches, withEvent: event)
  }
}

If the gesture ends, regardless of whether it was a valid swipe, you reset the starting column and row numbers to the special value nil.

Great! Build and run, and try out different swaps:

Valid swap

Of course, you won’t see anything happen in the game yet, but at least the debug pane logs your attempts to make a valid swap.

Animating the Swaps

To describe the swapping of two cookies, you will create a new type, Swap. This is another model object whose only purpose it is to say, “The player wants to swap cookie A with cookie B.”

Create a new Swift File named Swap.swift. Replace the contents of Swap.swift with the following:

struct Swap: CustomStringConvertible {
  let cookieA: Cookie
  let cookieB: Cookie
 
  init(cookieA: Cookie, cookieB: Cookie) {
    self.cookieA = cookieA
    self.cookieB = cookieB
  }
 
  var description: String {
    return "swap \(cookieA) with \(cookieB)"
  }
}

Now that you have an object that can describe an attempted swap, the question becomes: Who will handle the logic of actually performing the swap? The swipe detection logic happens in GameScene, but all the real game logic so far is in GameViewController.

That means GameScene must have a way to communicate back to GameViewController that the player performed a valid swipe and that a swap must be attempted. One way to communicate is through a delegate protocol, but since this is the only message that GameScene must send back to GameViewController, you’ll use a closure instead.

Add the following property to the top of GameScene.swift:

var swipeHandler: ((Swap) -> ())?

That looks scary… The type of this variable is ((Swap) -> ())?. Because of the -> you can tell this is a closure or function. This closure or function takes a Swap object as its parameter and does not return anything. The question mark indicates that swipeHandler is allowed to be nil (it is an optional).

It’s the scene’s job to handle touches. If it recognizes that the user made a swipe, it will call the closure that’s stored in the swipe handler. This is how it communicates back to the GameViewController that a swap needs to take place.

Still in GameScene.swift, add the following code to the bottom of trySwapHorizontal(vertical:), replacing the print() statement:

if let handler = swipeHandler {
  let swap = Swap(cookieA: fromCookie, cookieB: toCookie)
  handler(swap)
}

This creates a new Swap object, fills in the two cookies to be swapped and then calls the swipe handler to take care of the rest. Because swipeHandler can be nil, you use optional binding to get a valid reference first.

GameViewController will decide whether the swap is valid; if it is, you’ll need to animate the two cookies. Add the following method to do this in GameScene.swift:

func animateSwap(swap: Swap, completion: () -> ()) {
  let spriteA = swap.cookieA.sprite!
  let spriteB = swap.cookieB.sprite!
 
  spriteA.zPosition = 100
  spriteB.zPosition = 90
 
  let Duration: NSTimeInterval = 0.3
 
  let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
  moveA.timingMode = .EaseOut
  spriteA.runAction(moveA, completion: completion)
 
  let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
  moveB.timingMode = .EaseOut
  spriteB.runAction(moveB)
}

This is basic SKAction animation code: You move cookie A to the position of cookie B and vice versa.

The cookie that was the origin of the swipe is in cookieA and the animation looks best if that one appears on top, so this method adjusts the relative zPosition of the two cookie sprites to make that happen.

After the animation completes, the action on cookieA calls a completion block so the caller can continue doing whatever it needs to do. That’s a common pattern for this game: The game waits until an animation is complete and then it resumes.

() -> () is simply shorthand for a closure that returns void and takes no parameters.

Now that you’ve handled the view, there’s still the model to deal with before getting to the controller! Open Level.swift and add the following method:

func performSwap(swap: Swap) {
  let columnA = swap.cookieA.column
  let rowA = swap.cookieA.row
  let columnB = swap.cookieB.column
  let rowB = swap.cookieB.row
 
  cookies[columnA, rowA] = swap.cookieB
  swap.cookieB.column = columnA
  swap.cookieB.row = rowA
 
  cookies[columnB, rowB] = swap.cookieA
  swap.cookieA.column = columnB
  swap.cookieA.row = rowB
}

This first makes temporary copies of the row and column numbers from the Cookie objects because they get overwritten. To make the swap, it updates the cookies array, as well as the column and row properties of the Cookie objects, which shouldn’t go out of sync. That’s it for the data model.

Go to GameViewController.swift and add the following method:

func handleSwipe(swap: Swap) {
  view.userInteractionEnabled = false
 
  level.performSwap(swap)
 
  scene.animateSwap(swap) {
    self.view.userInteractionEnabled = true
  }
}

You first tell the level to perform the swap, which updates the data model—and then tell the scene to animate the swap, which updates the view. Over the course of this tutorial, you’ll add the rest of the gameplay logic to this function.

While the animation is happening, you don’t want the player to be able to touch anything else, so you temporarily turn off userInteractionEnabled on the view. You turn it back on in the completion block that is passed to animateSwap().

Note: The above uses so-called trailing closure syntax, where the closure is written behind the function call. An alternative way to write it is as follows:

scene.animateSwap(swap, completion: {
  self.view.userInteractionEnabled = true
})

Also add the following line to viewDidLoad(), just before the line that presents the scene:

scene.swipeHandler = handleSwipe

This assigns the handleSwipe() function to GameScene’s swipeHandler property. Now whenever GameScene calls swipeHandler(swap), it actually calls a function in GameViewController. Freaky! This works because in Swift you can use functions and closures interchangeably.

Build and run the app. You can now swap the cookies! Also, try to make a swap across a gap—it won’t work!

Swap cookies

Note: You may be wondering why Cookie is a class but Swap is a struct. In Swift a struct is a value type, while a class is a reference type. That means structs get copied when you pass them around, but for classes only a reference is passed around. (There are other differences too; for example, you can’t use inheritance with structs.)

The reason you can make Swap, and also Set and Array2D, into structs is that these objects do not have an “identity”. A Swap that links to cookie X and cookie Y is identical to another Swap instance that links to cookie X and cookie Y, even though these two instances each take up their own space in memory. So these two Swap instances are interchangeable, which is why they don’t have an identity. Likewise for Array2D and Set. Swift’s struct fits better with this sort of thing than class.

A Cookie, on the other hand, is a uniquely identifiable thing. You want to have a reference to it, so different parts of the app all work on the same object instead of different copies. That’s why class makes more sense for Cookie than struct.

Highlighting the Cookies

In Candy Crush Saga, the candy you swipe lights up for a brief moment. You can achieve this effect in Cookie Crunch Adventure by placing a highlight image on top of the sprite.

The texture atlas has highlighted versions of the cookie sprites that are brighter and more saturated. The CookieType enum already has a function to return the name of this image: highlightedSpriteName.

You will improve GameScene to add this highlighted cookie on top of the existing cookie sprite. Adding it as a new sprite, as opposed to replacing the existing sprite’s texture, makes it easier to crossfade back to the original image.

In GameScene.swift, add a new private property to the class:

var selectionSprite = SKSpriteNode()

Add the following method:

func showSelectionIndicatorForCookie(cookie: Cookie) {
  if selectionSprite.parent != nil {
    selectionSprite.removeFromParent()
  }
 
  if let sprite = cookie.sprite {
    let texture = SKTexture(imageNamed: cookie.cookieType.highlightedSpriteName)
    selectionSprite.size = CGSize(width: TileWidth, height: TileHeight)
    selectionSprite.runAction(SKAction.setTexture(texture))
 
    sprite.addChild(selectionSprite)
    selectionSprite.alpha = 1.0
  }
}

This gets the name of the highlighted sprite image from the Cookie object and puts the corresponding texture on the selection sprite. Simply setting the texture on the sprite doesn’t give it the correct size but using an SKAction does.

You also make the selection sprite visible by setting its alpha to 1. You add the selection sprite as a child of the cookie sprite so that it moves along with the cookie sprite in the swap animation.

Add the opposite method, hideSelectionIndicator():

func hideSelectionIndicator() {
  selectionSprite.runAction(SKAction.sequence([
    SKAction.fadeOutWithDuration(0.3),
    SKAction.removeFromParent()]))
}

This method removes the selection sprite by fading it out.

What remains, is for you to call these methods. First, in touchesBegan(), in the if let cookie = ... section, add:

showSelectionIndicatorForCookie(cookie)

And in touchesMoved(), after the call to trySwapHorizontal(), add:

hideSelectionIndicator()

There is one last place to call hideSelectionIndicator(). If the user just taps on the screen rather than swipes, you want to fade out the highlighted sprite, too. Add these lines to the top of touchesEnded():

if selectionSprite.parent != nil && swipeFromColumn != nil {
  hideSelectionIndicator()
}

Build and run, and light up some cookies! :]

Highlighted cookies

A Smarter Way to Fill the Array

The purpose of this game is to make chains of three or more of the same cookie. But right now, when you run the game there may already be such chains on the screen. That’s no good — you only want matches after the user swaps two cookies or after new cookies falls down the screen.

Here’s your rule: Whenever it’s the user’s turn to make a move, whether at the start of the game or at the end of a turn, no matches may exist on the board. To guarantee this is the case, you have to make the method that fills up the cookies array a bit smarter.

Go to Level.swift and find createInitialCookies(). Replace the single line that calculates the random cookieType with the following:

var cookieType: CookieType
repeat {
  cookieType = CookieType.random()
} while (column >= 2 &&
        cookies[column - 1, row]?.cookieType == cookieType &&
        cookies[column - 2, row]?.cookieType == cookieType)
   || (row >= 2 &&
        cookies[column, row - 1]?.cookieType == cookieType &&
        cookies[column, row - 2]?.cookieType == cookieType)

Yowza! What is all this? This piece of logic picks the cookie type at random and makes sure that it never creates a chain of three or more.

In pseudo-code, it looks like this:

repeat {
  generate a new random cookie type
}
while there are already two cookies of this type to the left
   or there are already two cookies of this type below

If the new random number causes a chain of three (because there are already two cookies of this type to the left or below) then the method tries again. The loop repeats until it finds a random number that does not create a chain of three or more. It only has to look to the left or below because there are no cookies yet on the right or above.

Try it out! Run the app and verify that there are no longer any chains in the initial state of the game.

No chains in initial state

Track Allowable Swaps

You only want to let the player swap two cookies if it would result in either (or both) of these cookies making a chain of three or more.

Allowed swap

You need to add some logic to the game to detect whether a swap results in a chain. There are two ways you could do this. The most obvious way is to check at the moment the user tries the swap.

Alternatively, you could build a list of all possible moves after the level is shuffled. Then you only have to check if the attempted swap is in that list.

Note: Building a list also makes it easy to show a hint to the player. You’re not going to do that in this tutorial, but in Candy Crush Saga, when the player takes too long, the game lights up a possible swap. You can implement this for yourself by picking a random item from this list of possible moves.

In Level.swift, add a new property:

private var possibleSwaps = Set<Swap>()

Again, you’re using a Set here instead of an Array because the order of the elements in this collection isn’t important. This Set will contain Swap objects. If the player tries to swap two cookies that are not in the set, then the game won’t accept the swap as a valid move.

Xcode warns that Swap cannot be used in a Set, and that’s because Swap does not implement the Hashable protocol yet.

Open up Swap.swift and make the following changes. First, add Hashable to the struct declaration:

struct Swap: CustomStringConvertible, Hashable {

Then add the hashValue property inside the struct:

var hashValue: Int {
  return cookieA.hashValue ^ cookieB.hashValue
}

This simply combines the hash values of the two cookies with the exclusive-or operator. That’s a common trick to make hash values.

And finally, add the == function outside of the struct:

func ==(lhs: Swap, rhs: Swap) -> Bool {
  return (lhs.cookieA == rhs.cookieA && lhs.cookieB == rhs.cookieB) ||
         (lhs.cookieB == rhs.cookieA && lhs.cookieA == rhs.cookieB)
}

Now you can use Swap objects in a Set and the compiler error should be history.

At the start of each turn, you need to detect which cookies the player can swap. You’re going to make this happen in shuffle(). Go back to Level.swift and change the code for that method to:

func shuffle() -> Set<Cookie> {
  var set: Set<Cookie>
  repeat {
    set = createInitialCookies()
    detectPossibleSwaps()
    print("possible swaps: \(possibleSwaps)")
  } while possibleSwaps.count == 0
 
  return set
}

As before, this calls createInitialCookies() to fill up the level with random cookie objects. But then it calls a new method that you will add shortly, detectPossibleSwaps(), to fill up the new possibleSwaps set.

In the very rare case that you end up with a distribution of cookies that allows for no swaps at all, this loop repeats to try again. You can test this with a very small level, such as one with only 3×3 tiles. I’ve included such a level for you in the project called Level_4.json.

detectPossibleSwaps() will use a helper method to see if a cookie is part of a chain. Add this method now:

private func hasChainAtColumn(column: Int, row: Int) -> Bool {
  let cookieType = cookies[column, row]!.cookieType
 
  // Horizontal chain check
  var horzLength = 1
 
  // Left
  var i = column - 1
  while i >= 0 && cookies[i, row]?.cookieType == cookieType {
    i -= 1
    horzLength += 1
  }
 
  // Right
  i = column + 1
  while i < NumColumns && cookies[i, row]?.cookieType == cookieType {
    i += 1
    horzLength += 1
  }
  if horzLength >= 3 { return true }
 
  // Vertical chain check
  var vertLength = 1
 
  // Down
  i = row - 1
  while i >= 0 && cookies[column, i]?.cookieType == cookieType {
    i -= 1
    vertLength += 1
  }
 
  // Up
  i = row + 1
  while i < NumRows && cookies[column, i]?.cookieType == cookieType {
    i += 1
    vertLength += 1
  }
  return vertLength >= 3
}

A chain is three or more consecutive cookies of the same type in a row or column.

Look left right up down

Given a cookie in a particular square on the grid, this method first looks to the left. As long as it finds a cookie of the same type, it increments horzLength and keeps going left.

Note: It’s possible that cookies[column, row] will return nil because of a gap in the level design, meaning there is no cookie at that location. That’s no problem because of Swift’s optional chaining. Because of the ? operator, the loop will terminate whenever such a gap is encountered.

Now that you have this method, you can implement detectPossibleSwaps(). Here’s how it will work at a high level:

  1. It will step through the rows and columns of the 2-D grid and simply swap each cookie with the one next to it, one at a time.
  2. If swapping these two cookies creates a chain, it will add a new Swap object to the list of possibleSwaps.
  3. Then, it will swap these cookies back to restore the original state and continue with the next cookie until it has swapped them all.
  4. It will go through the above steps twice: once to check all horizontal swaps and once to check all vertical swaps.

It’s a big one, so you’ll take it in parts!

First, add the outline of the method:

func detectPossibleSwaps() {
  var set = Set<Swap>()
 
  for row in 0..<NumRows {
    for column in 0..<NumColumns {
      if let cookie = cookies[column, row] {
 
        // TODO: detection logic goes here
      }
    }
  }
 
  possibleSwaps = set
}

This is pretty simple: The method loops through the rows and columns, and for each spot, if there is a cookie rather than an empty square, it performs the detection logic. Finally, the method places the results into the possibleSwaps property.

The detection will consist of two separate parts that do the same thing but in different directions. First you want to swap the cookie with the one on the right, and then you want to swap the cookie with the one above it. Remember, row 0 is at the bottom so you’ll work your way up.

Add the following code where it says “TODO: detection logic goes here”:

// Is it possible to swap this cookie with the one on the right?
if column < NumColumns - 1 {
  // Have a cookie in this spot? If there is no tile, there is no cookie.
  if let other = cookies[column + 1, row] {
    // Swap them
    cookies[column, row] = other
    cookies[column + 1, row] = cookie
 
    // Is either cookie now part of a chain?
    if hasChainAtColumn(column + 1, row: row) ||
       hasChainAtColumn(column, row: row) {
      set.insert(Swap(cookieA: cookie, cookieB: other))
    }
 
    // Swap them back
    cookies[column, row] = cookie
    cookies[column + 1, row] = other
  }
}

This attempts to swap the current cookie with the cookie on the right, if there is one. If this creates a chain of three or more, the code adds a new Swap object to the set.

Now add the following code directly below the code above:

if row < NumRows - 1 {
  if let other = cookies[column, row + 1] {
    cookies[column, row] = other
    cookies[column, row + 1] = cookie
 
    // Is either cookie now part of a chain?
    if hasChainAtColumn(column, row: row + 1) ||
       hasChainAtColumn(column, row: row) {
      set.insert(Swap(cookieA: cookie, cookieB: other))
    }
 
    // Swap them back
    cookies[column, row] = cookie
    cookies[column, row + 1] = other
  }
}

This does exactly the same thing, but for the cookie above instead of on the right.

That should do it. In summary, this algorithm performs a swap for each pair of cookies, checks whether it results in a chain and then undoes the swap, recording every chain it finds.

Now run the app and you should see something like this in the Xcode debug pane:

possible swaps: [
swap type:SugarCookie square:(6,5) with type:Cupcake square:(7,5): true,
swap type:Croissant square:(3,3) with type:Macaroon square:(4,3): true,
swap type:Danish square:(6,0) with type:Macaroon square:(6,1): true,
swap type:Cupcake square:(6,4) with type:SugarCookie square:(6,5): true,
swap type:Croissant square:(4,2) with type:Macaroon square:(4,3): true,
. . .

Block Unwanted Swaps

Let’s put this list of possible moves to good use. Add the following method to Level.swift:

func isPossibleSwap(swap: Swap) -> Bool {
  return possibleSwaps.contains(swap)
}

This looks to see if the set of possible swaps contains the specified Swap object. But wait a minute… when you perform a swipe, GameScene creates a new Swap object. How could isPossibleSwap() possibly find that object inside its list? It may have a Swap object that describes exactly the same move, but the actual instances in memory are different.

When you run set.contains(object), the set calls == on that object and all the objects it contains to determine if they match. Because you already provided an == operator for Swap, this automagically works! It doesn’t matter that the Swap objects are actually different instances; the set will find a match as long as two Swaps can be considered equal.

Finally call the method in GameViewController.swift, inside the handleSwipe() function. Replace the existing handleSwipe() function with the following:

func handleSwipe(swap: Swap) {
  view.userInteractionEnabled = false
 
  if level.isPossibleSwap(swap) {
    level.performSwap(swap)
    scene.animateSwap(swap) {
      self.view.userInteractionEnabled = true
    }
  } else {
     view.userInteractionEnabled = true
  }
}

Now the game will only perform the swap if it’s in the list of sanctioned swaps.

Build and run to try it out. You should only be able to make swaps if they result in a chain.

Ignore invalid swap

Note that after you perform a swap, the “valid swaps” list is now invalid. You’ll fix that in the next part of the series.

It’s also fun to animate attempted swaps that are invalid, so add the following method to GameScene.swift:

func animateInvalidSwap(swap: Swap, completion: () -> ()) {
  let spriteA = swap.cookieA.sprite!
  let spriteB = swap.cookieB.sprite!
 
  spriteA.zPosition = 100
  spriteB.zPosition = 90
 
  let Duration: NSTimeInterval = 0.2
 
  let moveA = SKAction.moveTo(spriteB.position, duration: Duration)
  moveA.timingMode = .EaseOut
 
  let moveB = SKAction.moveTo(spriteA.position, duration: Duration)
  moveB.timingMode = .EaseOut
 
  spriteA.runAction(SKAction.sequence([moveA, moveB]), completion: completion)
  spriteB.runAction(SKAction.sequence([moveB, moveA]))
}

This method is similar to animateSwap(swap:, completion:), but here it slides the cookies to their new positions and then immediately flips them back.

In GameViewController.swift, change the else-clause inside handleSwipe() to:

} else {
  scene.animateInvalidSwap(swap) {
    self.view.userInteractionEnabled = true
  }
}

Now run the app and try to make a swap that won’t result in a chain:

Invalid swap

Add Sound Efffects

Before wrapping up the first part of this tutorial, why don’t you go ahead and add some sound effects to the game? Open the Resources folder for this tutorial and drag the Sounds folder into Xcode.

Add new properties for these sound effects to GameScene.swift:

let swapSound = SKAction.playSoundFileNamed("Chomp.wav", waitForCompletion: false)
let invalidSwapSound = SKAction.playSoundFileNamed("Error.wav", waitForCompletion: false)
let matchSound = SKAction.playSoundFileNamed("Ka-Ching.wav", waitForCompletion: false)
let fallingCookieSound = SKAction.playSoundFileNamed("Scrape.wav", waitForCompletion: false)
let addCookieSound = SKAction.playSoundFileNamed("Drip.wav", waitForCompletion: false)

Rather than recreate an SKAction every time you need to play a sound, you’ll load all the sounds just once and keep re-using them.

Then add the following line to the bottom of animateSwap()

runAction(swapSound)

And add this line to the bottom of animateInvalidSwap():

runAction(invalidSwapSound)

That’s all you need to do to make some noise. Chomp! :]

Where to Go From Here?

Here is the sample project with all of the code from the Swift tutorial up to this point.

Good job on finishing the second part of this four-part tutorial series. There’s still some way to go, but you’ve done a really great job laying down the foundation for your game. You surely deserve another cookie for making it halfway!

In the next part, you’ll work on finding and removing chains and refilling the level with new yummy cookies after successful swipes. It’ll be fun, we promise.

While you have a well-deserved break, take a moment to let us hear from you in the forums :]

Credits: Free game art from Game Art Guppy. The music is by Kevin MacLeod. The sound effects are based on samples from freesound.org.

Portions of the source code were inspired by Gabriel Nica‘s Swift port of the game.

The post How to Make a Game Like Candy Crush with SpriteKit and Swift: Part 2 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4400

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>