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

Video Tutorial: Collection Views Part 6: Deleting Cells


Readers’ App Reviews – May 2016

$
0
0

UnderpantsQuest

It’s May, and for most of us that means getting ready for WWDC – also known as Christmas for iOS developers!

As the most wonderful time of year draws near, the excitement grows among the iOS community. We’ve been making wish lists, predictions, and counting down the remaining days.

At raywenderlich.com, the hard work is just about to begin. WWDC brings tons of new APIs, frameworks, and other changes – and for us that means tons of tutorials to write and update.

In the meantime, our amazing community has continued to release tons of great new apps. I’ve got an inbox full of apps from readers like you to prove it. :]

I receive more than an app a day from fellow readers, and although I don’t have time to write about every app that hits my inbox, hopefully the few I show inspires you to finish your app.

This month we have:

  • An app that will help you defeat writer’s block (or else!)
  • A keyboard to add animated GIFs
  • An app motivate you to complete your goals
  • And of course, much more!

Keep reading to see the the latest apps released by raywenderlich.com readers like you.

GIPHY KEYS. The GIF Keyboard

Giphy
Gifs. Gifs. Gifs. We all love them – no matter how we may pronounce the word! :]

You’re probably familiar with Giphy, the fantastic database of gifs for every occasion easily searchable at giphy.com.

Well get ready, because the gifs are breaking free! Giphy Keys is a new app from Giphy bringing their gif database right to your keyboard. You can easily search for gifs right from any app in your keyboard. Simply type what you’re looking for and gifs appear.

It’s easy to find great gifs even if you don’t have a particular one in mind. There are curated gifs, popular gifs, saved favorites, and more. You can easily find a gif on the fly from text you type, and it even allows you to type a zip code and get a customized gif with current weather conditions!

Best of all, Giphy keys still works as a regular keyboard. There’s no need to keep switching back and forth like some other gif keyboards I’ve tried. Highly recommended!

MotivAction – Set & Achieve Goals

Motivaction
MotivAction is an app to help you achieve your goals.

You simply set your goal and MotivAction holds you to it. MotivAction will track your progress along the way and help you get through the week.

Tasks are powerful and full featured. Set tasks for specific days, by specific times, and sort by difficulty and priority. You can also set subtasks to help break down the big things you need to get done.

Along the way, MotivAction will ask you about your mood to tune your reminders and encouragement. You’ll earn achievements for completed tasks and completed weeks. As you climb that mountain, MotivAction will show you encouraging animations to get you through. Check your stats for the week, month, or even quarter!

If you need a helping hand getting through your week, give MotivAction a try, it might be just the thing you need to push through.

Danger Text – never stop writing

DangerText

Have you ever had suffered from writer’s block? If so, Danger Test is a new app with a solution that might work for you.

Danger Text gives you set time intervals to sit down in front of your keyboard and write – but adds a bit of extra danger.

You start out by saying you want to write for 10 minutes. Once you hit start, the app’s interface melts away and you’re left with a timer, the keyboard, and a blank canvas.

As you type, Danger Text is watching. If you stop for too long it starts to get upset. If you don’t type anything for more than a few seconds, you’ll lose everything you’ve written! Its quite an effective tool to force you to focus. Knowing that you’ll lose what you’ve done so far is a lot of pressure, and it works.

Danger Text has a few modes beyond the standard. If you’re just wanting to write, you can get a “Head Start” sentence to kick you off. If you’re wanting to share some thoughts there is a “Stream of Consciousness” mode that shortens the time to erase to the extreme.

There is also a “No Going Back” mode that disables delete to keep you focused on writing forward. And there is even a “Russian Roulette” mode that randomly deletes at the end even if you didn’t stop if you’re feeling lucky.

To give your short writings a place to go, the app also includes “Danger Times”: a shared collection of Danger Text creations from writers like you, bound by fear and time. You can choose to share your own writings there if you want to give others a taste of what 10 minutes of non stop writing looks like for you!

Repulsion

Repulsion
Repulsion is an addicting and challenging game. It’s similar to some gate based games you may have played but the added physics make it a big more difficult to master.

You’re controlling two simple balls that need to make it through the oncoming gates. Taping the screen will cause the balls to be repelled from each other with immediate force. The longer you hold the press the more force between the balls.

But it’s when you let go that the magic happens: the balls are drawn to each other. They spring to and from as they are never quite able to touch. Its this physical attraction and repulsion that you have to master to keep going.

Each gate will have openings at various spots you must pass through. To align the balls with the gate you’ll need to time your interaction so the balls properly react. Sometimes you need to wait until the last minute to avoid too much separation. Sometimes you need to preempt so the blowback attraction will bring them closer than their natural state.

The result is a challenging game, yet so simple to play. Repulsion definitely sets the new standard for easy to play, difficult to master.

White Noise and Deep Sleep Sounds to Help You and Your Baby Sleep Better

WhiteNoise
Sometimes it can be a little too quiet. This can make it hard to sleep or concentrate for some of us. This app is here to help.

White Noise or background sound can help us sleep and help us focus. It works a couple of different ways. Blocking out distracting noises while also forming sound associations. Perhaps a nice coffee shop background helps you work while rain fall helps you sleep.

This app features all sorts of sounds from household to nature. You can listen to a vacuum cleaner or a hairdryer. There are nature sounds like rain, thunder, or ocean waves. Or you can listen to various white noise or other color based noises.

All the sounds are seamlessly looped and the app supports background audio so you can listen anytime. Airplay is supported if you want to blast it outside of your headphones. Or you can even use your Apple TV to play the sound!

Scala Architectural and Engineering Scale

ScalaRuler
We all need a ruler from time to time. But for some, its a necessary tool to get the job done, and not all rulers are created equal.

Scala Architectural and Engineering Scale is an advanced measurement app that will allow you to precisely measure drawings at any scale at your desk or on the go. The app supports both fixed scales if you already know the scale of your diagrams or variable scales if the drawing you’re working with is unknown. In that case you can simple adjust the scale based on any known elements and the app will lock it in allowing you to measure other relative elements.

Scala Architectural and Engineering Scale supports a variety of scales and units covering common architectural, engineering, and general metric needs. It can also easily convert between units and scales as needed.

The app is free with a variety of larger scales to try. An in app purchase unlocks the smaller, more precise scales you’ll likely use on professional work. Scala Architectural and Engineering Scale is a must have app for architects, engineers, or any professional that needs precise measurements anywhere.

Miss D

MissD
Miss D is a different kind of translating app. It translates one word at a time, beautifully and completely.

Miss D will translate any word into up to 10 different languages from French to Japanese. It take in 90+ languages for the input. The output however is high quality. It includes both written and spoken translations with audio recordings for the 10 different output languages.

Miss D has a few great tricks up it’s sleeve though that make it really unique. In addition to looking great and focusing on multiple translations for a single word. Miss D also pulls Wikipedia content for input words. This can solve a few problems. It can help point out names and places if you weren’t sure what you were translating. It can also help define slang words or multiple meaning words. I found it helpful, I can’t imagine how helpful it could be to a non native speaker.

Miss D also throws in a bit of whimsy matching emoji as well! Translate happy and in addition to feliz and felice (Spanish and Italian) you’ll get a range of happy emoji. I doubt you’ll be using Miss D as an emoji searcher but its a very nice touch that adds some character to an already unique translation app.

Quest for King’s Underpants

UnderpantsQuest
We’ve probably all had it happen to us. We’re getting ready for the day and we realize we’ve misplaced our favorite underpants!

In this game, this time it’s the King’s underpants and he’s relying on you to help find them. There is of course a royal reward awaiting you if you should return with the underpants alive.

To succeed in your quest, you’ll need to explore 75 rooms across 3 floors of the castle. There are enemies to be fought and treasures to be found. You’ll need to talk with the locals for clues on where the underpants might be.

Quest for the King’s Underpants has nostalgically retro 8 bit graphics. There are simple swipe controls to move around the room fighting and collecting. And of course no retro game would be complete without great music and sound effects!

Honorable Mentions

Every month I get way more submissions than I have time to write about. I download every app to try them out, but only have time to review a select few. I love seeing our readers through the apps submitted to me every month.

It’s not a popularity contest or even a favorite picking contest — I just try to share a snapshot of the community through your submissions. Please take a moment and try these other fantastic apps from readers like you.

Go Robo Run (ios)
Editable File
Blox 3D Junior
Biker Buddies
Sub Zero
Experimentum
Jumpox
Oh My Plane
Space Patrol 2016
Esquío: Pro workout creation, execution and sharing

Where To Go From Here?

Each month, I really enjoy seeing what our community of readers comes up with. The apps you build are the reason we keep writing tutorials. Make sure you tell me about your next one, submit here.

If you saw an app your liked, hop to the App Store and leave a review! A good review always makes a dev’s day. And make sure you tell them you’re from raywenderlich.com; this is a community of makers.

If you’ve never made an app, this is the month! Check out our free tutorials to become an iOS star. What are you waiting for – I want to see your app next month.

The post Readers’ App Reviews – May 2016 appeared first on Ray Wenderlich.

Video Tutorial: Collection Views Part 7: Moving Cells

Video Tutorial: Collection Views Part 8: Custom Layout

Video Tutorial: Beginning SpriteKit: Introduction

Video Tutorial: Beginning SpriteKit Part 1: Sprites

Video Tutorial: Beginning SpriteKit Part 2: Game Loop

Open Call for Applications: Android, OS X, and Unity Team

$
0
0

OpenCall-Android-OSX-Unity-Feature

Did you know we have over 50 Android, OS X, and Unity tutorials? This site is about more than just iOS.

To help us continue to expand our Android, OS X, and Unity tutorial offerings, we are currently recruiting some new Android, OS X, and Unity developers to join the tutorial team.

  • The Android and Unity teams are looking for tech editors. Tech editors are incredibly important on our site – they are the guardians that makes sure each tutorial comes out is top-notch.
  • The OS X team is looking for authors. This is your chance to give back to the community and influence the next generation of OS X developers!

Joining one of these teams is a great way to get your foot in the door on our team (and all the special opportunities that involves) – not to mention, getting paid to learn!

Time commitment for tech editors is 1 tech edit / month, and for writers 1 tutorial / 3 months, so it’s easy to fit in to your schedule.

If this sounds interesting, keep reading to find out what’s involved and how to apply!

Why Join Our Team?

Here are the top 5 reasons to join our Android, OS X, or Unity teams:

  1. Learning. You’ll always be learning something new — and will have fun doing it! You’ll become a better developer, writer and person. The best part… you’ll make a lot of new friends along the way.
  2. Money! Get paid to learn! We offer the highest rates in the industry.
  3. Special Opportunities. Members of the Tutorial Team get access to special opportunities such as contributing to our books and products, speaking at our conference, being a guest on our podcast, working on team projects and much more.
  4. You’ll Make a Difference. We get emails every day about how our tutorials help our readers make their first app, get their dream job or accomplish a lifelong dream of making their own app. This means a lot to us, and makes all the hard work worth it!
  5. Free Stuff! And as a final bonus, by joining the Tutorial Team you’ll get a lot of free stuff! You’ll get a free copy of all of the products we sell on the site — over $1,000 in value!

Aww Yeah!

Requirements and How to Apply

Here are the requirements:

  • You must be an experienced Android, OS X, or Unity developer.
  • You should be a great writer with fluent English writing skills.
  • You should be comfortable learning brand new topics that you’ve never done before, which are either not documented or poorly documented.
  • You should have a strong work ethic — this will be a significant time commitment and is not easy.

To apply, send me an email. Be sure to include the following information:

  • Which team are you most interested in (Android, OS X, or Unity)?
  • Please tell me a little bit about yourself and your experience.
  • What is the best app you’ve made or worked on for Android, OS X, or Unity? [Please include link]
  • Please link to any examples of technical writing you’ve done in the past.
  • Please include links to: your GitHub account, your StackOverflow account and your Twitter account.

If your application looks promising, we’ll send you a tryout to gauge your editing skills.

If you pass the tryout, you’re in!

Now’s The Time!

We will likely be starting up a big team project soon for each team (shhh), so this is probably your last chance to get in on it from the ground floor. Don’t let this one pass you by!

Now what are you waiting for – send me that email! The Android, OS X, and Unity team leads and I look forward to creating some great tutorials with you. :]

The post Open Call for Applications: Android, OS X, and Unity Team appeared first on Ray Wenderlich.


Universal Links – Make the Connection

$
0
0
Universal Links

Connecting Apps and Websites

Do you have a website that shares content with an iOS app? As of iOS 9, you can connect them using universal links, meaning that users can now touch an HTTP link on an iPhone and be sent directly to your app!

