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

How to Make a Game Like Candy Crush: Part 1

$
0
0
Learn to make a tasty match-3 game

Learn to make a tasty match-3 game

For months, Candy Crush Saga has been one of the most-played games in the world. With over 500 million players and $5 million per day in revenue, it’s one of the megahits of the App Store. Not bad for a game that’s free to play!

The rules of this casual match-3 game—like its predecessors Bejeweled and Puzzle Quest—are extremely simple: you score points by swapping two candies (or jewels) to make chains of three or more of the same candy.

You keep doing this until you reach the target score for the level or run out of moves. And unlike eating that much real candy, it doesn’t give you a tummy ache!

In this tutorial, you’ll learn how to make a game like Candy Crush named Cookie Crunch Adventure. Yum, that sounds even better than candy!

This is Part One of a two-part series. In this first part, you’ll put the foundation in place: the gameplay view, the sprites and some of the logic for detecting swipes and swapping cookies.

In the second part, you’ll complete the gameplay and add the final polish to transform Cookie Crunch Adventure into a game of top-10 quality.

Note: This tutorial assumes you have working knowledge of Sprite Kit. If you’re new to Sprite Kit, check out the beginner tutorials on the site or our book, iOS Games by Tutorials.

Getting Started

Before you continue, download the resources for this 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\SpriteKit Game template and click Next. Name the project CookieCrunch and enter RWT for the Class Prefix.

RWT-prefix

Note: You’ll be using RWT for the class prefix for all classes you create in this tutorial. Because Objective-C does not have namespaces, it’s a good idea to add a unique prefix to your own classes to prevent potential conflicts with Apple’s own frameworks. You can read more about this in our Objective-C style guide.

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, uncheck the Landscape Left and Landscape Right options in the Device Orientation section:

Device orientation

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

You should now have a blue folder in your project:

Sprites-atlas-in-project-navigator.pngXcode 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 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:

Images in asset catalog

Outside of the asset catalog in the Project Navigator, delete Spaceship.png from the project. This is a sample image that came with the template but you won’t need any spaceships while crunching those tasty cookies! :]

Great! It’s time to write some code. In RWTViewController.m, add the following method to permanently hide the status bar:

- (BOOL)prefersStatusBarHidden {
  return YES;
}

For the final piece of setup, replace the contents of RWTMyScene.m with this:

#import "RWTMyScene.h"
 
@implementation RWTMyScene
 
- (id)initWithSize:(CGSize)size {
  if ((self = [super initWithSize:size])) {
 
    self.anchorPoint = CGPointMake(0.5, 0.5);
 
    SKSpriteNode *background = [SKSpriteNode spriteNodeWithImageNamed:@"Background"];
    [self addChild:background];
  }
  return self;
}
 
@end

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 both 3.5-inch and 4-inch devices.

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

Background image

Can I Have Some Cookies?

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

2D grid

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

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

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

To being implementing this, you need to create the class representing a cookie object. Go to File\New\File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next. Name the class RWTCookie, make it a subclass of NSObject, click Next and then Create.

Replace the contents of RWTCookie.h with the following:

@import SpriteKit;
 
static const NSUInteger NumCookieTypes = 6;
 
@interface RWTCookie : NSObject
 
@property (assign, nonatomic) NSInteger column;
@property (assign, nonatomic) NSInteger row;
@property (assign, nonatomic) NSUInteger cookieType;
@property (strong, nonatomic) SKSpriteNode *sprite;
 
- (NSString *)spriteName;
- (NSString *)highlightedSpriteName;
 
@end

The constant NumCookieTypes keeps track of the number of different cookie types in the game. You set the constant here in the header file because other classes will also need to know how many cookie types there are.

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

The cookieType property describes the—wait for it—type of the cookie, which is just a number from 1 to 6. You will deliberately not use cookie type 0. This value has a special meaning, as you’ll learn toward the end of this part of the tutorial.

Each cookie type number corresponds to a sprite image:

Cookie types

You may wonder why you’re not making RWTCookie 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 RWTCookie 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.

The spriteName helper method returns the file name of that 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.

Your implementation of RWTCookie will be very straightforward. Paste this into the @implementation block of RWTCookie.m:

- (NSString *)spriteName {
  static NSString * const spriteNames[] = {
    @"Croissant",
    @"Cupcake",
    @"Danish",
    @"Donut",
    @"Macaroon",
    @"SugarCookie",
  };
 
  return spriteNames[self.cookieType - 1];
}
 
- (NSString *)highlightedSpriteName {
  static NSString * const highlightedSpriteNames[] = {
    @"Croissant-Highlighted",
    @"Cupcake-Highlighted",
    @"Danish-Highlighted",
    @"Donut-Highlighted",
    @"Macaroon-Highlighted",
    @"SugarCookie-Highlighted",
  };
 
  return highlightedSpriteNames[self.cookieType - 1];
}
 
- (NSString *)description {
  return [NSString stringWithFormat:@"type:%ld square:(%ld,%ld)", (long)self.cookieType,
    (long)self.column, (long)self.row];
}

The spriteName and highlightedSpriteName methods simply look up the name for the cookie sprite in an array of strings. Recall that cookieType starts at 1 but arrays are indexed starting at 0, so you need to subtract 1 from cookieType to find the correct array index.

