In this video we add the ability to delete cells from the collection view.
The post Video Tutorial: Collection Views Part 6: Deleting Cells appeared first on Ray Wenderlich.
In this video we add the ability to delete cells from the collection view.
The post Video Tutorial: Collection Views Part 6: Deleting Cells appeared first on Ray Wenderlich.
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:
Keep reading to see the the latest apps released by raywenderlich.com readers like you.
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 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.
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 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.
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!
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 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.
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!
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
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.
In this video we look at moving cells using drag and drop. We will also add table view style headers that scroll with the cells.
The post Video Tutorial: Collection Views Part 7: Moving Cells appeared first on Ray Wenderlich.
In this video we move on to creating our own custom layout breaking free of UICollectionViewFlowLayout.
The post Video Tutorial: Collection Views Part 8: Custom Layout appeared first on Ray Wenderlich.
The post Video Tutorial: Beginning SpriteKit: Introduction appeared first on Ray Wenderlich.
Your challenge is to add the zombie to the scene. See the Challenge PDF in the resources link below for full details.
View previous video: Introduction
The post Video Tutorial: Beginning SpriteKit Part 1: Sprites appeared first on Ray Wenderlich.
Learn about the SpriteKit game loop.
The post Video Tutorial: Beginning SpriteKit Part 2: Game Loop appeared first on Ray Wenderlich.
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.
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!
Here are the top 5 reasons to join our Android, OS X, or Unity teams:
Here are the requirements:
To apply, send me an email. Be sure to include the following information:
If your application looks promising, we’ll send you a tryout to gauge your editing skills.
If you pass the tryout, you’re in!
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.
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.
Download the starter project here. Build and run, and you’ll see the app’s main view controller, ComputersController
.
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.
A basic website with similar information has also been created. Check out the starter website here.
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:
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.
You first need to configure the app to handle universal links. Open the UniversalLinks.xcodeproj from the Starter folder and do the following:
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:
The first step is complete! The app is now aware of the website’s URL. Next, you’ll configure 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!
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.
You can find the app’s bundle ID via Xcode’s Targets\UniversalLinks\General tab:
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.
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:
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.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
.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.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.
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.
Learn how to make your game work on different devices so you'll have one universal app supported on different screen sizes.
The post Video Tutorial: Beginning SpriteKit Part 3: Univesal App Support appeared first on Ray Wenderlich.
Learn how to set up the playable area for your game, Zombie Conga.
The post Video Tutorial: Beginning SpriteKit Part 4: Boundaries and the Playable Area appeared first on Ray Wenderlich.
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!
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.
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:
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:
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:
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:
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!
This game’s playing field will consist of a grid, 9 columns by 9 rows. Each square of this grid can contain a cookie.
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:
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.
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.
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 } } |
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!
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:
random()
function you added to the CookieType
enum earlier.Cookie
object and adds it to the 2D array.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.
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.
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:
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.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.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 SKNode
s 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:
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:
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.
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:
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:
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.tilesArray
is therefore array-of-array-of-Int, or [[Int]]
.enumerate()
function, which is useful because it also returns the current row number.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:
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:
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).
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.
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!
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!
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:
cookiesLayer
.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:
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.touchesBegan()
does to calculate the row and column numbers currently under the player’s finger.else if
statements, only one of horzDelta
or vertDelta
will be set).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.
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.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:
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.
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!
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.
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! :]
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.
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.
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.
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:
Swap
object to the list of possibleSwaps
.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, . . . |
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 Swap
s 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.
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:
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! :]
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.
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 :]
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! :]
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:
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.
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.
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:
Chain
objects). Later, you’ll remove the cookies in these chains from the playing field.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.Chain
object. You increment column
for each match.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:
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.
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.
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.
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:
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.
Here is how it all works, step by step:
tiles
array describes the shape of the level.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:
fillHoles()
guarantees that lower cookies are first in the array.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!
It’s raining cookies! Notice that the cookies even fall properly across gaps in the level design.
There’s one more thing to do to complete the game loop. Falling cookies leave their own holes at the top of each column.
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:
while
loop ends when cookies[column, row]
is not nil
—that is, when it has found a cookie.Cookie
object and add it to the array for this column.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:
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!
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:
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> |
---|---|
After the player makes a move, the list of possible swaps is out of date. You need to recalculate this list before letting the player move again.
|
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!
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.
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!
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 :]
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:
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:
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:
The scoring rules are simple:
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:
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!
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!
Challenge: How would you detect an L-shaped chain and make it count double the value for a row?
Solution Inside: Solution | SelectShow> |
---|---|
An L-shape consists of two chains, one horizontal and one vertical, that share a corner cookie. You can loop through the set of horizontal chains and check if the chain’s first or last cookie is also present in any of the vertical chains. If so, remove those two chains and combine them into a new one, with a new ChainType .
|
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.
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.
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.
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:
Likewise, when you run out of moves, you should see a “Game Over” message.
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!
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.
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.
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:
Finally, connect the shuffleButton
outlet to the button and its Touch Up Inside event to the shuffleButtonPressed:
action.
Try it out!
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:
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!
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:
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:
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()
:
"Tile"
with "MaskTile"
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:
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> |
---|---|
Imagine dividing each tile into four quadrants. The four boolean variables indicate what kind of borders the tile has. For example, in a square level, the tile in the lower-right corner would need a background to cover the top-left only (see Tile_1.png). A tile with all neighboring tiles would get a full background (see Tile_15.png).
|
Build and run, and you should now have a game that looks and acts just like Candy Crush Saga!
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.
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:
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.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.
Learn about SpriteKit actions which allow you to do things like rotate, scale or change a sprite's position over time.
The post Video Tutorial: Beginning SpriteKit Part 5: Actions appeared first on Ray Wenderlich.
Learn more about SpriteKit actions and how they can be used to animate sprites.
The post Video Tutorial: Beginning SpriteKit Part 6: Animations appeared first on Ray Wenderlich.
Learn about collision detections in SpriteKit.
The post Video Tutorial: Beginning SpriteKit Part 7: Collision Detection appeared first on Ray Wenderlich.
Learn how to work with scenes and scene transitions in SpriteKit.
The post Video Tutorial: Beginning SpriteKit Part 8: Scenes appeared first on Ray Wenderlich.
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! :]
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:
▪️🔴▪️🔴▪️🔴▪️🔴 🔴▪️🔴▪️🔴▪️🔴▪️ ▪️🔴▪️🔴▪️🔴▪️🔴 ▪️▪️▪️▪️▪️▪️▪️▪️ ▪️▪️▪️▪️▪️▪️▪️▪️ ⚪️▪️⚪️▪️⚪️▪️⚪️▪️ ▪️⚪️▪️⚪️▪️⚪️▪️⚪️ ⚪️▪️⚪️▪️⚪️▪️⚪️▪️
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!
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:
func
keyword and function name, you use the special subscript
keyword.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:
Coordinate
, the getter returns the square at the column and row.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!
Subscripts are similar to computed properties in many regards:
get
or set
; the entire body is a getter.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
.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:
inout
or default parameters. However, variadic (...
) parameters are allowed.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.
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.