In this tutorial, you’ll learn how to link an iOS app to a Heroku website. Both the app and website provide reference material for single board computers, so hardware-hobbyist freaks, rejoice! You’re in for a treat. :]

This tutorial assumes you have a basic understanding of Swift and iOS development. If you’re new to these, go through some of our other tutorials on Swift and iOS development first. Previous experience with Heroku and web development would be beneficial, but isn’t strictly required.

Lastly, you must have a paid Apple developer account. A free account won’t work, unfortunately.

Note: Universal links are in many ways a replacement for apps registering their own URL schemes. They work by providing a small file (served over HTTPS) from the root of a web domain that points to a specific app ID, and by that specific app ID being registered with Apple as handling links from that domain.

Because of these requirements, you won’t be able to fully try out universal links without having a real website accessible via HTTPS and a real app on the App Store, but you can still gain experience with universal links by going through the process of setting everything up.

Getting Started

Download the starter project here. Build and run, and you’ll see the app’s main view controller, ComputersController.

UniversalLinks - Main View

Main View

The app shows a few of the most popular single board computers on the market. Touch one of the rows to segue to the details view controller, ComputerDetailController. Here you can see product-specific information, including the website, vendor, and pricing.

UniversalLinks - Navigating

Navigating the Views

A basic website with similar information has also been created. Check out the starter website here.

Single Board Computer's Website

Single Board Computers Website

Don’t fret the Heroku part: it’s just a quick way for you to access the demo website for the tutorial. Though not required by this tutorial, you may optionally deploy the website to your own Heroku account. If you want to do so, follow the instructions here.

Now that you’ve seen both the app and the website, it’s time to dive into the code to connect them. The end goal: whenever a user taps any of your website links on an iOS device—such as https://rw-universal-links-starter.herokuapp.com/arduino.html—it will be opened by the app, instead of Safari. As a final bit of polish, the app will even navigate directly to the specific computer detail controller.

To accomplish this, you need to do three things:

  1. Inform the app of the website.
  2. Inform the website of the app.
  3. Handle navigation within the app whenever a link is tapped.

There are a couple requirements for creating universal links. First, you must have “write access” to the linked website, which must use HTTPS, and you must be able to upload a file to it. Second, you must own the associated app, so you can edit its provisioning profile “capabilities” to register the website and be able to deploy it to the App Store.

For these reasons, you won’t be able to test the tutorial app. However, you’ll still go through the process to learn how it’s done.

Configuring the App

You first need to configure the app to handle universal links. Open the UniversalLinks.xcodeproj from the Starter folder and do the following:

  1. Select the UniversalLinks project.
  2. Select the UniversalLinks target.
  3. Select the Capabilities tab.
  4. Scroll to and turn ON Associated Domains.
Turn On Capabilities

Turn On Capabilities

You may get a prompt to Select a Development Team to Use for Provisioning. Select any paid Apple developer team that you’re on and press Choose to continue.

If you see a prompt to Add an Apple ID Account, it means you’re not currently signed into any accounts. In this case, press the Add button and sign into a paid Apple developer team of which you’re a member.

It’s important that this be a paid account. When I tried to use a free account in my testing, Xcode consistently crashed; this appears to be an Xcode bug.

You’ll also see a error message saying “An App ID with identifier ‘com.razeware.UniversalLinks’ is not available”. This is because the app identifier is already taken by the original tutorial app.

In your own app, make sure your app identifier is unique (following reverse domain name notation). For this tutorial app, however, it’s okay to simply ignore this message. ;]

Next, press + and add the following domain:
applinks:rw-universal-links-final.herokuapp.com

Be sure to add the applinks prefix. You should now see the following:

Associated Domains

Associated Domains

The first step is complete! The app is now aware of the website’s URL. Next, you’ll configure the website.

Configuring the Website

Now you need to let the website in on the “big secret.” To do this, create an apple-app-site-association file. This file isn’t new to the iOS world: it’s the same file used in iOS 8 for web credentials and Handoff. The format matches that of a JSON file, but it CANNOT have the .json extension. No extension should be used, and to preserve your debugging sanity, double-check that the file name matches exactly!

Rage_AppleFile

Create the apple-app-site-association file and add the following content:

{
  "applinks": {
    "apps": [],
    "details": [
    {
      "appID": "KFCNEC27GU.com.razeware.UniveralLinksTutorial",
      "paths": ["*"]
    }
    ]
  }
}

The applinks tag determines which apps are associated with the website. Leave the apps value as an empty array. Inside the details tag is an array of dictionaries for linking appIDs and URL paths. For simplicity, the “*” wildcard character is used to associate all of this website’s links with the UniversalLinks app. Additionally, the paths value can be limited to specific folders or file names.

The appID consists of your team ID combined with the app’s bundle ID. The team ID listed above belongs to the Ray Wenderlich team, but you’ll need to use the identifier for your own account.

Apple assigned you a team ID when creating your Apple developer account. It can be found in the Apple developer center. Log into the website, click on the Your Account tab and scroll down to the Developer Account Summary section. You will find your team ID there.

Developer Account Summary

Developer Account Summary

You can find the app’s bundle ID via Xcode’s Targets\UniversalLinks\General tab:

Bundle ID

Now that the apple-app-site-association is complete, it’s time to upload it to the web server.

Again, you must have “write access” to the website to do this. For the sake of the tutorial, you can refer to the finished website, which already contains this file.

If you want to ensure it is there, open http://rw-universal-links-final.herokuapp.com/apple-app-site-association. You’ll see that it matches the info shown above.

Great! The app knows about the website, and the website knows about the UniversalLinks app. It’s time for the final step: adding app logic to handle when a universal link is tapped.

Handling Universal Links

Now that the app and the website are officially aware of each other, all the app needs is code to handle the link when it’s called.

Open AppDelegate.swift and add the following helper method:

func presentDetailViewController(computer: Computer) {
 
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
 
    let detailVC = storyboard.instantiateViewControllerWithIdentifier("NavigationController")
      as! ComputerDetailController
    detailVC.item = computer
 
    let navigationVC = storyboard.instantiateViewControllerWithIdentifier("DetailController")
      as! UINavigationController
    navigationVC.modalPresentationStyle = .FormSheet
 
    navigationVC.pushViewController(detailVC, animated: true)
  }

This method opens the ComputerDetailController and displays the Computer parameter’s information. This provides you with a clean way of navigating to a specific single board computer. You’ll use this method next.

Right after the previous method, add the following delegate method:

func application(application: UIApplication,
                 continueUserActivity userActivity: NSUserActivity,
                                      restorationHandler: ([AnyObject]?) -> Void) -> Bool {
 
  // 1
  guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
    let url = userActivity.webpageURL,
    let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: true),
    let path = components.path else {
      return false
  }
 
  // 2
  if let computer = ItemHandler.sharedInstance.items.filter({ $0.path == path}).first {
    self.presentDetailViewController(computer)
    return true
  }
 
  // 3
  let webpageUrl = NSURL(string: "http://rw-universal-links-final.herokuapp.com")!
  application.openURL(webpageUrl)
 
  return false
}

This method is called whenever a universal link related to the app is tapped. Here’s what each step does:

  1. First, you verify that the passed-in userActivity has expected characteristics. Ultimately, you want to get the path component for the activity. Otherwise, you return false to indicate that the app can’t handle the activity.
  2. Using the path, you look for a known computer that matches it. If one is found, you present the detail view controller for it and return true.
  3. If no computer that matches the path is found, you instruct the application to open the URL, which will use the default system app instead—most likely Safari. You also return false here, to indicate that the app can’t handle the user activity.

Testing the Links

As discussed above, there isn’t a great way to test whether the universal links work in the tutorial app, but it’s important to understand the expected outcome for when you implement universal links in your own applications.

If you had implemented this in your own app, you would be able to e-mail yourself a link (e.g. https://rw-universal-links-final.herokuapp.com/arduino.html), tap it, and verify that the correct page is shown.

Remember that universal links can be triggered from other places as well, such as within a UIWebView, WKWebView, SFSafariViewController, Notes or directly within Safari.

Where to Go From Here?

Here is the tutorial’s finished sample project, and you’ll find the finished website here: https://rw-universal-links-final.herokuapp.com.

Congratulations! You’ve learned a lot about how to implement universal links in iOS. You’re now ready to apply this concept to your own app and create “real” universal links—you can do it!

Want to dig deeper into this topic? Check out the section on universal links (Chapter 3) in iOS 9 by Tutorials.

You can find additional reference material directly from Apple’s documentation.

To test out your implementation of universal links on your own website, you can use this tool or Apple’s own link validator, both of which can point out possible problems in your setup.

If you have questions or comments, feel free to jump into the forum below and ask away!

The post Universal Links – Make the Connection appeared first on Ray Wenderlich.

Video Tutorial: Beginning SpriteKit Part 3: Univesal App Support

Video Tutorial: Beginning SpriteKit Part 4: Boundaries and the Playable Area

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

$
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.

A little while back, I wrote an Objective-C tutorial about how to make a game like the Candy Crush Saga, which is a very popular casual match-3 game.

But I thought it would be great to make a Swift version as well, hence this post!

In this four-part “How to” tutorial with SpriteKit and Swift series, you’ll learn how to make a game like Candy Crush named Cookie Crunch Adventure. Yum, that sounds even better than candy!

  • (You’re here) In the first part, you’ll put some of the foundation in place. You’ll setup the gameplay view, the sprites, and the logic for loading levels.
  • The second part will continue expanding on the foundation of the game. You’ll focus on detecting swipes and swapping cookies, and will 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.

In the process of going through this tutorial, you’ll get some excellent practice with Swift techniques such as enums, generics, subscripting, closures, and extensions. You’ll also learn a lot about game architecture and best practices.

There’s a lot to cover, so let’s get you started!

Note: This Swift tutorial assumes you have working knowledge of Sprite Kit and Swift. If you’re new to Sprite Kit, check out the Sprite Kit for beginners tutorial or our book, 2D iOS & tvOS Games by Tutorials. For an introduction to Swift, see our Swift tutorial.

Getting Started

Before you continue, download the resources for this Swift tutorial and unpack the zip file. You’ll have a folder containing all the images and sound effects you’ll need later on.

Start up Xcode, go to File\New\Project…, choose the iOS\Application\Game template and click Next. Fill out the options as follows:

  • Product Name: CookieCrunch
  • Language: Swift
  • Game Technology: SpriteKit
  • Devices: iPhone

Click Next, choose a folder for your project and click Create.

This is a portrait-only game, so open the Target Settings screen and in the General tab, make sure only Portrait is checked in the Device Orientation section:

Device orientation

To start importing the graphics files, go to the Resources folder you just downloaded and drag the Sprites.atlas folder into Xcode’s Project Navigator. Make sure Destination: Copy items if needed is checked.

You should now have a blue folder in your project:

Sprites atlas in project navigator

Xcode will automatically pack the images from this folder into a texture atlas when it builds the game. Using a texture atlas as opposed to individual images will dramatically improve your game’s drawing performance.

Note: To learn more about texture atlases and performance, check out Chapter 25 in iOS Games by Tutorials, “Sprite Kit Performance: Texture Atlases”.

There are a few more images to import, but they don’t go into a texture atlas. This is because they are either large full-screen background images (which are more efficient to keep outside of the texture atlas) or images that you will later use from UIKit controls (UIKit controls cannot access images inside texture atlases).

From the Resources/Images folder, drag each of the individual images into the asset catalog:

Device Orientation

Delete the Spaceship image from the asset catalog. This is a sample image that came with the template but you won’t need any spaceships while crunching those tasty cookies! :]

Outside of the asset catalog in the Project Navigator, delete GameScene.sks. You won’t be using Xcode’s built-in level editor for this game.

Great! It’s time to write some code. Replace the contents of GameViewController.swift with the following:

import UIKit
import SpriteKit
 
class GameViewController: UIViewController {
  var scene: GameScene!
 
  override func prefersStatusBarHidden() -> Bool {
    return true
  }
 
  override func shouldAutorotate() -> Bool {
    return true
  }
 
  override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return [UIInterfaceOrientationMask.Portrait, UIInterfaceOrientationMask.PortraitUpsideDown]
  }
 
  override func viewDidLoad() {
    super.viewDidLoad()
 
    // Configure the view.
    let skView = view as! SKView
    skView.multipleTouchEnabled = false
 
    // Create and configure the scene.
    scene = GameScene(size: skView.bounds.size)
    scene.scaleMode = .AspectFill
 
    // Present the scene.
    skView.presentScene(scene)
  }
}

This is mostly boilerplate code that creates the Sprite Kit scene and presents it in the SKView.

For the final piece of setup, replace the contents of GameScene.swift with this:

import SpriteKit
 