The description method is a debugging tool, so passing an RWTCookie object to NSLog() will print out something helpful: the type of cookie and its column and row in the level grid. You’ll use this in practice later.

print_cookies

Keeping the Cookies: the 2-D Grid

Now you need something to hold that 9×9 grid of cookies. For that, you’ll make another class, RWTLevel.

Go to File\New\File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next. Name the class RWTLevel, make it a subclass of NSObject, click Next and then Create.

Replace the contents of RWTLevel.h with the following:

#import "RWTCookie.h"
 
static const NSInteger NumColumns = 9;
static const NSInteger NumRows = 9;
 
@interface RWTLevel : NSObject
 
- (NSSet *)shuffle;
 
- (RWTCookie *)cookieAtColumn:(NSInteger)column row:(NSInteger)row;
 
@end

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 shuffle method fills up the level with random cookies. There is also a method to obtain a cookie object at a specific position in the level grid.

Of course, the interesting stuff happens in RWTLevel.m, so switch to that file and add the following instance variable:

@implementation RWTLevel {
  RWTCookie *_cookies[NumColumns][NumRows];
}

This creates a two-dimensional array that holds pointers to RWTCookie objects, 81 in total (9 rows of 9 columns).

You may be wondering why you are making a C array instead of an NSArray. There are two reasons.

  1. A multi-dimensional C array is a perfect representation of a 2D grid. If you know the column and row numbers of a specific item, you can index the array as follows:
    // find the element at column 3, row 6
    myCookie = _cookies[3][6];
  2. The second advantage over an NSArray is that a C array can contain nil elements, which is something you’ll need to make non-square levels.

With this in mind, implementing cookieAtColumn:row: becomes easy. Add it to RWTLevel.m:

- (RWTCookie *)cookieAtColumn:(NSInteger)column row:(NSInteger)row {
  NSAssert1(column >= 0 && column < NumColumns, @"Invalid column: %ld", (long)column);
  NSAssert1(row >= 0 && row < NumRows, @"Invalid row: %ld", (long)row);
 
  return _cookies[column][row];
}

Notice the use of NSAssert1() to verify that the specified column and row numbers are within the valid range of 0-8. This is important when using C arrays because, unlike NSArrays, they don’t check that the index you specify is within bounds. Array indexing bugs can make a big mess of things and they are hard to find, so always protect C array access with an NSAssert!

Note: New to NSAssert? The idea behind NSAssert 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. NSAssert 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 three methods to RWTLevel.m:

- (NSSet *)shuffle {
  return [self createInitialCookies];
}
 
- (NSSet *)createInitialCookies {
  NSMutableSet *set = [NSMutableSet set];
 
  // 1
  for (NSInteger row = 0; row < NumRows; row++) {
    for (NSInteger column = 0; column < NumColumns; column++) {
 
      // 2
      NSUInteger cookieType = arc4random_uniform(NumCookieTypes) + 1;
 
      // 3
      RWTCookie *cookie = [self createCookieAtColumn:column row:row withType:cookieType];
 
      // 4
      [set addObject:cookie];
    }
  }
  return set;
}
 
- (RWTCookie *)createCookieAtColumn:(NSInteger)column row:(NSInteger)row withType:(NSUInteger)cookieType {
  RWTCookie *cookie = [[RWTCookie alloc] init];
  cookie.cookieType = cookieType;
  cookie.column = column;
  cookie.row = row;
  _cookies[column][row] = cookie;
  return cookie;
}

The real work happens in createInitialCookies. Here’s what it does, step by step:

  1. The method loops through the rows and columns of the 2-D array. This is something you’ll see a lot in this tutorial. Remember that column 0, row 0 is in the bottom-left corner of the 2-D grid.
  2. Then the method picks a random cookie type. Recall that this needs to be a number between 1 and 6. arc4random_uniform(NumCookieTypes) returns a number between 0 and 5, so you have to add 1.
  3. Next, the method creates a new RWTCookie object and adds it to the 2-D array. This step employs a helper method, createCookieAtColumn:row:withType:. The reason for having this separate method is that later you also need to create RWTCookie objects from another place.
  4. Finally, the method adds the new RWTCookie object to an NSSet. A set is like an array, except it cannot contain the same object more than once and the objects are not ordered (they do not have an index). 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 an NSSet or NSArray.

In this case, after you create a new RWTLevel object and call shuffle to fill it up with cookies, the RWTLevel replies, “Here is a set with all the new RWTCookie 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 next.

The View Controller

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

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

MVC

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

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

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

Note: Putting the game data and rules in separate model objects is especially useful for unit testing. This tutorial doesn’t cover unit testing but, for a game such as this, it’s a good idea to have a comprehensive set of tests for the game rules.

If game logic and sprites are all mixed up, then it’s hard to write such tests, but in this case you can test RWTLevel 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 RWTMyScene.h and replace its contents with the following:

@import SpriteKit;
 
@class RWTLevel;
 
@interface RWTMyScene : SKScene
 
@property (strong, nonatomic) RWTLevel *level;
 
- (void)addSpritesForCookies:(NSSet *)cookies;
 
@end

The scene has one public property to hold a reference to the current level.

Next, open RWTMyScene.m and add the following code at the top, just underneath the existing #import:

#import "RWTCookie.h"
#import "RWTLevel.h"
 
static const CGFloat TileWidth = 32.0;
static const CGFloat TileHeight = 36.0;
 
@interface RWTMyScene ()
 
@property (strong, nonatomic) SKNode *gameLayer;
@property (strong, nonatomic) SKNode *cookiesLayer;
 
@end

Each square of the 2-D 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, RWTMyScene 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 initWithSize: to create the layers. Put this after the code that creates the background node:

self.gameLayer = [SKNode node];
[self addChild:self.gameLayer];
 
CGPoint layerPosition = CGPointMake(-TileWidth*NumColumns/2, -TileHeight*NumRows/2);
 
self.cookiesLayer = [SKNode node];
self.cookiesLayer.position = layerPosition;
 
[self.gameLayer addChild:self.cookiesLayer];

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

Remember that earlier you set the anchorPoint of the scene to (0, 0), and the position of the scene also defaults to (0, 0). This means (0, 0) is in the center of the screen. Therefore, when you add these layers as children of the scene, by default (0, 0) in the 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 2-D 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.

The only missing piece is addSpritesForCookies:, so add it below:

- (void)addSpritesForCookies:(NSSet *)cookies {
  for (RWTCookie *cookie in cookies) {
    SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:[cookie spriteName]];
    sprite.position = [self pointForColumn:cookie.column row:cookie.row];
    [self.cookiesLayer addChild:sprite];
    cookie.sprite = sprite;
  }
}
 
- (CGPoint)pointForColumn:(NSInteger)column row:(NSInteger)row {
  return CGPointMake(column*TileWidth + TileWidth/2, 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: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 RWTViewController.m and add the following code at the top, just underneath the existing #import lines:

#import "RWTLevel.h"
 
@interface RWTViewController ()
 
@property (strong, nonatomic) RWTLevel *level;
@property (strong, nonatomic) RWTMyScene *scene;
 
@end

This class continuation includes private properties for the RWTLevel and RWTMyScene objects.

Next, add these two new methods:

- (void)beginGame {
  [self shuffle];
}
 
- (void)shuffle {
  NSSet *newCookies = [self.level shuffle];
  [self.scene addSpritesForCookies:newCookies];
}

beginGame kicks off the game by calling shuffle. This is where you call RWTLevel’s shuffle method, which returns the NSSet containing new RWTCookie 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 RWTMyScene to add sprites for those cookies.

Finally, replace viewDidLoad with the following implementation:

- (void)viewDidLoad {
  [super viewDidLoad];
 
  // Configure the view.
  SKView *skView = (SKView *)self.view;
  skView.multipleTouchEnabled = NO;
 
  // Create and configure the scene.
  self.scene = [RWTMyScene sceneWithSize:skView.bounds.size];
  self.scene.scaleMode = SKSceneScaleModeAspectFill;
 
  // Load the level.
  self.level = [[RWTLevel alloc] init];
  self.scene.level = self.level;
 
  // Present the scene.
  [skView presentScene:self.scene];
 
  // Let's start the game!
  [self beginGame];
}

This is largely the same as before, except now you place the RWTMyScene instance into self.scene and you create a new RWTLevel instance. Then, you set the level property on the scene to tie together the model and the view.

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

First cookies

Note: You may wonder why this game uses NSInteger and NSUInteger instead of just int, and CGFloat instead of float. This has everything to do with the iPhone 5s and 5c, which are 64-bit devices.

An int is only 32-bits, so using int on a 64-bit device is not ideal. The size of an NSInteger or an NSUInteger, on the other hand, is based on the architecture of the device. In other words, they are 32-bit on 32-bit devices and 64-bit on 64-bit devices. The same thing goes for a CGFloat.

It wouldn’t create a big problem to use int and float, but now that we have 64-bit devices it’s good to get into the habit of using NSInteger and CGFloat. Oh, and if you’re wondering about the difference between NSInteger and NSUInteger: the latter can only do positive numbers, not negative ones—the U stands for “unsigned”.

To learn more about the integer types in iOS, check out our Objective-C Data Types: Integer video tutorial.

Loading Levels from JSON Files

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

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

Levels-in-project-navigator

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

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

Level_1 json

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

Add a new Objective-C class file to the project. Name it RWTTile and make it a subclass of NSObject.

You don’t have to make any changes in the source files for this new class 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 RWTLevel.h and add an import for this new class:

#import "RWTTile.h"

Add these two method declarations, too:

- (instancetype)initWithFile:(NSString *)filename;
- (RWTTile *)tileAtColumn:(NSInteger)column row:(NSInteger)row;

In RWTLevel.m, add a new instance variable that describes the structure of the level:

RWTTile *_tiles[NumColumns][NumRows];

Like the _cookies variable, this is a 2-dimensional C array. Whereas the _cookies array keeps track of the RWTCookie objects in the level, _tiles simply describes which parts of the level grid are empty and which can contain a cookie:

JSON and tiles

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

Now that the instance variables for level data are in place, you can start adding the code to fill in the data. Add this method to load a JSON file:

- (NSDictionary *)loadJSON:(NSString *)filename {
  NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:@"json"];
  if (path == nil) {
    NSLog(@"Could not find level file: %@", filename);
    return nil;
  }
 
  NSError *error;
  NSData *data = [NSData dataWithContentsOfFile:path options:0 error:&error];
  if (data == nil) {
    NSLog(@"Could not load level file: %@, error: %@", filename, error);
    return nil;
  }
 
  NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
  if (dictionary == nil || ![dictionary isKindOfClass:[NSDictionary class]]) {
    NSLog(@"Level file '%@' is not valid JSON: %@", filename, error);
    return nil;
  }
 
  return dictionary;
}

This method simply loads the specified file into an NSData object and then converts that to an NSDictionary. 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 initWithFile: method to RWTLevel.m:

- (instancetype)initWithFile:(NSString *)filename {
  self = [super init];
  if (self != nil) {
    NSDictionary *dictionary = [self loadJSON:filename];
 
    // Loop through the rows
    [dictionary[@"tiles"] enumerateObjectsUsingBlock:^(NSArray *array, NSUInteger row, BOOL *stop) {
 
      // Loop through the columns in the current row
      [array enumerateObjectsUsingBlock:^(NSNumber *value, NSUInteger column, BOOL *stop) {
 
        // Note: In Sprite Kit (0,0) is at the bottom of the screen,
        // so we need to read this file upside down.
        NSInteger tileRow = NumRows - row - 1;
 
        // If the value is 1, create a tile object.
        if ([value integerValue] == 1) {
          _tiles[column][tileRow] = [[RWTTile alloc] init];
        }
      }];
    }];
  }
  return self;
}

This loads the named file into an NSDictionary using the loadJSON: helper method and then steps through the “tiles” array to look at the design of the level. Every time it finds a 1, it creates an RWTTile object and places it into the _tiles array.

Note that you have to reverse the order of the rows here, because the first row you read from the JSON corresponds to the last row of the table, since you have set up (0, 0) to be the bottom left of the table.

The final method to add is tileAtColumn:row::

- (RWTTile *)tileAtColumn:(NSInteger)column row:(NSInteger)row {
  NSAssert1(column >= 0 && column < NumColumns, @"Invalid column: %ld", (long)column);
  NSAssert1(row >= 0 && row < NumRows, @"Invalid row: %ld", (long)row);
 
  return _tiles[column][row];
}

Like the corresponding cookieAtColumn:row:, this method simply peeks into the array and returns the corresponding RWTTile object, or nil if there is no tile at that location.

At this point, the code will compile without errors but 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 RWTCookie object:

// This line is new
if (_tiles[column][row] != nil) {
 
  NSUInteger cookieType = ...
  ...
  [set addObject:cookie];
}

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

One last thing remains: In RWTViewController.m’s viewDidLoad, replace the line that creates the level object with:

self.level = [[RWTLevel alloc] initWithFile:@"Level_1"];

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

Non-square level

Making the Tiles Visible

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

To do this, first add a new private property to RWTMyScene.m:

@property (strong, nonatomic) SKNode *tilesLayer;

Then add this code to initWithSize:, right above where you create the cookiesLayer. 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):