class GameScene: SKScene {
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder) is not used in this app")
  }
 
  override init(size: CGSize) {
    super.init(size: size)
 
    anchorPoint = CGPoint(x: 0.5, y: 0.5)
 
    let background = SKSpriteNode(imageNamed: "Background")
    background.size = size
    addChild(background)
  }
}

This loads the background image from the asset catalog and places it in the scene. Because the scene’s anchorPoint is (0.5, 0.5), the background image will always be centered on the screen on all iPhone screen sizes.

Build and run to see what you’ve got so far. Excellent!

Background image

The Cookie Class

This game’s playing field will consist of a grid, 9 columns by 9 rows. Each square of this grid can contain a cookie.

2D grid

Column 0, row 0 is in the bottom-left corner of the grid. Since the point (0,0) is also at the bottom-left of the screen in Sprite Kit’s coordinate system, it makes sense to have everything else “upside down”—at least compared to the rest of UIKit. :]

Note: Wondering why Sprite Kit’s coordinate system is different than UIKit’s? This is because OpenGL ES’s coordinate system has (0, 0) at the bottom-left, and Sprite Kit is built on top of OpenGL ES and Metal since iOS 9 on supported devices.

To learn more about OpenGL ES, we have a video tutorial series for that.

To begin implementing this, you need to create the class representing a cookie object. Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Cookie.swift and click Create.

Replace the contents of Cookie.swift with the following:

import SpriteKit
 
enum CookieType: Int {
  case Unknown = 0, Croissant, Cupcake, Danish, Donut, Macaroon, SugarCookie
}
 
class Cookie {
  var column: Int
  var row: Int
  let cookieType: CookieType
  var sprite: SKSpriteNode?
 
  init(column: Int, row: Int, cookieType: CookieType) {
    self.column = column
    self.row = row
    self.cookieType = cookieType
  }
}

The column and row properties let Cookie keep track of its position in the 2D grid.

The sprite property is optional, hence the question mark after SKSpriteNode, because the cookie object may not always have its sprite set.

The cookieType property describes the—wait for it—type of the cookie, which takes a value from the CookieType enum. The type is really just a number from 1 to 6, but wrapping it in an enum allows you to work with easy-to-remember names instead of numbers.

You will deliberately not use cookie type Unknown (value 0). This value has a special meaning, as you’ll learn toward the end of this part of the tutorial.

Each cookie type number corresponds to a sprite image:

Cookie types

In Swift, an enum isn’t just useful for associating symbolic names with numbers; you can also add functions and computed properties to an enum. Add the following code inside the enum CookieType:

var spriteName: String {
  let spriteNames = [
    "Croissant",
    "Cupcake",
    "Danish",
    "Donut",
    "Macaroon",
    "SugarCookie"]
 
  return spriteNames[rawValue - 1]
}
 
var highlightedSpriteName: String {
  return spriteName + "-Highlighted"
}

The spriteName property returns the filename of the corresponding sprite image in the texture atlas. In addition to the regular cookie sprite, there is also a highlighted version that appears when the player taps on the cookie.

The spriteName and highlightedSpriteName properties simply look up the name for the cookie sprite in an array of strings. To find the index, you use rawValue to convert the enum’s current value to an integer. Recall that the first useful cookie type, Croissant, starts at 1 but arrays are indexed starting at 0, so you need to subtract 1 to find the correct array index.

Every time a new cookie gets added to the game, it will get a random cookie type. It makes sense to add that as a function on CookieType. Add the following to the enum as well:

static func random() -> CookieType {
  return CookieType(rawValue: Int(arc4random_uniform(6)) + 1)!
}

This calls arc4random_uniform() to generate a random number between 0 and 5, then adds 1 to make it a number between 1 and 6. Because Swift is very strict, the result from arc4random_uniform() (an UInt32) must first be converted to an Int, and you can convert this number into a proper CookieType value.

Now, you may wonder why you’re not making Cookie a subclass of SKSpriteNode. After all, the cookie is something you want to display on the screen.

If you’re familiar with the model-view-controller (or MVC) pattern, think of Cookie as a model object that simply describes the data for the cookie. The view is a separate object, stored in the sprite property.

This kind of separation between data models and views is something you’ll use consistently throughout this tutorial. The MVC pattern is more common in regular apps than in games but, as you’ll see, it can help keep the code clean and flexible.

Printing Cookies

If you were to use print to print out a Cookie at the moment, it wouldn’t look very nice.

What you’d like is to customize the output of when you print a cookie. You can do this by making the Cookie conform to the CustomStringConvertible protocol.

To do this, modify the declaration of Cookie as follows:

class Cookie: CustomStringConvertible {

Then add a computed property named description:

var description: String {
  return "type:\(cookieType) square:(\(column),\(row))"
}

Now print() will print out something helpful: the type of cookie and its column and row in the level grid. You’ll use this in practice later.

print_cookies

Let’s also make the CookieType enum printable. Add the CustomStringConvertible protocol to the enum definition and have the description property return the sprite name, which is a pretty good description of the cookie type:

enum CookieType: Int, CustomStringConvertible {
  ...
  var description: String {
    return spriteName
  }
}

Working with 2D Arrays

Now you need something to hold that 9×9 grid of cookies. The Objective-C version of this tutorial did this,

Cookie *_cookies[9][9];

to create a two-dimensional array of 81 elements. Then you could simply do myCookie = _cookies[3][6]; to find the cookie at column 3, row 6.

Swift arrays, however, work quite differently from plain old C arrays and the above is not possible. Fortunately, you can create your own type that acts like a 2D array and that is just as convenient to use.

Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Array2D.swift and click Create.

Replace the contents of Array2D.swift with the following:

struct Array2D<T> {
  let columns: Int
  let rows: Int
  private var array: Array<T?>
 
  init(columns: Int, rows: Int) {
    self.columns = columns
    self.rows = rows
    array = Array<T?>(count: rows*columns, repeatedValue: nil)
  }
 
  subscript(column: Int, row: Int) -> T? {
    get {
      return array[row*columns + column]
    }
    set {
      array[row*columns + column] = newValue
    }
  }
}

The notation Array2D<T> means that this struct is a generic; it can hold elements of any type T. You’ll use Array2D to store Cookie objects, but later on in the tutorial you’ll use another Array2D to store a different type of object, Tile.

Array2D‘s initializer creates a regular Swift Array with a count of rows × columns and sets all these elements to nil. When you want a value to be nil in Swift, it needs to be declared optional, which is why the type of the array property is Array<T?> and not just Array<T>.

What makes Array2D easy to use is that is supports subscripting. If you know the column and row numbers of a specific item, you can index the array as follows: myCookie = cookies[column, row]. Sweet!

The Level Class

That’s the preliminaries out of the way. Let’s put Array2D to use.

First, however, a minor change to the Cookie.swift class. Cookies will later be used in a Set and the objects that you put into the set must conform to the Hashable protocol. That’s a requirement from Swift. Right now, Cookie does not conform to Hashable.

Switch to Cookie.swift and change the class declaration to include Hashable:

class Cookie: CustomStringConvertible, Hashable {

Add the following property inside the class:

var hashValue: Int {
  return row*10 + column
}

The Hashable protocol requires that you add a hashValue property to the object. This should return an Int value that is as unique as possible for your object. Its position in the 2D grid is enough to identify each cookie, and you’ll use that to generate the hash value.

Also add the following function outside the Cookie class:

func ==(lhs: Cookie, rhs: Cookie) -> Bool {
  return lhs.column == rhs.column && lhs.row == rhs.row
}

Whenever you add the Hashable protocol to an object, you also need to supply the == comparison operator for comparing two objects of the same type. That’s it! The Cookie class is now ready to be used within a Set.

Next, go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Level.swift and click Create.

Replace the contents of Level.swift with the following:

import Foundation
 
let NumColumns = 9
let NumRows = 9
 
class Level {
  private var cookies = Array2D<Cookie>(columns: NumColumns, rows: NumRows)
}

This declares two constants for the dimensions of the level, NumColumns and NumRows, so you don’t have to hardcode the number 9 everywhere.

The property cookies is the two-dimensional array that holds the Cookie objects, 81 in total (9 rows of 9 columns).

The cookies array is private, so Level needs to provide a way for others to obtain a cookie object at a specific position in the level grid.

Add the code for this method to Level.swift:

func cookieAtColumn(column: Int, row: Int) -> Cookie? {
  assert(column >= 0 && column < NumColumns)
  assert(row >= 0 && row < NumRows)
  return cookies[column, row]
}

Using cookieAtColumn(3, row: 6) you can ask the Level for the cookie at column 3, row 6. Behind the scenes this asks the Array2D for the cookie and then returns it. Note that the return type is Cookie?, an optional, because not all grid squares will necessarily have a cookie (they may be nil).

Notice the use of assert() to verify that the specified column and row numbers are within the valid range of 0-8.

Note: New to assert? The idea behind assert is you give it a condition, and if the condition fails the app will crash with a log message.

“Wait a minute,” you may think, “why would I want to crash my app on purpose?!”

Crashing your app on purpose is actually a good thing if you have a condition that you don’t expect to ever happen in your app like this one. assert will help you because when the app crashes, the backtrace will point exactly to this unexpected condition, making it nice and easy to resolve the source of the problem.

Now to fill up that cookies array with some cookies! Later on you will learn how to read level designs from a JSON file but for now, you’ll fill up the array yourself, just so there is something to show on the screen.

Add the following two methods to Level.swift:

func shuffle() -> Set<Cookie> {
  return createInitialCookies()
}
 
private func createInitialCookies() -> Set<Cookie> {
  var set = Set<Cookie>()
 
  // 1
  for row in 0..<NumRows {
    for column in 0..<NumColumns {
 
      // 2
      var cookieType = CookieType.random()
 
      // 3
      let cookie = Cookie(column: column, row: row, cookieType: cookieType)
      cookies[column, row] = cookie
 
      // 4
      set.insert(cookie)
    }
  }
  return set
}

Both methods return a Set<Cookie> object. A Set is a collection, like an array, but it allows each element to appear only once, and it does not store the elements in any particular order.

The shuffle method fills up the level with random cookies. Right now it just calls createInitialCookies(), where the real work happens. Here’s what it does, step by step:

  1. The method loops through the rows and columns of the 2D array. This is something you’ll see a lot in this tutorial. Remember that column 0, row 0 is in the bottom-left corner of the 2D grid.
  2. Then the method picks a random cookie type. This uses the random() function you added to the CookieType enum earlier.
  3. Next, the method creates a new Cookie object and adds it to the 2D array.
  4. Finally, the method adds the new Cookie object to a Set. shuffle returns this set of cookie objects to its caller.

One of the main difficulties when designing your code is deciding how the different objects will communicate with each other. In this game, you often accomplish this by passing around a collection of objects, usually a Set or Array.

In this case, after you create a new Level object and call shuffle to fill it up with cookies, the Level replies, “Here is a set with all the new Cookie objects I just added.” You can take that set and, for example, create new sprites for all the cookie objects it contains. In fact, that’s exactly what you’ll do in the next section.

Press Command+B to build the app and make sure you’re not getting any compilation errors.

The Scene and the View Controller

In many Sprite Kit games, the “scene” is the main object for the game. In Cookie Crunch, however, you’ll make the view controller play that role.

Why? The game will include UIKit elements, such as labels, and it makes sense for the view controller to manage them. You’ll still have a scene object—GameScene from the template—but this will only be responsible for drawing the sprites; it won’t handle any of the game logic.

MVC

Cookie Crunch will use an architecture that is very much like the model-view-controller or MVC pattern that you may know from non-game apps:

  • The data model will consist of Level, Cookie and a few other classes. The models will contain the data, such as the 2D grid of cookie objects, and handle most of the gameplay logic.
  • The views will be GameScene and the SKSpriteNodes on the one hand, and UIViews on the other. The views will be responsible for showing things on the screen and for handling touches on those things. The scene in particular will draw the cookie sprites and detect swipes.
  • The view controller will play the same role here as in a typical MVC app: it will sit between the models and the views and coordinate the whole shebang.

All of these objects will communicate with each other, mostly by passing arrays and sets of objects to be modified. This separation will give each object only one job that it can do, totally independent of the others, which will keep the code clean and easy to manage.

Note: Putting the game data and rules in separate model objects is especially useful for unit testing. This tutorial doesn’t cover unit testing but, for a game such as this, it’s a good idea to have a comprehensive set of tests for the game rules. To learn more about unit testing, check out our Unit Testing Basics video series.

If game logic and sprites are all mixed up, then it’s hard to write such tests, but in this case you can test Level separate from the other components. This kind of testing lets you add new game rules with confidence you didn’t break any of the existing ones.

Open GameScene.swift and add the following properties to the class:

var level: Level!
 
let TileWidth: CGFloat = 32.0
let TileHeight: CGFloat = 36.0
 
let gameLayer = SKNode()
let cookiesLayer = SKNode()

The scene has a public property to hold a reference to the current level. This variable is marked as Level! with an exclamation point because it will not initially have a value.

Each square of the 2D grid measures 32 by 36 points, so you put those values into the TileWidth and TileHeight constants. These constants will make it easier to calculate the position of a cookie sprite.

To keep the Sprite Kit node hierarchy neatly organized, GameScene uses several layers. The base layer is called gameLayer. This is the container for all the other layers and it’s centered on the screen. You’ll add the cookie sprites to cookiesLayer, which is a child of gameLayer.

Add the following lines to init(size:) to add the new layers. Put this after the code that creates the background node:

addChild(gameLayer)
 
let layerPosition = CGPoint(
    x: -TileWidth * CGFloat(NumColumns) / 2,
    y: -TileHeight * CGFloat(NumRows) / 2)
 
cookiesLayer.position = layerPosition
gameLayer.addChild(cookiesLayer)

This adds two empty SKNodes to the screen to act as layers. You can think of these as transparent planes you can add other nodes in.

Remember that earlier you set the anchorPoint of the scene to (0, 0), and the position of the scene also defaults to (0, 0). This means (0, 0) is in the center of the screen. Therefore, when you add these layers as children of the scene, the point (0, 0) in layer coordinates will also be in the center of the screen.

However, because column 0, row 0 is in the bottom-left corner of the 2D grid, you want the positions of the sprites to be relative to the cookiesLayer’s bottom-left corner, as well. That’s why you move the layer down and to the left by half the height and width of the grid.

Note: Because NumColumns and NumRows are of type Int but CGPoint‘s x and y fields are of type CGFloat, you have to convert these values by writing CGFloat(NumColumns). You’ll see this sort of thing a lot in Swift code.

Adding the sprites to the scene happens in addSpritesForCookies(). Add it below:

func addSpritesForCookies(cookies: Set<Cookie>) {
  for cookie in cookies {
    let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
    sprite.size = CGSize(width: TileWidth, height: TileHeight)
    sprite.position = pointForColumn(cookie.column, row:cookie.row)
    cookiesLayer.addChild(sprite)
    cookie.sprite = sprite
  }
}
 
func pointForColumn(column: Int, row: Int) -> CGPoint {
  return CGPoint(
          x: CGFloat(column)*TileWidth + TileWidth/2,
          y: CGFloat(row)*TileHeight + TileHeight/2)
}

addSpritesForCookies() iterates through the set of cookies and adds a corresponding SKSpriteNode instance to the cookie layer. This uses a helper method, pointForColumn(column:, row:), that converts a column and row number into a CGPoint that is relative to the cookiesLayer. This point represents the center of the cookie’s SKSpriteNode.

Hop over to GameViewController.swift and add a new property to the class:

var level: Level!

Next, add these two new methods:

func beginGame() {
  shuffle()
}
 
func shuffle() {
  let newCookies = level.shuffle()
  scene.addSpritesForCookies(newCookies)
}

beginGame() kicks off the game by calling shuffle(). This is where you call Level’s shuffle() method, which returns the Set containing new Cookie objects. Remember that these cookie objects are just model data; they don’t have any sprites yet. To show them on the screen, you tell GameScene to add sprites for those cookies.

The only missing piece is creating the actual Level instance. Add the following lines in viewDidLoad(), just before the code that presents the scene:

level = Level()
scene.level = level

After creating the new Level instance, you set the level property on the scene to tie together the model and the view.

Note: The reason you declared the var level property as Level!, with an exclamation point, is that all properties must have a value by the time the class is initialized. But you can’t give level a value in init() yet; that doesn’t happen until viewDidLoad. With the ! you tell Swift that this variable won’t have a value until later (but once it’s set, it will never become nil again).

Finally, make sure you call beginGame() at the end of viewDidLoad() to set things in motion:

override func viewDidLoad() {
   ...
   beginGame()
}

Build and run, and you should finally see some cookies:

First cookies

Loading Levels from JSON Files

Not all the levels in Candy Crush Saga have grids that are a simple square shape. You will now add support for loading level designs from JSON files. The five designs you’re going to load still use the same 9×9 grid, but they leave some of the squares blank.

Drag the Levels folder from the tutorial’s Resources folder into your Xcode project. As always, make sure Destination: Copy items if needed is checked. This folder contains five JSON files:

Levels in project navigator

Click on Level_1.json to look inside. You’ll see that the contents are structured as a dictionary containing three elements: tiles, targetScore and moves.

The tiles array contains nine other arrays, one for each row of the level. If a tile has a value of 1, it can contain a cookie; a 0 means the tile is empty.

Level_1 json

You’ll load this data in Level, but first you need to add a new class, Tile, to represent a single tile in the 2D level grid. Note that a tile is different than a cookie — think of tiles as “slots”, and of cookies as the things inside the slots. I’ll discuss more about this in a bit.

Add a new Swift File to the project. Name it Tile.swift. Replace the contents of this file with:

class Tile {
}

You can leave this new class empty right now. Later on, I’ll give you some hints for how to use this class to add additional features to the game, such as “jelly” tiles.

Open Level.swift and add a new property and method:

private var tiles = Array2D<Tile>(columns: NumColumns, rows: NumRows)
 
func tileAtColumn(column: Int, row: Int) -> Tile? {
  assert(column >= 0 && column < NumColumns)
  assert(row >= 0 && row < NumRows)
  return tiles[column, row]
}

The tiles variable describes the structure of the level. This is very similar to the cookies array, except now you make it an Array2D of Tile objects.

Whereas the cookies array keeps track of the Cookie objects in the level, tiles simply describes which parts of the level grid are empty and which can contain a cookie:

JSON and tiles

Wherever tiles[a, b] is nil, the grid is empty and cannot contain a cookie.

Now that the instance variables for level data are in place, you can start adding the code to fill in the data. The top-level item in the JSON file is a dictionary, so it makes sense to add the code for loading the JSON file to Swift’s Dictionary.

Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Extensions.swift and click Create.

Replace the contents of Extensions.swift with the following:

import Foundation
 
extension Dictionary {
  static func loadJSONFromBundle(filename: String) -> Dictionary <String, AnyObject>? {
    var dataOK: NSData
    var dictionaryOK: NSDictionary = NSDictionary()
    if let path = NSBundle.mainBundle().pathForResource(filename, ofType: "json") {
      let _: NSError?
      do {
        let data = try NSData(contentsOfFile: path, options: NSDataReadingOptions()) as NSData!
        dataOK = data
      }
      catch {
        print("Could not load level file: \(filename), error: \(error)")
        return nil
      }
      do {
        let dictionary = try NSJSONSerialization.JSONObjectWithData(dataOK, options: NSJSONReadingOptions()) as AnyObject!
        dictionaryOK = (dictionary as! NSDictionary as? Dictionary <String, AnyObject>)!
      }
      catch {
        print("Level file '\(filename)' is not valid JSON: \(error)")
        return nil
      }
    }
    return dictionaryOK as? Dictionary <String, AnyObject>
  }
}

Using Swift’s extension mechanism you can add new methods to existing types. Here you have added loadJSONFromBundle() to load a JSON file from the app bundle, into a new dictionary of type Dictionary<String, AnyObject>. This means the dictionary’s keys are always strings but the associated values can be any type of object.

The method simply loads the specified file into an NSData object and then converts that to a Dictionary using the NSJSONSerialization API. This is mostly boilerplate code that you’ll find in any app that deals with JSON files.

Note: To learn more about JSON and parsing it in iOS, check out our Working with JSON Tutorial.

Next, add the new init(filename:) initializer to Level.swift:

init(filename: String) {
  // 1
  guard let dictionary = Dictionary<String, AnyObject>.loadJSONFromBundle(filename) else { return }
  // 2
  guard let tilesArray = dictionary["tiles"] as? [[Int]] else { return }
  // 3
  for (row, rowArray) in tilesArray.enumerate() {
    // 4
    let tileRow = NumRows - row - 1
    // 5
    for (column, value) in rowArray.enumerate() {
      if value == 1 {
        tiles[column, tileRow] = Tile()
      }
    }
  }
}

Here’s what this initializer does, step-by-step:

  1. Load the named file into a Dictionary using the loadJSONFromBundle() helper function that you just added. Note that this function may return nil — it returns an optional — and here you use a guard to handle this situation.
  2. The dictionary has an array named “tiles”. This array contains one element for each row of the level. Each of those row elements is itself an array containing the columns for that row. The type of tilesArray is therefore array-of-array-of-Int, or [[Int]].
  3. Step through the rows using built-in enumerate() function, which is useful because it also returns the current row number.
  4. In Sprite Kit (0, 0) is at the bottom of the screen, so you have to reverse the order of the rows here. The first row you read from the JSON corresponds to the last row of the 2D grid.
  5. Step through the columns in the current row. Every time it finds a 1, it creates a Tile object and places it into the tiles array.

You still need to put this new tiles array to good use. Inside createInitialCookies(), add an if-clause inside the two for-loops, around the code that creates the Cookie object:

// This line is new
if tiles[column, row] != nil {
 
  var cookieType = ...
  ...
  set.insert(cookie)
}

Now the app will only create a Cookie object if there is a tile at that spot.

One last thing remains: In GameViewController.swift’s viewDidLoad(), replace the line that creates the level object with:

level = Level(filename: "Level_1")

Build and run, and now you should have a non-square level shape:

Non-square level

Making the Tiles Visible

To make the cookie sprites stand out from the background a bit more, you can draw a slightly darker “tile” sprite behind each cookie. The texture atlas already contains an image for this (Tile.png). These new tile sprites will live on their own layer, the tilesLayer.

To do this, first add a new private property to GameScene.swift:

let tilesLayer = SKNode()

Then add this code to init(size:), right above where you add the cookiesLayer:

tilesLayer.position = layerPosition
gameLayer.addChild(tilesLayer)

It needs to be done first so the tiles appear behind the cookies (Sprite Kit nodes with the same zPosition are drawn in order of how they were added).

Add the following method to GameScene.swift, as well:

func addTiles() {
  for row in 0..<NumRows {
    for column in 0..<NumColumns {
      if level.tileAtColumn(column, row: row) != nil {
        let tileNode = SKSpriteNode(imageNamed: "Tile")
        tileNode.size = CGSize(width: TileWidth, height: TileHeight)
        tileNode.position = pointForColumn(column, row: row)
        tilesLayer.addChild(tileNode)
      }
    }
  }
}

This loops through all the rows and columns. If there is a tile at that grid square, then it creates a new tile sprite and adds it to the tiles layer.

Next, open GameViewController.swift. Add the following line to viewDidLoad(), immediately after you set scene.level:

scene.addTiles()

Build and run, and you can clearly see where the tiles are:

Tiles layer

You can switch to another level design by specifying a different file name in viewDidLoad(). Simply change the filename: parameter to “Level_2”, “Level_3” or “Level_4” and build and run. Does Level 3 remind you of anything? :]

Feel free to make your own designs, too! Just remember that the “tiles” array should contain nine arrays (one for each row), with nine numbers each (one for each column).

Where to Go From Here?

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

Your game is shaping up nicely, but there’s still a way to go before it’s finished. For now give yourself a cookie for making it through part one!

In the next part, you’ll work on detecting swipes and swapping cookies. You’re in for a treat ;]

While you eat your cookie, take a moment to let us hear from you in the forums!

Credits: Free game art from Game Art Guppy.

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 1 appeared first on Ray Wenderlich.

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.

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

$
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.

Once again, welcome back to our “How to Make a Game Like Candy Crush” tutorial with SpriteKit and Swift series. This is the third instalment in the four-part series, and we’re excited to get your game past the foundations :]

  • 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.
  • In the second part you continued expanding on the foundation of the game. You focused on detecting swipes and swapping cookies, as well as creating some nice visual effects for your game.
  • (You’re here) 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 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.

Let’s get started! :]

Getting Started

Everything you’ve worked on so far has been to allow the player to swap cookies. Next, your game needs to process the results of the swaps.

Swaps always lead to a chain of three or more matching cookies. The next thing to do is to remove those matching cookies from the screen and reward the player with some points.

This is the sequence of events:

Game flow

You’ve already done the first three steps: filling the level with cookies, calculating possible swaps and waiting for the player to make a swap. In this part of the Swift tutorial, you’ll implement the remaining steps.

Finding the Chains

At this point in the game flow, the player has made her move and swapped two cookies. Because the game only lets the player make a swap if it will result in a chain of three or more cookies of the same type, you know there is now at least one chain—but there could be additional chains, as well.

Before you can remove the matching cookies from the level, you first need to find all the chains. That’s what you’ll do in this section.