self.tilesLayer = [SKNode node];
self.tilesLayer.position = layerPosition;
[self.gameLayer addChild:self.tilesLayer];

Add the following method to RWTMyScene.m, as well:

- (void)addTiles {
  for (NSInteger row = 0; row < NumRows; row++) {
    for (NSInteger column = 0; column < NumColumns; column++) {
      if ([self.level tileAtColumn:column row:row] != nil) {
        SKSpriteNode *tileNode = [SKSpriteNode spriteNodeWithImageNamed:@"Tile"];
        tileNode.position = [self pointForColumn:column row:row];
        [self.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.

You’ll need to call this method from other classes, so open RWTMyScene.h and add the method declaration there:

- (void)addTiles;

Next, open RWTViewController.m. Add the following line to viewDidLoad, immediately after you set self.scene.level:

[self.scene addTiles];

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

Tiles layer

You can switch to another level design by specifying a different file name in viewDidLoad. Simply change the initWithFile: 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).

Swiping to Swap Cookies

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 RWTMyScene. 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 RWTMyScene. 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 RWTMyScene.m and add two private properties:

@property (assign, nonatomic) NSInteger swipeFromColumn;
@property (assign, nonatomic) NSInteger swipeFromRow;

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 the main if block in initWithSize::

self.swipeFromColumn = self.swipeFromRow = NSNotFound;

The special value NSNotFound means that these properties have invalid values. In other words, they don’t yet point at any of the cookies.

Now add a new method touchesBegan:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // 1
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self.cookiesLayer];
 
  // 2
  NSInteger column, row;
  if ([self convertPoint:location toColumn:&column row:&row]) {
 
    // 3
    RWTCookie *cookie = [self.level cookieAtColumn:column row:row];
    if (cookie != nil) {
 
      // 4
      self.swipeFromColumn = column;
      self.swipeFromRow = row;
    }
  }
}

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

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

The convertPoint:toColumn:row: method is new. It’s the opposite of pointForColumn:row:, so you may want to add this method right below pointForColumn:row: so the two methods are nearby.

- (BOOL)convertPoint:(CGPoint)point toColumn:(NSInteger *)column row:(NSInteger *)row {
  NSParameterAssert(column);
  NSParameterAssert(row);
 
  // Is this a valid location within the cookies layer? If yes,
  // calculate the corresponding row and column numbers.
  if (point.x >= 0 && point.x < NumColumns*TileWidth &&
      point.y >= 0 && point.y < NumRows*TileHeight) {
 
    *column = point.x / TileWidth;
    *row = point.y / TileHeight;
    return YES;
 
  } else {
    *column = NSNotFound;  // invalid location
    *row = NSNotFound;
    return NO;
  }
}

This method takes a CGPoint that is relative to the cookiesLayer and converts it into column and row numbers. If the point falls outside the grid, this method returns NO.

Note: column and row are so-called output parameters. These are necessary because a method can only return a single value, but here you need the method to return three values: 1) the BOOL that indicates success or failure; 2) the column number; and 3) the row number. NSParameterAssert() ensures that the column and row pointers are not nil, which would horribly crash the app.

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:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  // 1
  if (self.swipeFromColumn == NSNotFound) return;
 
  // 2
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self.cookiesLayer];
 
  NSInteger column, row;
  if ([self convertPoint:location toColumn:&column row:&row]) {
 
    // 3 
    NSInteger horzDelta = 0, vertDelta = 0;
    if (column < self.swipeFromColumn) {          // swipe left
      horzDelta = -1;
    } else if (column > self.swipeFromColumn) {   // swipe right
      horzDelta = 1;
    } else if (row < self.swipeFromRow) {         // swipe down
      vertDelta = -1;
    } else if (row > self.swipeFromRow) {         // swipe up
      vertDelta = 1;
    }
 
    // 4
    if (horzDelta != 0 || vertDelta != 0) {
      [self trySwapHorizontal:horzDelta vertical:vertDelta];
 
      // 5
      self.swipeFromColumn = NSNotFound;
    }
  }
}