First, make a class that describes a chain. Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Chain.swift and click Create.

Replace the contents of Chain.swift with this:

class Chain: Hashable, CustomStringConvertible {
  var cookies = [Cookie]()
 
  enum ChainType: CustomStringConvertible {
    case Horizontal
    case Vertical
 
    var description: String {
      switch self {
      case .Horizontal: return "Horizontal"
      case .Vertical: return "Vertical"
      }
    }
  }
 
  var chainType: ChainType
 
  init(chainType: ChainType) {
    self.chainType = chainType
  }
 
  func addCookie(cookie: Cookie) {
    cookies.append(cookie)
  }
 
  func firstCookie() -> Cookie {
    return cookies[0]
  }
 
  func lastCookie() -> Cookie {
    return cookies[cookies.count - 1]
  }
 
  var length: Int {
    return cookies.count
  }
 
  var description: String {
    return "type:\(chainType) cookies:\(cookies)"
  }
 
  var hashValue: Int {
    return cookies.reduce (0) { $0.hashValue ^ $1.hashValue }
  }
}
 
func ==(lhs: Chain, rhs: Chain) -> Bool {
  return lhs.cookies == rhs.cookies
}

A chain has a list of cookie objects and a type: It’s either horizontal (a row of cookies) or vertical (a column). The type is defined as an enum; it is nested inside the Chain class because these two things are tightly coupled. If you feel adventurous, you can also add more complex chain types, such as L- and T-shapes.

There is a reason you’re using an array here to store the cookie objects and not a Set: It’s convenient to remember the order of the cookie objects so that you know which cookies are at the ends of the chain. This makes it easier to combine multiple chains into a single one to detect those L- or T-shapes.

Note: The chain implements Hashable so it can be placed inside a Set. The code for hashValue may look strange but it simply performs an exclusive-or on the hash values of all the cookies in the chain. The reduce() function is one of Swift’s more advanced functional programming features.

To start putting these new chain objects to good use, open Level.swift. You’re going to add a method named removeMatches(), but before you get to that, you need a couple of helper methods to do the heavy lifting of finding chains.

To find a chain, you’ll need a pair of for loops that step through each square of the level grid.

Finding chains

While stepping through the cookies in a row horizontally, you want to find the first cookie that starts a chain.

You know a cookie begins a chain if at least the next two cookies on its right are of the same type. Then you skip over all the cookies that have that same type until you find one that breaks the chain. You repeat this until you’ve looked at all the possibilities.

Add this method to Level.swift to scan for horizontal cookie matches:

private func detectHorizontalMatches() -> Set<Chain> {
  // 1
  var set = Set<Chain>()
  // 2
  for row in 0..<NumRows {
    var column = 0
    while column < NumColumns-2 {
      // 3
      if let cookie = cookies[column, row] {
        let matchType = cookie.cookieType
        // 4  
        if cookies[column + 1, row]?.cookieType == matchType &&
           cookies[column + 2, row]?.cookieType == matchType {
          // 5
          let chain = Chain(chainType: .Horizontal)
          repeat {
            chain.addCookie(cookies[column, row]!)
            column += 1
          } while column < NumColumns && cookies[column, row]?.cookieType == matchType
 
          set.insert(chain)
          continue
        }
      }
      // 6
      column += 1
    }
  }
  return set
}

Here’s how this method works, step by step:

  1. You create a new set to hold the horizontal chains (Chain objects). Later, you’ll remove the cookies in these chains from the playing field.
  2. You loop through the rows and columns. Note that you don’t need to look at the last two columns because these cookies can never begin a new chain.
  3. You skip over any gaps in the level design.
  4. You check whether the next two columns have the same cookie type. Normally you have to be careful not to step outside the bounds of the array when doing something like cookies[column + 2, row], but here that can’t go wrong. That’s why the for loop only goes up to NumColumns - 2. Also note the use of optional chaining with the question mark.
  5. At this point, there is a chain of at least three cookies but potentially there are more. This steps through all the matching cookies until it finds a cookie that breaks the chain or it reaches the end of the grid. Then it adds all the matching cookies to a new Chain object. You increment column for each match.
  6. If the next two cookies don’t match the current one or if there is an empty tile, then there is no chain, so you skip over the cookie.

Note: If there’s a gap in the grid, the use of optional chaining — the question mark after cookies[column, row]? — makes sure the while loop terminates at that point. So the logic above also works on levels with empty squares. Neat!

Next, add this method to scan for vertical cookie matches:

private func detectVerticalMatches() -> Set<Chain> {
  var set = Set<Chain>()
 
  for column in 0..<NumColumns {
    var row = 0
    while row < NumRows-2 {
      if let cookie = cookies[column, row] {
        let matchType = cookie.cookieType
 
        if cookies[column, row + 1]?.cookieType == matchType &&
           cookies[column, row + 2]?.cookieType == matchType {
          let chain = Chain(chainType: .Vertical)
          repeat {
            chain.addCookie(cookies[column, row]!)
            row += 1
          } while row < NumRows && cookies[column, row]?.cookieType == matchType
 
          set.insert(chain)
          continue
        }
      }
      row += 1
    }
  }
  return set
}

The vertical version has the same kind of logic, but loops by column in the outer while loop and by row in the inner loop.

You may wonder why you don’t immediately remove the cookies from the level as soon as you detect that they’re part of a chain. The reason is that a cookie may be part of two chains at the same time: one horizontal and one vertical. So you don’t want to remove it until you’ve checked both the horizontal and vertical options.

Now that the two match detectors are ready, add the implementation for removeMatches():

func removeMatches() -> Set<Chain> {
  let horizontalChains = detectHorizontalMatches()
  let verticalChains = detectVerticalMatches()
 
  print("Horizontal matches: \(horizontalChains)")
  print("Vertical matches: \(verticalChains)")
 
  return horizontalChains.union(verticalChains)
}

This method calls the two helper methods and then combines their results into a single set. Later, you’ll add more logic to this method but for now you’re only interested in finding the matches and returning the set.

You still need to call removeMatches() from somewhere and that somewhere is GameViewController.swift. Add this helper method:

func handleMatches() {
  let chains = level.removeMatches()
  // TODO: do something with the chains set
}

Later, you’ll fill out this method with code to remove cookie chains and drop other cookies into the empty tiles.

In handleSwipe(), change the call to scene.animateSwap() to this:

scene.animateSwap(swap, completion: handleMatches)

Recall that in Swift a closure and a function are really the same thing, so instead of passing a closure block to animateSwap(), you can also give it the name of a function.

Build and run, and swap two cookies to make a chain. You should now see something like this in Xcode’s debug pane:

List of matches

Removing Chains

Level’s method is called “removeMatches”, but so far it only detects the matching chains. Now you’re going to remove those cookies from the game with a nice animation.

First, you need to update the data model by removing the Cookie objects from the array for the 2D grid. When that’s done, you can tell GameScene to animate the sprites for these cookies out of existence.

eatcookies

Removing the cookies from the model is simple enough. Add the following method to Level.swift:

private func removeCookies(chains: Set<Chain>) {
  for chain in chains {
    for cookie in chain.cookies {
      cookies[cookie.column, cookie.row] = nil
    }
  }
}

Each chain has a list of cookie objects and each cookie knows its column and row in the grid, so you simply set that element in the array to nil to remove the cookie object from the data model.

Note: At this point, the Chain object is the only owner of the Cookie object. When the chain gets deallocated, so will these cookie objects.

In removeMatches(), replace the print() statements with the following:

removeCookies(horizontalChains)
removeCookies(verticalChains)

That takes care of the data model. Now switch to GameScene.swift and add the following method:

func animateMatchedCookies(chains: Set<Chain>, completion: () -> ()) {
  for chain in chains {
    for cookie in chain.cookies {
      if let sprite = cookie.sprite {
        if sprite.actionForKey("removing") == nil {
          let scaleAction = SKAction.scaleTo(0.1, duration: 0.3)
          scaleAction.timingMode = .EaseOut
          sprite.runAction(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),
                           withKey:"removing")
        }
      }
    }
  }
  runAction(matchSound)
  runAction(SKAction.waitForDuration(0.3), completion: completion)
}

This loops through all the chains and all the cookies in each chain, and then triggers the animations.

Because the same Cookie could be part of two chains (one horizontal and one vertical), you need to make sure to add only one animation to the sprite, not two. That’s why the action is added to the sprite under the key “removing”. If such an action already exists, you shouldn’t add a new animation to the sprite.

When the shrinking animation is done, the sprite is removed from the cookie layer. The waitForDuration() action at the end of the method ensures that the rest of the game will only continue after the animations finish.

Open GameViewController.swift and change handleMatches() to call this new animation:

func handleMatches() {
  let chains = level.removeMatches()
 
  scene.animateMatchedCookies(chains) {
    self.view.userInteractionEnabled = true
  }
}

Try it out. Build and run, and make some matches.

Match animation

Note: You don’t want the player to be able to tap or swipe on anything while the chain removal animations are happening. That’s why you disable userInteractionEnabled as the first thing in the swipe handler and enable it again once all the animations are done.

Dropping Cookies Into Empty Tiles

Removing the cookie chains leaves holes in the grid. Other cookies should now fall down to fill up those holes. Again, you’ll tackle this in two steps:

  1. Update the model.
  2. Animate the sprites.

Add this new method to Level.swift:

func fillHoles() -> [[Cookie]] {
  var columns = [[Cookie]]()
  // 1
  for column in 0..<NumColumns {
    var array = [Cookie]()
    for row in 0..<NumRows {
      // 2
      if tiles[column, row] != nil && cookies[column, row] == nil {
        // 3
        for lookup in (row + 1)..<NumRows {
          if let cookie = cookies[column, lookup] {
            // 4
            cookies[column, lookup] = nil
            cookies[column, row] = cookie
            cookie.row = row
            // 5
            array.append(cookie)
            // 6
            break
          }
        }
      }
    }
    // 7
    if !array.isEmpty {
      columns.append(array)
    }
  }
  return columns
}

This method detects where there are empty tiles and shifts any cookies down to fill up those tiles. It starts at the bottom and scans upward. If it finds a square that should have a cookie but doesn’t, then it finds the nearest cookie above it and moves this cookie to the empty tile.

Filling holes

Here is how it all works, step by step:

  1. You loop through the rows, from bottom to top.
  2. If there’s a tile at a position but no cookie, then there’s a hole. Remember that the tiles array describes the shape of the level.
  3. You scan upward to find the cookie that sits directly above the hole. Note that the hole may be bigger than one square (for example, if this was a vertical chain) and that there may be holes in the grid shape, as well.
  4. If you find another cookie, move that cookie to the hole. This effectively moves the cookie down.
  5. You add the cookie to the array. Each column gets its own array and cookies that are lower on the screen are first in the array. It’s important to keep this order intact, so the animation code can apply the correct delay. The farther up the piece is, the bigger the delay before the animation starts.
  6. Once you’ve found a cookie, you don’t need to scan up any farther so you break out of the inner loop.
  7. If a column does not have any holes, then there’s no point in adding it to the final array.

At the end, the method returns an array containing all the cookies that have been moved down, organized by column.

Note: The return type of fillHoles() is [[Cookie]], or an array-of-array-of-cookies. You can also write this as Array<Array<Cookie>>.

You’ve already updated the data model for these cookies with the new positions, but the sprites need to catch up. GameScene will animate the sprites and GameViewController is the in-between object to coordinate between the the model (Level ) and the view (GameScene).

Switch to GameScene.swift and add a new animation method:

func animateFallingCookies(columns: [[Cookie]], completion: () -> ()) {
  // 1
  var longestDuration: NSTimeInterval = 0
  for array in columns {
    for (idx, cookie) in array.enumerate() {
      let newPosition = pointForColumn(cookie.column, row: cookie.row)
      // 2
      let delay = 0.05 + 0.15*NSTimeInterval(idx)
      // 3
      let sprite = cookie.sprite!
      let duration = NSTimeInterval(((sprite.position.y - newPosition.y) / TileHeight) * 0.1)
      // 4
      longestDuration = max(longestDuration, duration + delay)
      // 5
      let moveAction = SKAction.moveTo(newPosition, duration: duration)
      moveAction.timingMode = .EaseOut
      sprite.runAction(
        SKAction.sequence([
          SKAction.waitForDuration(delay),
          SKAction.group([moveAction, fallingCookieSound])]))
      }
  }
  // 6
  runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}