Here is what this does step by step:

  1. If swipeFromColumn is NSNotFound, 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 BOOL but using swipeFromColumn is just as easy.
  2. This is similar to what touchesBegan does to calculate the row and column numbers currently under the player’s finger.
  3. Here the method figures out the direction of the player’s swipe by simply comparing the new column and row numbers to the previous ones. Note that you’re not allowing diagonal swipes (since you’re using else if statements, only one of horzDelta or vertDelta will be set).
  4. The method only performs the swap if the player swiped out of the old square.
  5. By setting swipeFromColumn to NSNotFound, the game will ignore the rest of this swipe motion.

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

- (void)trySwapHorizontal:(NSInteger)horzDelta vertical:(NSInteger)vertDelta {
  // 1
  NSInteger toColumn = self.swipeFromColumn + horzDelta;
  NSInteger toRow = self.swipeFromRow + vertDelta;
 
  // 2
  if (toColumn < 0 || toColumn >= NumColumns) return;
  if (toRow < 0 || toRow >= NumRows) return;
 
  // 3
  RWTCookie *toCookie = [self.level cookieAtColumn:toColumn row:toRow];
  if (toCookie == nil) return;
 
  // 4
  RWTCookie *fromCookie = [self.level cookieAtColumn:self.swipeFromColumn row:self.swipeFromRow];
 
  NSLog(@"*** swapping %@ with %@", fromCookie, toCookie);
}

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

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

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