Here’s how this works:

  1. As with the other animation methods, you should only call the completion block after all the animations are finished. Because the number of falling cookies may vary, you can’t hardcode this total duration but instead have to compute it.
  2. The higher up the cookie is, the bigger the delay on the animation. That looks more dynamic than dropping all the cookies at the same time. This calculation works because fillHoles() guarantees that lower cookies are first in the array.
  3. Likewise, the duration of the animation is based on how far the cookie has to fall (0.1 seconds per tile). You can tweak these numbers to change the feel of the animation.
  4. You calculate which animation is the longest. This is the time the game has to wait before it may continue.
  5. You perform the animation, which consists of a delay, a movement and a sound effect.
  6. You wait until all the cookies have fallen down before allowing the gameplay to continue.

Now you can tie it all together. Open GameViewController.swift. Replace the existing handleMatches() function with the following:

func handleMatches() {
  let chains = level.removeMatches()
  scene.animateMatchedCookies(chains) {
    let columns = self.level.fillHoles()
    self.scene.animateFallingCookies(columns) {
      self.view.userInteractionEnabled = true
    }
  }
}

This now calls fillHoles() to update the model, which returns the array that describes the fallen cookies and then passes that array onto the scene so it can animate the sprites to their new positions.

Note: To access a property or call a method in Objective-C you always had to use self. In Swift you don’t have to do this, except inside closures. That’s why inside handleMatches() you see self a lot. Swift insists on this to make it clear that the closure actually captures the value of self with a strong reference. In fact, if you don’t specify self inside a closure, the Swift compiler will give an error message.

Try it out!

Falling animation

It’s raining cookies! Notice that the cookies even fall properly across gaps in the level design.

Adding New Cookies

There’s one more thing to do to complete the game loop. Falling cookies leave their own holes at the top of each column.

Holes at top

You need to top up these columns with new cookies. Add a new method to Level.swift:

func topUpCookies() -> [[Cookie]] {
  var columns = [[Cookie]]()
  var cookieType: CookieType = .Unknown
 
  for column in 0..<NumColumns {
    var array = [Cookie]()
 
    // 1
    var row = NumRows - 1
    while row >= 0 && cookies[column, row] == nil {
      // 2
      if tiles[column, row] != nil {
        // 3
        var newCookieType: CookieType
        repeat {
          newCookieType = CookieType.random()
        } while newCookieType == cookieType
        cookieType = newCookieType
        // 4
        let cookie = Cookie(column: column, row: row, cookieType: cookieType)
        cookies[column, row] = cookie
        array.append(cookie)
      }
 
      row -= 1
    }
    // 5
    if !array.isEmpty {
      columns.append(array)
    }
  }
  return columns
}

Where necessary, this adds new cookies to fill the columns to the top. It returns an array with the new Cookie objects for each column that had empty tiles.

If a column has X empty tiles, then it also needs X new cookies. The holes are all at the top of the column now, so you can simply scan from the top down until you find a cookie.

Here’s how it works, step by step:

  1. You loop through the column from top to bottom. This while loop ends when cookies[column, row] is not nil—that is, when it has found a cookie.
  2. You ignore gaps in the level, because you only need to fill up grid squares that have a tile.
  3. You randomly create a new cookie type. It can’t be equal to the type of the last new cookie, to prevent too many “freebie” matches.
  4. You create the new Cookie object and add it to the array for this column.
  5. As before, if a column does not have any holes, you don’t add it to the final array.

The array that topUpCookies() returns contains a sub-array for each column that had holes. The cookie objects in these arrays are ordered from top to bottom. This is important to know for the animation method coming next.

Switch to GameScene.swift and the new animation method:

func animateNewCookies(columns: [[Cookie]], completion: () -> ()) {
  // 1
  var longestDuration: NSTimeInterval = 0
 
  for array in columns {
    // 2
    let startRow = array[0].row + 1
 
    for (idx, cookie) in array.enumerate() {
      // 3
      let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
      sprite.size = CGSize(width: TileWidth, height: TileHeight)
      sprite.position = pointForColumn(cookie.column, row: startRow)
      cookiesLayer.addChild(sprite)
      cookie.sprite = sprite
      // 4
      let delay = 0.1 + 0.2 * NSTimeInterval(array.count - idx - 1)
      // 5
      let duration = NSTimeInterval(startRow - cookie.row) * 0.1
      longestDuration = max(longestDuration, duration + delay)
      // 6
      let newPosition = pointForColumn(cookie.column, row: cookie.row)
      let moveAction = SKAction.moveTo(newPosition, duration: duration)
      moveAction.timingMode = .EaseOut
      sprite.alpha = 0
      sprite.runAction(
        SKAction.sequence([
          SKAction.waitForDuration(delay),
          SKAction.group([
            SKAction.fadeInWithDuration(0.05),
            moveAction,
            addCookieSound])
          ]))
    }
  }
  // 7
  runAction(SKAction.waitForDuration(longestDuration), completion: completion)
}

This is very similar to the “falling cookies” animation. The main difference is that the cookie objects are now in reverse order in the array, from top to bottom. Step by step, this is what the method does:

  1. The game is not allowed to continue until all the animations are complete, so you calculate the duration of the longest animation to use later in step 7.
  2. The new cookie sprite should start out just above the first tile in this column. An easy way to find the row number of this tile is to look at the row of the first cookie in the array, which is always the top-most one for this column.
  3. You create a new sprite for the cookie.
  4. The higher the cookie, the longer you make the delay, so the cookies appear to fall after one another.
  5. You calculate the animation’s duration based on far the cookie has to fall.
  6. You animate the sprite falling down and fading in. This makes the cookies appear less abruptly out of thin air at the top of the grid.
  7. You wait until the animations are done before continuing the game.

Finally, in GameViewController.swift, replace the chain of completion blocks in handleMatches() with the following:

func handleMatches() {
  let chains = level.removeMatches()
  scene.animateMatchedCookies(chains) {
    let columns = self.level.fillHoles()
    self.scene.animateFallingCookies(columns) {
      let columns = self.level.topUpCookies()
      self.scene.animateNewCookies(columns) {
        self.view.userInteractionEnabled = true
      }
    }
  }
}

Try it out!

Adding new cookies

A Cascade of Cookies

You may have noticed a couple of oddities after playing for a while. When the cookies fall down to fill up the holes and new cookies drop from the top, these actions sometimes create new chains of three or more. But what happens then?

You also need to remove these matching chains and ensure other cookies take their place. This cycle should continue until there are no matches left on the board. Only then should the game give control back to the player.

Handling these possible cascades may sound like a tricky problem, but you’ve already written all the code to do it! You just have to call handleMatches() again and again and again until there are no more chains.

In GameViewController.swift, inside handleMatches(), change the line that sets userInteractionEnabled to:

self.handleMatches()

Yep, you’re seeing that right: handleMatches() calls itself. This is called recursion and it’s a powerful programming technique. There’s only one thing you need to watch out for with recursion: At some point, you need to stop it, or the app will go into an infinite loop and eventually crash.

For that reason, add the following to the top of handleMatches(), right after the line that calls removeMatches() on the level:

if chains.count == 0 {
  beginNextTurn()
  return
}

If there are no more matches, the player gets to move again and the function exits to prevent another recursive call.

Finally, add this new beginNextTurn() method:

func beginNextTurn() {
  view.userInteractionEnabled = true
}

Try it out. If removing a chain creates another chain elsewhere, the game should now remove that chain, as well:

Cascade

There’s another problem. After a while, the game no longer seems to recognize swaps that it should consider valid. There’s a good reason for that. Can you guess what it is?

Solution Inside: Solution SelectShow>

The logic for this sits in Level.swift, in detectPossibleSwaps(). You need to call this method from beginNextTurn() in GameViewController.swift:

func beginNextTurn() {
  level.detectPossibleSwaps()
  view.userInteractionEnabled = true
}

Excellent! Now your game loop is complete. It has an infinite supply of cookies!

Where to Go From Here?

Once again, here is the sample project with all of the code from the Swift tutorial up to this point.

By now you’re almost done and only have one part left of this exciting cookie crunching adventure.

In the final part, you’ll complete the gameplay by adding support for scoring points, winning and losing, shuffling the cookies, and more. We know you’ll enjoy finishing up your game.

We love hearing what you have to say about our tutorials. Please 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.

Some of the source code for this tutorial series was 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 3 appeared first on Ray Wenderlich.

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

$
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 the fourth and final part of the “How to Make a Game Like Candy Crush” tutorial with SpriteKit and Swift series!

  • 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.
  • In the second part you continued expanding on the foundation of the game. You focused on detecting swipes and swapping cookies, as well as creating some nice visual effects for your game.
  • In the third part, you’ll work on finding and removing chains and refilling the level with new yummy cookies after successful swipes.
  • (You’re here) 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.

Time for you to finish this yummy game of yours :]

Scoring Points

In Cookie Crunch Adventure, the player’s objective is to score a certain number of points within a maximum number of swaps. Both of these values come from the JSON level file. The game should show these numbers on the screen so that the player can keep track of them.

First, add the following properties to GameViewController.swift:

var movesLeft = 0
var score = 0
 
@IBOutlet weak var targetLabel: UILabel!
@IBOutlet weak var movesLabel: UILabel!
@IBOutlet weak var scoreLabel: UILabel!

The movesLeft and score variables keep track of how well the player is doing (model data), while the outlets show this on the screen (views).

Open Main.storyboard to add these labels to the view. Design the view controller to look like this:

Score labels

Make sure to set the outer Stack View‘s Distribution to be equal to Fill Equally in the Attributes Inspector. To permanently pin the outer Stack View to the top of the screen, and to have it fit all screen sizes, give it the following 3 layout constraints:

Autolayout constraints for score labels

To make the labels easier to see, give the main view a gray background color. Make the font for the labels Gill Sans Bold, size 20.0 for the number labels and 14.0 for the text labels. You may also wish to set a slight drop shadow for the labels so they are easier to see.

It looks best if you set center alignment on the number labels. Connect the three number labels to their respective outlets.

Because the target score and the maximum number of moves are stored in the JSON level file, you should load them into Level. Add the following properties to Level.swift:

var targetScore = 0
var maximumMoves = 0

In Level.swift, add these two lines to the bottom of init(filename:):

init(filename: String) {
  ...
  targetScore = dictionary["targetScore"] as! Int
  maximumMoves = dictionary["moves"] as! Int
}

By this point, you’ve parsed the JSON into a dictionary, so you grab the two values and store them.

Back in GameViewController.swift, add the following method:

func updateLabels() {
  targetLabel.text = String(format: "%ld", level.targetScore)
  movesLabel.text = String(format: "%ld", movesLeft)
  scoreLabel.text = String(format: "%ld", score)
}

You’ll call this method after every turn to update the text inside the labels.

Add the following lines to the top of beginGame(), before the call to shuffle():

movesLeft = level.maximumMoves
score = 0
updateLabels()

This resets everything to the starting values. Build and run, and your display should look like this:

Game with labels

Calculating the Score

The scoring rules are simple:

  • A 3-cookie chain is worth 60 points.
  • Each additional cookie in the chain increases the chain’s value by 60 points.

Thus, a 4-cookie chain is worth 120 points, a 5-cookie chain is worth 180 points and so on.

It’s easiest to store the score inside the Chain object, so each chain knows how many points it’s worth.

Add the following to Chain.swift:

var score = 0

The score is model data, so it needs to be calculated by Level. Add the following method to Level.swift:

private func calculateScores(chains: Set<Chain>) {
  // 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on
  for chain in chains {
    chain.score = 60 * (chain.length - 2)
  }
}

Now call this method from removeMatches(), just before the return statement:

calculateScores(horizontalChains)
calculateScores(verticalChains)

You need to call it twice because there are two sets of chain objects.

Now that the level object knows how to calculate the scores and stores them inside the Chain objects, you can update the player’s score and display it onscreen.

This happens in GameViewController.swift. Inside handleMatches(), just before the call to self.level.fillHoles(), add the following lines:

for chain in chains {
  self.score += chain.score
}
self.updateLabels()

This simply loops through the chains, adds their scores to the player’s total and then updates the labels.

Try it out. Swap a few cookies and observe your increasing score:

Score

Animating Point Values

It would be fun to show the point value of each chain with a cool little animation. In GameScene.swift, add a new method:

func animateScoreForChain(chain: Chain) {
  // Figure out what the midpoint of the chain is.
  let firstSprite = chain.firstCookie().sprite!
  let lastSprite = chain.lastCookie().sprite!
  let centerPosition = CGPoint(
    x: (firstSprite.position.x + lastSprite.position.x)/2,
    y: (firstSprite.position.y + lastSprite.position.y)/2 - 8)
 
  // Add a label for the score that slowly floats up.
  let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic")
  scoreLabel.fontSize = 16
  scoreLabel.text = String(format: "%ld", chain.score)
  scoreLabel.position = centerPosition
  scoreLabel.zPosition = 300
  cookiesLayer.addChild(scoreLabel)
 
  let moveAction = SKAction.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7)
  moveAction.timingMode = .EaseOut
  scoreLabel.runAction(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
}

This creates a new SKLabelNode with the score and places it in the center of the chain. The numbers will float up a few pixels before disappearing.

Call this new method from animateMatchedCookies(), in between the two for loops:

for chain in chains {
 
  // Add this line:
  animateScoreForChain(chain)
 
  for cookie in chain.cookies {

When using SKLabelNode, Sprite Kit needs to load the font and convert it to a texture. That only happens once, but it does create a small delay, so it’s smart to pre-load this font before the game starts in earnest.

At the bottom of GameScene‘s init(size:), add the following line:

let _ = SKLabelNode(fontNamed: "GillSans-BoldItalic")

Now try it out. Build and run, and score some points!

Floating score

Handle Combo Scenrios

What makes games like Candy Crush Saga fun is the ability to make combos, or more than one match in a row.

Of course, you should reward the player for making a combo by giving extra points. To that effect, you’ll add a combo “multiplier”, where the first chain is worth its normal score, but the second chain is worth twice its score, the third chain is worth three times its score, and so on.

In Level.swift, add the following private property:

private var comboMultiplier = 0

Update calculateScores() to:

private func calculateScores(chains: Set<Chain>) {
  // 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on
  for chain in chains {
    chain.score = 60 * (chain.length - 2) * comboMultiplier
    comboMultiplier += 1
  }
}

The method now multiplies the chain’s score by the combo multiplier and then increments the multiplier so it’s one higher for the next chain.

You also need a method to reset this multiplier on the next turn. Add the following method to Level.swift:

func resetComboMultiplier() {
  comboMultiplier = 1
}

Open GameViewController.swift and find beginGame(). Add this line just before the call to shuffle():

level.resetComboMultiplier()

Add the same line at the top of beginNextTurn().

And now you have combos. Try it out!

Combo

Challenge: How would you detect an L-shaped chain and make it count double the value for a row?

Solution Inside: Solution SelectShow>

Handle Winning and Losing Scenarios

The player only has so many moves to reach the target score. Fail, then it’s game over. The logic for this isn’t difficult to add.

Create a new method in GameViewController.swift:

func decrementMoves() {
  movesLeft -= 1
  updateLabels()
}

This simply decrements the counter keeping track of the number of moves and updates the onscreen labels.

Call it from the bottom of beginNextTurn():

decrementMoves()

Build and run to see it in action. After each swap, the game clears the matches and decreases the number of remaining moves by one.

Moves

Of course, you still need to detect when the player runs out of moves (game over!) or when the target score is reached (success and eternal fame!), and respond accordingly.

First, though, the storyboard needs some work.

The Look of Victory or Defeat

Open Main.storyboard and drag an UIImageView into the view. In the Attributes Inspector give the view a mode of Aspect Fit. Next, give it the following 3 layout constraints (make sure to uncheck “Constrain to margins”) and center it vertically in the view.

Autolayout for game over panel

This image view will show either the “Game Over!” or “Level Complete!” message.

Now connect this image view to a new outlet on GameViewController.swift named gameOverPanel.

@IBOutlet weak var gameOverPanel: UIImageView!

Also, add a property for a gesture recognizer:

var tapGestureRecognizer: UITapGestureRecognizer!

In viewDidLoad(), before you present the scene, make sure to hide this image view:

gameOverPanel.hidden = true

Now add a new method to show the game over panel:

func showGameOver() {
  gameOverPanel.hidden = false
  scene.userInteractionEnabled = false
 
  self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.hideGameOver))
  view.addGestureRecognizer(tapGestureRecognizer)
}

This un-hides the image view, disables touches on the scene to prevent the player from swiping and adds a tap gesture recognizer that will restart the game.

Add one more method:

func hideGameOver() {
  view.removeGestureRecognizer(tapGestureRecognizer)
  tapGestureRecognizer = nil
 
  gameOverPanel.hidden = true
  scene.userInteractionEnabled = true
 
  beginGame()
}

This hides the game over panel again and restarts the game.

The logic that detects whether it’s time to show the game over panel goes into decrementMoves(). Add the following lines to the bottom of that method:

if score >= level.targetScore {
  gameOverPanel.image = UIImage(named: "LevelComplete")
  showGameOver()
} else if movesLeft == 0 {
  gameOverPanel.image = UIImage(named: "GameOver")
  showGameOver()
}

If the current score is greater than or equal to the target score, the player has won the game! If the number of moves remaining is 0, the player has lost the game.

In either case, the method loads the proper image into the image view and calls showGameOver() to put it on the screen.

Try it out. When you beat the game, you should see this:

Level complete

Likewise, when you run out of moves, you should see a “Game Over” message.

Animating the Transitions

It looks a bit messy with this banner on top of all those cookies, so add some animation here. Add these two methods to GameScene.swift:

func animateGameOver(completion: () -> ()) {
  let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
  action.timingMode = .EaseIn
  gameLayer.runAction(action, completion: completion)
}
 
func animateBeginGame(completion: () -> ()) {
  gameLayer.hidden = false
  gameLayer.position = CGPoint(x: 0, y: size.height)
  let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3)
  action.timingMode = .EaseOut
  gameLayer.runAction(action, completion: completion)
}

animateGameOver() animates the entire gameLayer out of the way. animateBeginGame() does the opposite and slides the gameLayer back in from the top of the screen.

The very first time the game starts, you also want to call animateBeginGame() to perform this same animation. It looks better if the game layer is hidden before that animation begins, so add the following line to GameScene.swift in init(size:), immediately after you create the gameLayer node:

gameLayer.hidden = true

Now open GameViewController.swift and call animateGameOver() in showGameOver():

func showGameOver() {
  gameOverPanel.hidden = false
  scene.userInteractionEnabled = false
 
  scene.animateGameOver() {
    self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.hideGameOver))
    self.view.addGestureRecognizer(self.tapGestureRecognizer)
  }
}

Note that the tap gesture recognizer is now added after the animation is complete. This prevents the player from tapping while the game is still performing the animation.

Finally, in GameViewController.swift’s beginGame(), just before the call to shuffle(), call animateBeginGame():

scene.animateBeginGame() { }

The completion block for this animation is currently empty, but you’ll put something there soon.

Now when you tap after game over, the cookies should drop down the screen to their starting positions. Sweet!

Too many cookies

Whoops! Something’s not right. It appears you didn’t properly remove the old cookie sprites.

Add this new method to GameScene.swift to perform the cleanup:

func removeAllCookieSprites() {
  cookiesLayer.removeAllChildren()
}

And call it as the very first thing from shuffle() inside GameViewController.swift:

scene.removeAllCookieSprites()

That solves that! Build and run and your game should reset cleanly.

Manual Shuffling

There’s one more situation to manage: It may happen—though only rarely—that there is no way to swap any of the cookies to make a chain. In that case, the player is stuck.

There are different ways to handle this. For example, Candy Crush Saga automatically reshuffles the cookies. But in Cookie Crunch, you’ll give that power to the player. You will allow the player to shuffle at any time by tapping a button, but it will cost the player a move.

shufflecomic

Add an outlet property in GameViewController.swift:

@IBOutlet weak var shuffleButton: UIButton!

And add an action method:

@IBAction func shuffleButtonPressed(AnyObject) {
  shuffle()
  decrementMoves()
}

Tapping the shuffle button costs a move, so this also calls decrementMoves().

In showGameOver(), add the following line to hide the shuffle button:

shuffleButton.hidden = true

Also do the same thing in viewDidLoad(), so the button is hidden when the game first starts.

In beginGame(), in the animation’s completion block, put the button back on the screen again:

scene.animateBeginGame() {
  self.shuffleButton.hidden = false
}

Now open Main.storyboard and add a button to the bottom of the screen.

Set the title to “Shuffle” and make the button 100×36 points big. To style the button, give it the font Gill Sans Bold, 20 pt. Make the text color white with a 50% opaque black drop shadow. For the background image, choose “Button”, an image you added to the asset catalog in Part One.

To pin the button to the bottom of the screen, center it horizontally, and add the following 3 layout constraints:

Autolayout for shuffle button

Finally, connect the shuffleButton outlet to the button and its Touch Up Inside event to the shuffleButtonPressed: action.

Try it out!

Shuffle button in the game

Note: When shuffling a deck of cards, you take the existing cards, change their order and deal out the same cards again in a different order. In this game, however, you simply get all new—random!—cookies. Finding a distribution of the same set of cookies that allows for at least one swap is an extremely difficult computational problem, and after all, this is only a casual game.

The shuffle is a bit abrupt, rather make the new cookies appear with a cute animation. In GameScene.swift, go to addSpritesForCookies() and add the following lines inside the for loop, after the existing code:

// Give each cookie sprite a small, random delay. Then fade them in.
sprite.alpha = 0
sprite.xScale = 0.5
sprite.yScale = 0.5
 
sprite.runAction(
  SKAction.sequence([
    SKAction.waitForDuration(0.25, withRange: 0.5),
    SKAction.group([
      SKAction.fadeInWithDuration(0.25),
      SKAction.scaleTo(1.0, duration: 0.25)
      ])
    ]))

This gives each cookie sprite a small, random delay and then fades them into view. It looks like this:

Shuffle animation

Adding Music

Give the player some smooth, relaxing music to listen to while crunching cookies. Add this line to the top of GameViewController.swift to include the AVFoundation framework:

import AVFoundation

Also add the following property:

lazy var backgroundMusic: AVAudioPlayer? = {
  guard let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3") else {
    return nil
  }
  do {
    let player = try AVAudioPlayer(contentsOfURL: url)
    player.numberOfLoops = -1
    return player
  } catch {
    return nil
  }
}()

What you see here is a common pattern for declaring a variable and initializing it in the same statement. The initialization code sits in a closure. It loads the background music MP3 and sets it to loop forever. Because the variable is marked lazy, the code from the closure won’t run until backgroundMusic is first accessed.

Finally, add this line to viewDidLoad(), just before the call to beginGame():

backgroundMusic?.play()

It gives the game a whole lot more swing!

Drawing Better Tiles

If you compare your game closely to Candy Crush Saga, you’ll notice that the tiles are drawn slightly differently. The borders in Candy Crush look much nicer:

Border comparison

Also, if a cookie drops across a gap, your game draws it on top of the background, but candies in Candy Crush appear to fall behind the background:

Masked sprite comparison

Recreating this effect isn’t too difficult but it requires a number of new sprites. You can find these in the tutorial’s Resources in the Grid.atlas folder. Drag this folder into your Xcode project. This creates a second texture atlas with just these images.

In GameScene.swift, add two new properties:

let cropLayer = SKCropNode()
let maskLayer = SKNode()

In init(size:), add these lines below the code that creates the tilesLayer:

gameLayer.addChild(cropLayer)
 
maskLayer.position = layerPosition
cropLayer.maskNode = maskLayer

This makes two new layers: cropLayer, which is a special kind of node called an SKCropNode, and a mask layer. A crop node only draws its children where the mask contains pixels. This lets you draw the cookies only where there is a tile, but never on the background.

Replace this line:

gameLayer.addChild(cookiesLayer)

With this:

cropLayer.addChild(cookiesLayer)

Now, instead of adding the cookiesLayer directly to the gameLayer, you add it to this new cropLayer.

To fill in the mask of this crop layer, make two changes to addTiles():

  • Replace "Tile" with "MaskTile"
  • Replace tilesLayer with maskLayer

Wherever there’s a tile, the method now draws the special MaskTile sprite into the layer functioning as the SKCropNode’s mask. The MaskTile is slightly larger than the regular tile.

Build and run. Notice how the cookies get cropped when they fall through a gap:

Cookie is cropped

Tip: If you want to see what the mask layer looks like, add this line to init(size:)
cropLayer.addChild(maskLayer)

Don’t forget to remove it again when you’re done!

For the final step, add the following code to the bottom of addTiles():

for row in 0...NumRows {
  for column in 0...NumColumns {
    let topLeft     = (column > 0) && (row < NumRows)
                                   && level.tileAtColumn(column - 1, row: row) != nil
    let bottomLeft  = (column > 0) && (row > 0)
                                   && level.tileAtColumn(column - 1, row: row - 1) != nil
    let topRight    = (column < NumColumns) && (row < NumRows)
                                            && level.tileAtColumn(column, row: row) != nil
    let bottomRight = (column < NumColumns) && (row > 0)
                                            && level.tileAtColumn(column, row: row - 1) != nil
 
    // The tiles are named from 0 to 15, according to the bitmask that is
    // made by combining these four values.
    let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3
 
    // Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn.
    if value != 0 && value != 6 && value != 9 {
      let name = String(format: "Tile_%ld", value)
      let tileNode = SKSpriteNode(imageNamed: name)
      tileNode.size = CGSize(width: TileWidth, height: TileHeight)
      var point = pointForColumn(column, row: row)
      point.x -= TileWidth/2
      point.y -= TileHeight/2
      tileNode.position = point
      tilesLayer.addChild(tileNode)
    }
  }
}