Add the following:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  self.swipeFromColumn = self.swipeFromRow = NSNotFound;
}
 
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
  [self 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 NSNotFound.

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

Valid swap

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

Animating the Swaps

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

Create a new Objective-C class file named RWTSwap, subclass of NSObject. Replace the contents of RWTSwap.h with the following:

@class RWTCookie;
 
@interface RWTSwap : NSObject
 
@property (strong, nonatomic) RWTCookie *cookieA;
@property (strong, nonatomic) RWTCookie *cookieB;
 
@end

For debugging purposes, add the following method to RWTSwap.m:

- (NSString *)description {
  return [NSString stringWithFormat:@"%@ swap %@ with %@", [super description], self.cookieA, self.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 RWTMyScene, but all the real game logic so far is in RWTViewController.

That means RWTMyScene must have a way to communicate back to RWTViewController 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 RWTMyScene must send back to RWTViewController, you’ll use a block instead.

Add the following forward declaration to the top of RWTMyScene.h:

@class RWTSwap;

Then add the following block property:

@property (copy, nonatomic) void (^swipeHandler)(RWTSwap *swap);

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

At the top of RWTMyScene.m, import the new swap class:

#import "RWTSwap.h"

Add the following code to the bottom of trySwapHorizontal:vertical:, replacing the NSLog() statement:

if (self.swipeHandler != nil) {
  RWTSwap *swap = [[RWTSwap alloc] init];
  swap.cookieA = fromCookie;
  swap.cookieB = toCookie;
 
  self.swipeHandler(swap);
}

This creates a new RWTSwap object, fills in the two cookies to be swapped and then calls the swipe handler to take care of the rest.

So RWTViewController 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 RWTMyScene.m:

- (void)animateSwap:(RWTSwap *)swap completion:(dispatch_block_t)completion {
  // Put the cookie you started with on top.
  swap.cookieA.sprite.zPosition = 100;
  swap.cookieB.sprite.zPosition = 90;
 
  const NSTimeInterval Duration = 0.3;
 
  SKAction *moveA = [SKAction moveTo:swap.cookieB.sprite.position duration:Duration];
  moveA.timingMode = SKActionTimingEaseOut;
  [swap.cookieA.sprite runAction:[SKAction sequence:@[moveA, [SKAction runBlock:completion]]]];
 
  SKAction *moveB = [SKAction moveTo:swap.cookieA.sprite.position duration:Duration];
  moveB.timingMode = SKActionTimingEaseOut;
  [swap.cookieB.sprite 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 the 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.

dispatch_block_t is simply shorthand for a block that returns void and takes no parameters.

This method needs to be publicly visible, so open RWTMyScene.h and add the method declaration:

- (void)animateSwap:(RWTSwap *)swap completion:(dispatch_block_t)completion;

Now that you’ve handled the view, there’s still the model to deal with before getting to the controller! Open RWTLevel.h and add an import:

#import "RWTSwap.h"

Also add the method signature:

- (void)performSwap:(RWTSwap *)swap;

Then implement performSwap: in RWTLevel.m:

- (void)performSwap:(RWTSwap *)swap {
  NSInteger columnA = swap.cookieA.column;
  NSInteger rowA = swap.cookieA.row;
  NSInteger columnB = swap.cookieB.column;
  NSInteger 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 RWTCookie objects because they get overwritten. To make the swap, it updates the _cookies array, as well as the column and row properties of the RWTCookie objects, which shouldn’t go out of sync. That’s it for the data model.

Go to RWTViewController.m and add the following code to viewDidLoad, just before the line that presents the scene:

id block = ^(RWTSwap *swap) {
  self.view.userInteractionEnabled = NO;
 
  [self.level performSwap:swap];
  [self.scene animateSwap:swap completion:^{
    self.view.userInteractionEnabled = YES;
  }];
};
 
self.scene.swipeHandler = block;

This creates the block and assigns it to RWTMyScene’s swipeHandler property. Inside the block, 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 block.

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.

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

Swap cookies

Highlighting the Cookies

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

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

You will improve RWTMyScene 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 RWTMyScene.m, add a new private property:

@property (strong, nonatomic) SKSpriteNode *selectionSprite;

Create this object at the bottom of the if block in initWithSize::

self.selectionSprite = [SKSpriteNode node];

Add the following method:

- (void)showSelectionIndicatorForCookie:(RWTCookie *)cookie {
  // If the selection indicator is still visible, then first remove it.
  if (self.selectionSprite.parent != nil) {
    [self.selectionSprite removeFromParent];
  }
 
  SKTexture *texture = [SKTexture textureWithImageNamed:[cookie highlightedSpriteName]];
  self.selectionSprite.size = texture.size;
  [self.selectionSprite runAction:[SKAction setTexture:texture]];
 
  [cookie.sprite addChild:self.selectionSprite];
  self.selectionSprite.alpha = 1.0;
}

This gets the name of the highlighted sprite image from the RWTCookie 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.

Also add the opposite method, hideSelectionIndicator:

- (void)hideSelectionIndicator {
  [self.selectionSprite runAction:[SKAction sequence:@[
    [SKAction fadeOutWithDuration:0.3],
    [SKAction removeFromParent]]]];
}

This method removes the selection sprite by fading it out.

It remains for you to call these methods. First, in touchesBegan, in the if (cookie != nil) section, add:

[self showSelectionIndicatorForCookie:cookie];

And in touchesMoved, after the call to trySwapHorizontal:vertical:, add:

[self 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 (self.selectionSprite.parent != nil && self.swipeFromColumn != NSNotFound) {
  [self hideSelectionIndicator];
}

Build and run, and highlight some cookies!

Highlighted cookies

A Smarter Way to Fill the Array

The purpose of this game is to make chains of three or more of the same cookie. But right now, when you run the game there may already be such chains on the screen. That’s no good—you only want there to be matches after the user swaps two cookies or after new cookies fall 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 be on the board already. To guarantee this is the case, you have to make the method that fills up the cookies array a bit smarter.

Go to RWTLevel.m and find createInitialCookies. Replace the single line that calculates the random cookieType using arc4random_uniform with the following:

NSUInteger cookieType;
do {
  cookieType = arc4random_uniform(NumCookieTypes) + 1;
}
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:

do {
  generate a new random number between 1 and 6
} 
while (there are already two cookies of this type to the left
    or there are already two cookies of this type below);

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

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

No chains in initial state

Not All Swaps Are Allowed

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

Allowed swap

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

Or—and this is what you’ll do in this tutorial—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, if you don’t play for a few seconds, 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 RWTLevel.m, add a new private property:

@interface RWTLevel ()
 
@property (strong, nonatomic) NSSet *possibleSwaps;
 
@end

Again, you’re using an NSSet here instead of an NSArray because the order of the elements in this collection isn’t important. This NSSet will contain RWTSwap 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.

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. Change the code for that method to:

- (NSSet *)shuffle {
  NSSet *set;
  do {
    set = [self createInitialCookies];
 
    [self detectPossibleSwaps];
 
    NSLog(@"possible swaps: %@", self.possibleSwaps);
  }
  while ([self.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:

- (BOOL)hasChainAtColumn:(NSInteger)column row:(NSInteger)row {
  NSUInteger cookieType = _cookies[column][row].cookieType;
 
  NSUInteger horzLength = 1;
  for (NSInteger i = column - 1; i >= 0 && _cookies[i][row].cookieType == cookieType; i--, horzLength++) ;
  for (NSInteger i = column + 1; i < NumColumns && _cookies[i][row].cookieType == cookieType; i++, horzLength++) ;
  if (horzLength >= 3) return YES;
 
  NSUInteger vertLength = 1;
  for (NSInteger i = row - 1; i >= 0 && _cookies[column][i].cookieType == cookieType; i--, vertLength++) ;
  for (NSInteger i = row + 1; i < NumRows && _cookies[column][i].cookieType == cookieType; i++, vertLength++) ;
  return (vertLength >= 3);
}

A chain is three or more consecutive cookies of the same type in a row or column. This method may look a little strange but that’s because it stuffs a lot of the logic inside the for-statements.

Look left right up down

Given a cookie in a particular square on the grid, this method first looks to the left. As long as it finds a cookie of the same type, it increments horzLength and keeps going left. This is expressed succinctly in a single line of code:

for (NSInteger i = column - 1; i >= 0 && _cookies[i][row].cookieType == cookieType; i--, horzLength++) ;

This for loop has no body, only a semicolon. That means all the logic happens inside its parameters.

for (NSInteger i = column - 1; // start on the left of the current cookie
 
     i >= 0 &&                                  // keep going while not left-most column reached
     _cookies[i][row].cookieType == cookieType; // and still the same cookie type
 
     i--,           // go to the next column on the left
     horzLength++)  // and increment the length
 
     ;              // do nothing inside the loop

You can also write this out using a while statement but the for loop allows you to fit everything on a single line. :] There are also loops for looking to the right, above and below.

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. In other programming languages, you have to do an explicit check for nil, but in Objective-C, if an object is nil and you access a property, that also returns nil or 0.

So if there’s no RWTCookie object at _cookies[a][b], then _cookies[a][b].cookieType is 0. Valid cookie types are numbers between 1 and 6, so 0 will never match any cookie. Therefore, the logic in hasChainAtColumn:row: works even if there are gaps in the level. When that happens, the for loop will immediately terminate.

And that’s why I made cookieType start at 1—so I didn’t have to add another nil check into these loops! This sort of thing may not be immediately obvious when you read the code. :]

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

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

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

First, add the outline of the method:

- (void)detectPossibleSwaps {
  NSMutableSet *set = [NSMutableSet set];
 
  for (NSInteger row = 0; row < NumRows; row++) {
    for (NSInteger column = 0; column < NumColumns; column++) {
 
      RWTCookie *cookie = _cookies[column][row];
      if (cookie != nil) {
 
        // TODO: detection logic goes here
      }
    }
  }
 
  self.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 self.possibleSwaps.

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.
  RWTCookie *other = _cookies[column + 1][row];
  if (other != nil) {
    // Swap them
    _cookies[column][row] = other;
    _cookies[column + 1][row] = cookie;
 
    // Is either cookie now part of a chain?
    if ([self hasChainAtColumn:column + 1 row:row] ||
        [self hasChainAtColumn:column row:row]) {
 
      RWTSwap *swap = [[RWTSwap alloc] init];
      swap.cookieA = cookie;
      swap.cookieB = other;
      [set addObject:swap];
    }
 
    // 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 RWTSwap object to the set.

Now add the following code directly below the code above:

if (row < NumRows - 1) {
 
  RWTCookie *other = _cookies[column][row + 1];
  if (other != nil) {
    // Swap them
    _cookies[column][row] = other;
    _cookies[column][row + 1] = cookie;
 
    if ([self hasChainAtColumn:column row:row + 1] ||
        [self hasChainAtColumn:column row:row]) {
 
      RWTSwap *swap = [[RWTSwap alloc] init];
      swap.cookieA = cookie;
      swap.cookieB = other;
      [set addObject:swap];
    }
 
    _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: {(
    <RWTSwap: 0x960a480> swap type:6 square:(1,7) with type:1 square:(2,7),
    <RWTSwap: 0x960a4b0> swap type:2 square:(4,5) with type:4 square:(5,5),
    <RWTSwap: 0x960ac10> swap type:2 square:(5,7) with type:5 square:(6,7),
    <RWTSwap: 0x960ac40> swap type:4 square:(7,5) with type:3 square:(8,5),
    <RWTSwap: 0x960a470> swap type:1 square:(2,7) with type:5 square:(2,8),
    <RWTSwap: 0x960a4a0> swap type:1 square:(5,6) with type:2 square:(6,6),
    . . .
)}

To Swap or Not to Swap…

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

- (BOOL)isPossibleSwap:(RWTSwap *)swap {
  return [self.possibleSwaps containsObject:swap];
}

You also need to add the method signature to RWTLevel.h so other classes can ask it whether a swap is possible:

- (BOOL)isPossibleSwap:(RWTSwap *)swap;

Finally call the method in RWTViewController.m, in the swipe handler block:

id block = ^(RWTSwap *swap) {
  self.view.userInteractionEnabled = NO;
 
  if ([self.level isPossibleSwap:swap]) {
    [self.level performSwap:swap];
    [self.scene animateSwap:swap completion:^{
      self.view.userInteractionEnabled = YES;
    }];
  } else {
   self.view.userInteractionEnabled = YES;
  }
};

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

Build and run to try it out. Hmm, something isn’t right—now you can’t make any swaps!

itsbroke

Here’s what’s happening:

  1. RWTLevel’s NSSet with possibleSwaps contains RWTSwap objects that describe all the allowed moves.
  2. But when you perform a swipe, RWTMyScene creates a new RWTSwap object.
  3. When isPossibleSwap: looks for that swap in its list, of course it does not find it. It may have an RWTSwap object that describes exactly the same move, but the actual instances in memory are different.

When you run [set containsObject:obj], the set calls isEqual: on that object and all the objects it contains to determine if they match. By default, isEqual: only looks at the pointer value of the object and, as you discovered, those pointer values will never match up.

The solution is to override isEqual: on the RWTSwap object to be a little smarter. In RWTSwap.m, add the following method:

- (BOOL)isEqual:(id)object {
  // You can only compare this object against other RWTSwap objects.
  if (![object isKindOfClass:[RWTSwap class]]) return NO;
 
  // Two swaps are equal if they contain the same cookie, but it doesn't
  // matter whether they're called A in one and B in the other.
  RWTSwap *other = (RWTSwap *)object;
  return (other.cookieA == self.cookieA && other.cookieB == self.cookieB) ||
         (other.cookieB == self.cookieA && other.cookieA == self.cookieB);
}

When you overwrite isEqual:, it is crucial that you also provide an implementation of the hash method. These two go hand-in-hand. If you forget to provide your own version of hash, then [NSSet containsObject:] still won’t work properly.

Add the hash method below:

- (NSUInteger)hash {
  return [self.cookieA hash] ^ [self.cookieB hash];
}

Also, import RWTCookie so this will work:

#import "RWTCookie.h"

The rule is that if two objects are equal, then their hashes must also be equal. The hash value must be as unique as possible. Combining the hashes of the two RWTCookies with a bitwise XOR works well. The hash of an RWTCookie object is simply its pointer.

Build and run again. Now you should be able to make swaps again, but only those that will result in a chain.

Ignore invalid swap

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

Animating Invalid Swaps

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

- (void)animateInvalidSwap:(RWTSwap *)swap completion:(dispatch_block_t)completion {
  swap.cookieA.sprite.zPosition = 100;
  swap.cookieB.sprite.zPosition = 90;
 
  const NSTimeInterval Duration = 0.2;
 
  SKAction *moveA = [SKAction moveTo:swap.cookieB.sprite.position duration:Duration];
  moveA.timingMode = SKActionTimingEaseOut;
 
  SKAction *moveB = [SKAction moveTo:swap.cookieA.sprite.position duration:Duration];
  moveB.timingMode = SKActionTimingEaseOut;
 
  [swap.cookieA.sprite runAction:[SKAction sequence:@[moveA, moveB, [SKAction runBlock:completion]]]];
  [swap.cookieB.sprite runAction:[SKAction sequence:@[moveB, moveA]]];
}

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

Don’t forget to add the method signature to RWTMyScene.h, as well:

- (void)animateInvalidSwap:(RWTSwap *)swap completion:(dispatch_block_t)completion;

In RWTViewController.m, change the else-clause inside the swipe handler to:

} else {
  [self.scene animateInvalidSwap:swap completion:^{
    self.view.userInteractionEnabled = YES;
  }];
}

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

Invalid swap

Make Some Noise

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 RWTMyScene.m:

@property (strong, nonatomic) SKAction *swapSound;
@property (strong, nonatomic) SKAction *invalidSwapSound;
@property (strong, nonatomic) SKAction *matchSound;
@property (strong, nonatomic) SKAction *fallingCookieSound;
@property (strong, nonatomic) SKAction *addCookieSound;

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.

Add the following method:

- (void)preloadResources {
  self.swapSound = [SKAction playSoundFileNamed:@"Chomp.wav" waitForCompletion:NO];
  self.invalidSwapSound = [SKAction playSoundFileNamed:@"Error.wav" waitForCompletion:NO];
  self.matchSound = [SKAction playSoundFileNamed:@"Ka-Ching.wav" waitForCompletion:NO];
  self.fallingCookieSound = [SKAction playSoundFileNamed:@"Scrape.wav" waitForCompletion:NO];
  self.addCookieSound = [SKAction playSoundFileNamed:@"Drip.wav" waitForCompletion:NO];
}

Call this from initWithSize: at the bottom of the main if block.

[self preloadResources];

Then add the following line to the bottom of animateSwap:completion:

[self runAction:self.swapSound];

And add this line to the bottom of animateInvalidSwap:

[self runAction:self.invalidSwapSound];

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

Where to Go From Here?

Here is a sample project with all of the code from the 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, though, give yourself a cookie for making it halfway!

In the next part, you’ll implement the remaining pieces of the game flow: removing matching chains and replacing them with new cookies. You’ll also add scoring, lots of new animations and some final polish.

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

Credits: Artwork by Vicki Wenderlich. The music is by Kevin MacLeod. The sound effects are based on samples from freesound.org.

How to Make a Game Like Candy Crush: Part 1 is a post from: Ray Wenderlich

The post How to Make a Game Like Candy Crush: Part 1 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4400

Trending Articles