This draws a pattern of border pieces in between the level tiles. As a challenge, try to decipher for yourself how this method works. :]

Solution Inside: Solution SelectShow>

Build and run, and you should now have a game that looks and acts just like Candy Crush Saga!

Final game

Going to the Next Level

You’re almost done, but wouldn’t it be cool if your game automatically switched to the next level upon completing the current one? Luckily, this is surprisingly easy to do.

First, in Level.swift add the following global constant for keeping track of the number of levels right below NumRows:

let NumLevels = 4 // Excluding level 0

Next, in GameViewController.swift add the following property for keeping track of the level the user is currently playing:

var currentLevelNum = 1

Now you need a way to know what level to use when loading your game scene. Still in GameViewController.swift replace the current viewDidLoad() method with the following:

override func viewDidLoad() {
  super.viewDidLoad()
 
  // Setup view with level 1
  setupLevel(currentLevelNum)
 
  // Start the background music.
  backgroundMusic?.play()
}

And implement the setupLevel(_:) function as follows:

func setupLevel(levelNum: Int) {
  let skView = view as! SKView
  skView.multipleTouchEnabled = false
 
  // Create and configure the scene.
  scene = GameScene(size: skView.bounds.size)
  scene.scaleMode = .AspectFill
 
  // Setup the level.
  level = Level(filename: "Level_\(levelNum)")
  scene.level = level
 
  scene.addTiles()
  scene.swipeHandler = handleSwipe
 
  gameOverPanel.hidden = true
  shuffleButton.hidden = true
 
  // Present the scene.
  skView.presentScene(scene)
 
  // Start the game.
  beginGame()
}

As you can see, this is almost the exact same code as you had in viewDidLoad() before, except for the line that setup the actual level instance. Now you choose the level number dynamically :]

Next, in decrementMoves() after the line:

gameOverPanel.image = UIImage(named: "LevelComplete")

add the following to update the current level number.

currentLevelNum = currentLevelNum < NumLevels ? currentLevelNum+1 : 1

Notice that this is only called if the player actually completes the level. Rather than congratulating the player when all levels are complete, you simply go back to level 1. This way the game goes on forever!

Now there’s only one last change you need to make before having implemented this awesome level-changing feature to your game. In hideGameOver() replace the line beginGame() with:

setupLevel(currentLevelNum)

That’s it! Build and run, and your game should now automatically go to the next level when a user completes the current one.

Where to Go From Here?

Congrats for making it to the end! This has been a long but “Swift” tutorial, and you are coming away with all the basic building blocks for making your own match-3 games.

You can download the final Xcode project here.

Here are ideas for other features you could add:

  • Special cookies when the player matches a certain shape. For example, Candy Crush Saga gives you a cookie that can clear an entire row when you match a 4-in-a-row chain.
  • Detection of special chains, such as L- or T-shapes, that reward the player with bonus points or special power-ups.
  • Boosts, or power-ups the player can use any time they want. For example, one boost might remove all the cookies of one type from the screen at once.
  • Jelly levels: On these levels, some tiles are covered in jelly. You have X moves to remove all the jelly. This is where the Tile class comes in handy. You can give it a Bool jelly property and if the player matches a cookie on this tile, set the jelly property to false to remove the jelly.
  • Hints: If the player doesn’t make a move for two seconds, light up a pair of cookies that make a valid swap.
  • Shuffle the cookies automatically if there are no possible moves.

As you can see, there’s still plenty to play with. Have fun! :]

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

Some of the techniques used in this source code are based on a blog post by Emanuele Feronato.

Note: If you want to learn more about Sprite Kit, you should check out our book 2D iOS & tvOS Games by Tutorials.

In this book we’ll teach you everything you need to know to make great games for iOS & tvOS – from physics, to tile maps, to particle systems, and even how to make your games “juicy” with polish and special effects.

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


Video Tutorial: Beginning SpriteKit Part 5: Actions

Video Tutorial: Beginning SpriteKit Part 6: Animations

Video Tutorial: Beginning SpriteKit Part 7: Collision Detection

Video Tutorial: Beginning SpriteKit Part 8: Scenes

Implementing Custom Subscripts in Swift

$
0
0
CAPT

Power up your own types with native-looking subscripts!

Update note: This tutorial was updated for Swift 2.2 and Xcode 7.3 by Mikael Konutgan. Original post by Tutorial Team member Evan Dekhayser.

Subscripts are a powerful language feature that, when used properly, can significantly enhance the convenience factor and readability of your code.

Like operator overloading, subscripts let you use native Swift constructs: something like checkerBoard[2][3] rather than the more verbose checkerBoard.objectAt(x: 2, y: 3).

In this tutorial, you’re going to explore subscripts by building the foundations for a basic checkers game in a playground. You’ll see how easy it is to use subscripting to move pieces around the board. When you’re done, you’ll be well on your way to building a new game to keep your fingers occupied during all of your spare time.

Oh, and you’ll know a lot more about subscripts too! :]


Note: This tutorial assumes you already know the basics of Swift development. If you are new to Swift, check out some of our beginner Swift tutorials or read the Swift Apprentice first.

Getting Started

Create a new playground and add the following code:

struct Checkerboard {
 
  enum Square: String {
    case Empty = "\u{25AA}\u{fe0f}" // Black square
    case Red = "\u{1f534}"          // Red piece
    case White = "\u{26AA}\u{fe0f}" // White piece
  }
 
  typealias Coordinate = (x: Int, y: Int)
 
  private var squares: [[Square]] = [
    [ .Empty, .Red,   .Empty, .Red,   .Empty, .Red,   .Empty, .Red   ],
    [ .Red,   .Empty, .Red,   .Empty, .Red,   .Empty, .Red,   .Empty ],
    [ .Empty, .Red,   .Empty, .Red,   .Empty, .Red,   .Empty, .Red   ],
    [ .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty ],
    [ .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty, .Empty ],
    [ .White, .Empty, .White, .Empty, .White, .Empty, .White, .Empty ],
    [ .Empty, .White, .Empty, .White, .Empty, .White, .Empty, .White ],
    [ .White, .Empty, .White, .Empty, .White, .Empty, .White, .Empty ]
  ]
}
 
extension Checkerboard: CustomStringConvertible {
  var description: String {
    return squares.map { row in row.map { $0.rawValue }.joinWithSeparator("") }
                  .joinWithSeparator("\n") + "\n"
  }
}

Checkerboard contains three definitions:

  • Square represents the state of a square on the board. .Empty represents an empty square while .Red and .White represent the presence of a red or white piece on that square.
  • Coordinate is an alias for a tuple of two integers. You will use this type to access the squares on the board.
  • squares is the two-dimensional array that stores the state of the board.

Finally, there’s an extension to add conformance to CustomStringConvertible that lets you print a checkerboard to the console.

Open the console using View/Debug Area/Show Debug Area, then enter the following lines at the bottom of the playground:

var checkerboard = Checkerboard()
print(checkerboard)

This code initializes an instance of Checkerboard then prints the description property of the CustomStringConvertible implementation to the console. The output in your console should look like this:

▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️🔴▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️

Getting And Setting Pieces

Looking at the console, it’s pretty easy for you to know what piece occupies a given square, but your program doesn’t have those powers yet. It can’t know which player is at a specified coordinate because the squares array is marked as private. There’s an important point to make here: the squares array is the implementation of the the board. However, the user of type Checkerboard shouldn’t know anything about the implementation of that type.

A type should shield its users from its internal implementation details; that’s why the squares array is kept private.

With that in mind, you’re going to add two methods to Checkerboard to find and set a piece at a given coordinate.

Add the following methods to Checkerboard, after the spot you assign the squares array:

func pieceAt(coordinate: Coordinate) -> Square {
  return squares[coordinate.y][coordinate.x]
}
 
mutating func setPieceAt(coordinate: Coordinate, to newValue: Square) {
  squares[coordinate.y][coordinate.x] = newValue
}

Notice how the squares array is accessed – using a Coordinate tuple – rather than accessing the array directly. The actual storage mechanism of an array-of-arrays is exactly the kind of implementation detail the user should be shielded from!

Defining Subscripts

You may have noticed these methods look an awful lot like a property getter and setter combination. Maybe they should be implemented as a computed property instead? Unfortunately, that won’t work. Your methods require a coordinate parameter, and computed properties can’t have parameters. Does that mean you’re stuck with methods?

Well no – this special case is exactly what subscripts are for! :]

Look at how you define a subscript:

subscript(parameterList) -> ReturnType {
  get {
    // return someValue of ReturnType
  }
 
  set (newValue) {
    // set someValue of ReturnType to newValue
  }
}

Subscript definitions mix both function and computed property definition syntax:

  • The first part looks a lot like a function definition, with a parameter list and a return type. Instead of the func keyword and function name, you use the special subscript keyword.
  • The main body looks a lot like a computed property, with a getter and a setter.

This combination of function and property syntax highlights the power of subscripts: to provide a shortcut to accessing the elements of an indexed collection. You’ll learn more about that soon, but first, consider the following example.

Replace methods pieceAt(_:) and setPieceAt(_:to:) with the following subscript:

subscript(coordinate: Coordinate) -> Square {
  get {
    return squares[coordinate.y][coordinate.x]
  }
  set {
    squares[coordinate.y][coordinate.x] = newValue
  }
}

The getter and setter of this subscript are implemented exactly like the methods they replace:

  • Given a Coordinate, the getter returns the square at the column and row.
  • Given a Coordinate and value, the setter accesses the square at the column and row and replaces its value.

Give your new subscript a test drive by adding the following code to the end of the playground:

let coordinate = (x: 3, y: 2)
print(checkerboard[coordinate])
checkerboard[coordinate] = .White
print(checkerboard)

The playground will tell you the piece at (3, 2) is red. After changing it to white, the output in the console will be:

▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️

You can now find out which piece is at a given coordinate, and set it, by using checkerboard[coordinate] in both cases. A shortcut indeed!

Comparing Subscripts, Properties And Functions

Subscripts are similar to computed properties in many regards:

  • They consist of a getter and setter.
  • The setter is optional, meaning a subscript can be either read-write or read-only.
  • A read-only subscript doesn’t explicitly state get or set; the entire body is a getter.
  • In the setter, there’s a default parameter newValue with a type that equals the subscript’s return type. You typically only declare this parameter when you want to change its name to something other than newValue.
  • Users expect subscripts to be fast, preferably O(1), so keep them short and sweet!

disguise

The major difference with computed properties is that subscripts don’t have a property name per se. Like operator overloading, subscripts let you override the language-level square brackets [] usually used for accessing elements of a collection.

Subscripts are similar to functions in that they have a parameter list and return type, but they differ on the following points:

  • Subscript parameters don’t have external names by default. If you want to use them, you’ll need to explicitly add them.
  • Subscripts cannot use inout or default parameters. However, variadic (...) parameters are allowed.
  • Subscripts cannot throw errors. This means a subscript getter must report errors through its return value and a subscript setter cannot throw or return any errors at all.

Adding a Second Subscript

There is one other point where subscripts are similar to functions: they can be overloaded. This means a type can have multiple subscripts, as long as they have different parameter lists or return types.

Add the following code after the existing subscript definition in Checkerboard:

subscript(x: Int, y: Int) -> Square {
  get {
    return self[(x: x, y: y)]
  }
  set {
    self[(x: x, y: y)] = newValue
  }
}

This code adds a second subscript to Checkerboard that two integers rather than a Coordinate tuple. Notice how the second subscript is implemented using the first through self[(x: x, y: y)].

Try out this new subscript by adding the following lines to the end of the playground:

print(checkerboard[1, 2])
checkerboard[1, 2] = .White
print(checkerboard)

You should see the piece at (1, 2) change from red to white.

Where to Go From Here?

You can download the completed playground for this tutorial here.

Why not extend this game a bit further with some gameplay logic and turn this into a playable checkers game? Here’s a nice checkerboard collection view cell class to get you started.

Now that you’ve added subscripts to your toolkit, look for opportunities to use them in your own code. When used properly, they can make your code more readable and intuitive. That being said, you don’t always want to revert to subscripts. If you’re writing an API, your users are used to using subscripts to access elements of an indexed collection. Using them for other things will likely feel unnatural and forced.

For more details, check out the subscripts chapter of The Swift Programming Language by Apple for further information on subscripts.

If you have any questions or comments, please leave them below!

The post Implementing Custom Subscripts in Swift appeared first on Ray Wenderlich.

Viewing all 4370 articles
Browse latest View live


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