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

Procedural Level Generation in Games using a Cellular Automaton: Part 1

$
0
0
Learn how to add a procedural cave system into your game!

Learn how to add a procedural cave system into your game!

In this tutorial series, you’ll use a discreet model called cellular automaton to generate procedural caves for your games. You’ll also learn how to overcome some of the obstacles this model imposes on level generation, like removing unreachable areas of the level and ensuring the exit can always be reached by the player.

If you have read the previous tutorial on procedural level generation on this site, you already know the basics of procedural level generation using the Drunkard Walk algorithm. The Drunkard Walk is a reliable, battle-tested algorithm that generates levels, but it’s just one of many options out there.

This tutorial series uses Sprite Kit, a framework introduced with iOS 7. You will also need Xcode 5. If you are not already familiar with Sprite Kit, I recommend you read the Sprite Kit Tutorial for Beginners on this site. For readers who are using a different game framework, fear not. You can easily use the ideas from this tutorial in the game framework of your choice.

By now, you’re probably asking yourself, “What on Earth is a cellular automaton, anyway?” Time to indulge in a little theory.

Cellular Automata Explained

A cellular automaton (pl. cellular automata) is a discreet computational model first discovered in the 1940s. Experts in mathematics, physics and biology have studied it extensively, and while it has produced mountains of complex mathematics, the basic concept is really simple.

At its core, a cellular automaton consists of an n-dimensional grid with a number of cells in a finite state and a set of transition rules. Each cell of the grid can be in one of several states; in the simplest case, cells can be on or off.

The initial distribution of cell states constitutes the seed of the automaton in an initial state (t0).

A new generation is created (advancing t by 1) by applying the transition rules to all cells simultaneously, thereby putting every cell in a new state in terms of its current state and the current states of all the cells in its neighborhood. The neighborhood defines which cells around a given cell affect its future state.

For a two-dimensional automata, the two most common types of neighborhoods are Moore neighborhoods and von Neumann neighborhoods, as illustrated below.

Moore and von Neumann neighborhoods

A Moore neighborhood (a) is a square: a Moore neighborhood of size 1 consists of the eight cells surrounding c, including those surrounding it diagonally.

A von Neumann neighborhood (b) is like a cross centered on P: above, below, left and right.

A well-known example of cellular automata to many game developers is Conway’s Game of Life, which is a two-dimensional grid of cells with each cell in a state of either dead or alive. Four transition rules govern the grid:

  1. Any live cell with fewer than two live neighbors dies, as if caused by under-population.
  2. Any live cell with two or three live neighbors lives on to the next generation.
  3. Any live cell with more than three live neighbors dies, as if by overcrowding.
  4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

You’ll be implementing a variation of Game of Life to generate cave-like levels in this tutorial. But enough theory. Time to code.

Getting Started

To get started, download the starter project.

Unzip it and double-click CellularAutomataStarter\CellularAutomataStarter.xcodeproj to open it in Xcode. Build and run using the iPhone Retina (3.5-inch) scheme. You’ll see the following on screen:

Starter Project first run

That is one sad knight. No dragons, no damsels, no battles and no glory in sight! Good thing you’re here to change that. How about you create a treasure-laden cave labyrinth for the knight to explore?

The starter project contains all the assets you’ll need for this tutorial and a few important classes:

  • Cave: An SKNode subclass that contains a stub implementation of the cave class. You’ll extend this throughout the tutorial.
  • DPad: Provides a basic implementation of a joystick so the player can control the knight
  • MyScene: Sets up the Sprite Kit scene and processes game logic
  • Player: A SKSpriteNode subclass that contains the knight’s logic

Take a moment to browse the project and familiarize yourself with the setup. The code has plenty of comments to help you understand how it works. Are you ready to give your knight something to do?

Implementing a Cellular Automaton

Your first step to implement a cellular automaton is to create a grid and put cells into the grid.

Open Cave.m and add the following private property to the class extension:

@property (strong, nonatomic) NSMutableArray *grid;

You made the property for the grid private, because you shouldn’t modify it directly from outside the class. Besides, the state of the grid will update via the transition rules you’ll add later in the tutorial.

Next, create a new class to serve as a cell in your grid.

Create a new file by selecting File\New\File…, choose iOS\Cocoa Touch\Objective-C class and click Next. Name the class CaveCell, make it a Subclass of NSObject, click Next, make sure the CellularAutomataStarter target is checked, and click Create.

Each cell should be in a finite state, so add the following enumeration to the CaveCell.h class after the #import statement:

typedef NS_ENUM(NSInteger, CaveCellType) {
  CaveCellTypeInvalid = -1,
  CaveCellTypeWall,
  CaveCellTypeFloor,
  CaveCellTypeMax
};

This defines the possible states for cells in the game, along with CaveCellTypeMax that always has a value of one greater than the last real state. Adding a value like this to an enum definition makes it easy to do things like loop through all the possible values.

Also in CaveCell.h, add the following code to the @interface section:

@property (assign, nonatomic) CGPoint coordinate;
@property (assign, nonatomic) CaveCellType type;
 
- (instancetype)initWithCoordinate:(CGPoint)coordinate;

This adds two public properties to the class, one to store the cell’s coordinates within the grid and one to store the cell’s type (or state). You also added an initializer to construct a new cell with the given coordinates.

Implement the initializer in CaveCell.m by adding the following code in the @implementation section:

- (instancetype)initWithCoordinate:(CGPoint)coordinate
{
  if ((self = [super init])) {
    _coordinate = coordinate;
    _type = CaveCellTypeInvalid;
  }
  return self;
}

The initializer creates a new CaveCell instance with the coordinate given and sets the type to CaveCellTypeInvalid. Later, you’ll set the type of the cell to either a wall (CaveCellTypeWall) or a floor (CaveCellTypeFloor).

You’ve now created the foundation for two of the three core parts of a cellular automaton: the grid and the cell. Next, you’ll create a method to put the cells into the grid.

Open Cave.m and import the CaveCell.h header:

#import "CaveCell.h"

Still inside Cave.m, add the following method that initializes the grid:

- (void)initializeGrid
{
  self.grid = [NSMutableArray arrayWithCapacity:(NSUInteger)self.gridSize.height];
 
  for (NSUInteger y = 0; y < self.gridSize.height; y++) {
    NSMutableArray *row = [NSMutableArray arrayWithCapacity:(NSUInteger)self.gridSize.width];
 
    for (NSUInteger x = 0; x < self.gridSize.width; x++) {
      CGPoint coordinate = CGPointMake(x, y);
      CaveCell *cell = [[CaveCell alloc] initWithCoordinate:coordinate];
      cell.type = CaveCellTypeFloor;
      [row addObject:cell];
    }
 
    [self.grid addObject:row];
  }
}

If you look at initWithAtlasNamed:gridSize: in Cave.m, you’ll see that you initialize instances of Cave by passing in the size of the grid, in cells. You use this information in initializeGrid to create a two-dimensional mutable array (i.e. an array of arrays) for the grid and assign a floor cell to each position in the grid.

Do you notice anything strange about how the grid is set up?

Solution Inside: Solution SelectShow>

To allow generating a new cave from the Cave class, add this new method declaration to Cave.h:

- (void)generateWithSeed:(unsigned int)seed;

Now, implement it in Cave.m:

- (void)generateWithSeed:(unsigned int)seed
{
  NSLog(@"Generating cave...");
  NSDate *startDate = [NSDate date];
 
  [self initializeGrid];
 
  NSLog(@"Generated cave in %f seconds", [[NSDate date] timeIntervalSinceDate:startDate]);
}

This method initializes the grid using initializeGrid and logs the time it took to generate the cave. The method also accepts a seed parameter. Although you don’t do anything with this parameter yet, soon you will implement code that allows you to generate different caves by using different values for this parameter.

But if you pass the same value, it will always generate the same cave. In this tutorial, you’ll always pass the same value – 0 – to make it easier to observe changes.

Tip: If you want to generate a random cave every time, just pass time(0) as the seed value in your call to generateWithSeed:.

To test that everything works as intended open MyScene.m and add the following import statement:

#import "Cave.h"

Also, add the following private property to the class extension:

@property (strong, nonatomic) Cave *cave;

You’re going to use this property to ensure easy access to the Cave object you generate.

Finally, add the following code after the inline comment // Add code to generate new cave here in initWithSize::

_cave = [[Cave alloc] initWithAtlasNamed:@"tiles" gridSize:CGSizeMake(64.0f, 64.0f)];
_cave.name = @"CAVE";
[_cave generateWithSeed:0];
[_world addChild:_cave];

This will instantiate a new Cave object using a texture atlas named “tiles” and then generate the cave with a seed value of 0. Then _cave is added as a child of _world (rather than the scene itself, which will make scrolling easier later).

Build and run the game. Check to see if the console has an entry like this:

Output to console

At the moment, there is no visual difference to the game. You should do something about that. :]

Creating Tiles

Do you remember passing the name of a texture atlas to the initializer when you created _cave during initialization of MyScene? Your next task is to add the code to create the tiles for the cave using the textures in the tiles atlas.

Before you add the method to create tiles, it’s convenient to have a couple helper methods: one to determine if a grid coordinate is valid, and one to get a cell from the grid based on the coordinates.

Add the following methods to Cave.m:

- (BOOL)isValidGridCoordinate:(CGPoint)coordinate
{
  return !(coordinate.x < 0 ||
           coordinate.x >= self.gridSize.width ||
           coordinate.y < 0 ||
           coordinate.y >= self.gridSize.height);
}
 
- (CaveCell *)caveCellFromGridCoordinate:(CGPoint)coordinate
{
  if ([self isValidGridCoordinate:coordinate]) {
    return (CaveCell *)self.grid[(NSUInteger)coordinate.y][(NSUInteger)coordinate.x];
  }
 
  return nil;
}

These methods are pretty straightforward.

  • isValidGridCoordinate: checks tile coordinates against the grid’s size to ensure they are within the range of possible grid coordinates.
  • caveCellFromGridCoordinate: returns a CaveCell instance based on the grid coordinate. Remember that this is backwards to what you might expect (it uses self.grid[y][x] instead of self.grid[x][y]) due to the way you set up the arrays, as discussed earlier. It returns nil if the grid coordinate is invalid.

With these methods in place, you can now implement the method to generate the grid’s tiles. Still in Cave.m add the following method:

- (void)generateTiles
{
  for (NSUInteger y = 0; y < self.gridSize.height; y++) {
    for (NSUInteger x = 0; x < self.gridSize.width; x++) {
      CaveCell *cell = [self caveCellFromGridCoordinate:CGPointMake(x, y)];
 
      SKSpriteNode *node;
 
      switch (cell.type) {
        case CaveCellTypeWall:
          node = [SKSpriteNode spriteNodeWithTexture:[self.atlas textureNamed:@"tile2_0"]];
          break;
 
        default:
          node = [SKSpriteNode spriteNodeWithTexture:[self.atlas textureNamed:@"tile0_0"]];
          break;
      }
 
      // Add code to position node here:
 
      node.blendMode = SKBlendModeReplace;
      node.texture.filteringMode = SKTextureFilteringNearest;
 
      [self addChild:node];
    }
  }
}

The code above simply loops through the grid to build sprites based on the types of cells.

Note: This sets the blend mode to replace, because there is no alpha transparency in these cells. It also sets the filtering mode to nearest, which gives a nice pixel-art style.

To create the tiles, add the following code to generateWithSeed: in Cave.m, just after [self initializeGrid];:

[self generateTiles];

Build and run. You should now see the following, it’s not much yet, but your knight is one step closer to adventure:

Tile Position Incorrect

Positioning Tiles

The tiles were created based on the node count, but why can’t you see them? They’re stacked atop each other! Once you create a tile, you need to calculate the position for it in the cave. Take a look at this diagram.

Calculating position for a tile

At the top, row numbers begin at 0 and increase towards the bottom, while column numbers begin at 0 on the left and increase towards the right. Remember that the grid array is indexed by (row (y), column (x)). So you’re not crazy and this tutorial was not written with a whiskey in hand, the grid is purposefully reversed.

As you see in the diagram, you need the size of a tile to calculate its position correctly. That is why you’ll add a handy property to the Cave class to get the size. Open Cave.h and add the following property:

@property (assign, nonatomic, readonly) CGSize tileSize;

The property is read only; it will not change once a Cave instance generates.

Open Cave.m and add this method:

- (CGSize)sizeOfTiles
{
  SKTexture *texture = [self.atlas textureNamed:@"tile0_0"];
  return texture.size;
}

This returns the size of one of the tile textures in the cave’s atlas, and assumes all are the same size. This is a fair assumption, considering how it builds tile maps.

Go to initWithAtlasNamed:gridSize: in Cave.m and add the following line of code after the line _gridSize = gridSize;:

_tileSize = [self sizeOfTiles];

Next, you need to create a new method to do the actual calculation of the tile position. Add this new method to Cave.m:

- (CGPoint)positionForGridCoordinate:(CGPoint)coordinate
{
  return CGPointMake(coordinate.x * self.tileSize.width + self.tileSize.width / 2.0f,
    (coordinate.y * self.tileSize.height + self.tileSize.height / 2.0f));
}

As you see, the calculation in this method corresponds to the calculation illustrated in the diagram above. It simply multiplies the given x and y coordinates with the tile’s width and height, respectively.

You add half the tile’s width and height to those values because you’re calculating the tile’s center point.

The last step is to add the positioning to the tile upon creation. Go back to generateTiles and add this single line of code after the comment // Add code to position node here::

node.position = [self positionForGridCoordinate:CGPointMake(x, y)];

Build and run the game and use the joystick to move around the cave.

The tiles are aligning perfectly

This isn’t exactly a cave, is it? More like a giant wasteland. Why are there no walls?

Solution Inside: Solution SelectShow>

The Initial Seed

Now that the boilerplate code is in place to manage the grid and tile creation, you can safely move on to implementing the first step in the cellular automaton creation: the initial distribution of cell states.

You’re going to start by randomly setting each cell to be either a wall or a floor. You’ll want to tweak the chance of a cell becoming either a wall or a floor during the cave generation, so, you add the following property to Cave.h:

@property (assign, nonatomic) CGFloat chanceToBecomeWall;

The value of chanceToBecomeWall will be in the range of 0.0 to 1.0. A value of 0.0 means all cells in the cave will become floors, and a value of 1.0 means all cells in the cave will become walls.

Inside Cave.m, set the default value of chanceToBecomeWall to 0.45 by adding the following code to initWithAtlasNamed:gridSize: after the line that initializes _tileSize:

_chanceToBecomeWall = 0.45f;

This will mean that there is a 45% chance that a cell become a wall. Why 0.45, you might ask? This value comes from good, old fashioned trial-and-error, and it tends to give satisfactory results.

You’ll need to generate random numbers quite a few times during cave generation, so add the following method to Cave.m:

- (CGFloat) randomNumberBetween0and1
{
  return random() / (float)0x7fffffff;
}

This method returns a value between 0 and 1. It uses the random() function, which needs to be seeded before use. This should only happen once per cave generation, so add the following line in generateWithSeed:, just before the line that calls initializeGrid:

srandom(seed);

At the moment, all cells in the cave are floors, but it won’t always be a two-dimensional environment. You need to take into account the fact that some cells will become a wall or a floor by modifying initializeGrid.

Inside initializeGrid in Cave.m, replace the line cell.type = CaveCellTypeFloor; with the following line of code:

cell.type = [self randomNumberBetween0and1] < self.chanceToBecomeWall ? CaveCellTypeWall : CaveCellTypeFloor;

Instead of defaulting every cell to be a floor, each cell gets a type based on the value returned by the random number generator, taking into account the chanceToBecomeWall property.

Time to see the fruit of your labor thus far. Build and run, and you should now see the following:

Initial seeded cave

Try moving around using the joystick. Not very cave-like, is it? So, what’s next?

Growing the cave

The next step you take is to apply the transition rules of the cellular automaton. Remember the rules that governed the cells in Conway’s Game of Life? The transition rules are applied to all cells simultaneously thereby changing the state of the cells based on their neighborhood.

Exactly the same approach will grow the cave. You’ll create a method that iterates each cell in the grid, and applies the transition rules to decide whether the cell will be a wall or a floor.

The below figure illustrates how applying the transition rules will impact cave generation:

The effect of transition rules on cave development
The first thing you do is define the transition rules. In this tutorial, you apply these rules:

  1. If a cell is a wall and less than 3 cells in the Moore neighborhood are walls, the cell changes state to a floor.
  2. If a cell is a floor and greater than 4 cells in the Moore neighborhood are walls, the cell changes state to a wall.
  3. Otherwise, the cell remains in its current state.

To make the cave generation as flexible as possible, add these two new properties to Cave.h:

@property (assign, nonatomic) NSUInteger floorsToWallConversion;
@property (assign, nonatomic) NSUInteger wallsToFloorConversion;

These properties allow you to set the number of floors or walls that mark the thresholds for the transition rules described earlier. You need to give them a default value, so open Cave.m and add the following code to initWithAtlasNamed:gridSize:, just after the line that initializes _chanceToBecomeWall:

_floorsToWallConversion = 4;
_wallsToFloorConversion = 3;

The default values are the result of trial-and-error and known to give good results. But don’t take my word for it, play around with these property values to see how they affect the cave generation.

Your last step before applying transition rules is to create a method to count the number of walls in the Moore neighborhood of a cell. Do you remember how many cells are in the Moore neighborhood?

Solution Inside: Solution SelectShow>

Add the following method to Cave.m:

- (NSUInteger)countWallMooreNeighborsFromGridCoordinate:(CGPoint)coordinate
{
  NSUInteger wallCount = 0;
 
  for (NSInteger i = -1; i < 2; i++) {
    for (NSInteger j = -1; j < 2; j++) {
      // The middle point is the same as the passed Grid Coordinate, so skip it
      if ( i == 0 && j == 0 ) {
        break;
      }
 
      CGPoint neighborCoordinate = CGPointMake(coordinate.x + i, coordinate.y + j);
      if (![self isValidGridCoordinate:neighborCoordinate]) {
        wallCount += 1;
      } else if ([self caveCellFromGridCoordinate:neighborCoordinate].type == CaveCellTypeWall) {
        wallCount += 1;
      }
    }
  }
  return wallCount;
}

The for loops might look a bit weird at first, but there is a method to the madness. The idea is to count the number of wall cells around the grid coordinates (x, y).

If you look at the illustration below, you can see coordinates for neighbors are one less, equal to, and one greater than the original coordinate. Your two for loops give you just that, starting at -1 and looping through +1.

Counting walls in the Moore neighborhood

You might have noticed the code counts invalid grid coordinates (for instance, coordinates that are off the edge of the grid) as walls. This will help fill in the edges of the cave, but it is a matter of preference if you want to do this. You can experiment by not doing it, if you like.

Add this method to Cave.m to perform a transition step to all the cells in the grid:

- (void)doTransitionStep
{
  // 1
  NSMutableArray *newGrid = [NSMutableArray arrayWithCapacity:(NSUInteger)self.gridSize.height];
 
  // 2
  for (NSUInteger y = 0; y < self.gridSize.height; y++) {
    NSMutableArray *newRow = [NSMutableArray arrayWithCapacity:(NSUInteger)self.gridSize.width];
    for (NSUInteger x = 0; x < self.gridSize.width; x++) {
      CGPoint coordinate = CGPointMake(x, y);
 
      // 3
      NSUInteger mooreNeighborWallCount = [self countWallMooreNeighborsFromGridCoordinate:coordinate];
 
      // 4
      CaveCell *oldCell = [self caveCellFromGridCoordinate:coordinate];
      CaveCell *newCell = [[CaveCell alloc] initWithCoordinate:coordinate];
 
      // 5
      // 5a
      if (oldCell.type == CaveCellTypeWall) {
        newCell.type = (mooreNeighborWallCount < self.wallsToFloorConversion) ?
          CaveCellTypeFloor : CaveCellTypeWall;
      } else {
        // 5b
        newCell.type = (mooreNeighborWallCount > self.floorsToWallConversion) ?
          CaveCellTypeWall : CaveCellTypeFloor;
      }
      [newRow addObject:newCell];
    }
    [newGrid addObject:newRow];
  }
 
  // 6
  self.grid = newGrid;
}

Let’s go over this section-by-section:

  1. Create a new grid that will be the state of the grid after the transition step has been performed. To understand why this is needed, remember that to calculate the new value of a cell in the grid, you need to look at the eight neighbors in the Moore neighborhood. If you already calculated the new value of some of the cells and put them back in the grid, then the calculation will be a mix of new and old data.
  2. Iterate through all the cells in the grid. You use two for loops for this purpose.
  3. For each cell, you use the countWallMooreNeighborsFromGridCoordinate: method you added earlier to calculate the number of walls in the Moore neighborhood.
  4. A copy (newCell) of the current cell (oldCell) is made for the reason stated in section 1.
  5. The transition rules apply to the cell. Based on the cell type (wall or floor), it checks the number of wall cells in the Moore neighborhood against the limits for changing the cell.
  • 5a: If the cell is a wall and mooreNeighborWallCount is less than the value of wallsToFloorConversion, then the wall changes to a floor (transition rule 1). Otherwise, it remains a wall (transition rule 3).
  • 5b: If the cell is a floor and the MooreNeighborWallCount is greater than the value of the floorsToWallConversion, then the floor changes to a wall (transition rule 2). Otherwise, it remains a floor (transition rule 3).
  • The transitioned grid of cells is assigned to the grid property. ARC will automatically free memory for the old grid.
  • Now you’ve almost implemented the transition step of the cellular automaton. The only thing missing is actually performing the transition step or steps when you generate the cave.

    To be flexible, add another property to add the ability to set how many transition steps perform when the cave generates. Open Cave.h and add the following property:

    @property (assign, nonatomic) NSUInteger numberOfTransitionSteps;

    Experimentation revealed that using two transitions steps works well. Initialize the property by adding this to initWithAtlasNamed:gridSize: in Cave.m, just after the line that initializes _wallsToFloorConversion:

    _numberOfTransitionSteps = 2;

    Still in Cave.m, add the following for loop to generateWithSeed: between the lines [self initializeGrid]; and [self generateTiles];:

    for (NSUInteger step = 0; step < self.numberOfTransitionSteps; step++) {
      NSLog(@"Performing transition step %lu", (unsigned long)step + 1);
      [self doTransitionStep];
    }

    This simply runs the desired number of transition steps by calling doTransitionStep at each iteration of the loop.

    Now you’ve implemented the cellular automaton fully. Build and run to see how the transition steps affect cave generation.

    Disconnected caverns

    At this point, you have a nice, realistic looking cave, but did you notice there are unreachable caverns? In the above image, green highlights indicate where the unreachable areas lie.

    Since the knight has no holy hand grenades, this leaves the player in a bad way, so you’ll need to fix that. Or, maybe you could make that an In-App Purchase. ;]

    Holy Hand Grenade In-App Purchase

    Identifying Caverns

    Flood-Fill

    Recursive flood-fill with four directions

    Good thing there’s a fix for the knight, and that is to identify each cavern that is part of the cave. For the sake of clarity with this tutorial, a cavern is defined as an area that isn’t connected to any other areas.

    Once you identify individual caverns, you can then either connect or remove them all, except the largest cavern. You’ll be doing both in this tutorial.

    The best way to identify the caverns in the cave is to apply a flood fill algorithm that can determine the area connected to a given cell in a grid.

    Have you ever used a flood fills in paint programs like Photoshop to fill areas with a different color? It works just as well on caves. :]

    First, add the following private property to the class extension in Cave.m:

    @property (strong, nonatomic) NSMutableArray *caverns;

    This allows easy access to information about the caverns later.

    To perform the flood fill, you need two new methods in Cave.m:

    • One that creates a copy of the current grid. From this, you’ll loop over every floor cell. For each one you’ll recursively test each floor cell in its von Neumann neighborhood.
    • Another you’ll call recursively to test cells in the von Neumann neighborhood of a cell. This will also change each tested cell’s type to a fill value.

    Tackle the recursive method first. Add this method to Cave.m:

    - (void)floodFillCavern:(NSMutableArray *)array fromCoordinate:(CGPoint)coordinate
        fillNumber:(NSInteger)fillNumber
    {
      // 1
      CaveCell *cell = (CaveCell *)array[(NSUInteger)coordinate.y][(NSUInteger)coordinate.x];
     
      // 2
      if (cell.type != CaveCellTypeFloor) {
        return;
      }
     
      // 3
      cell.type = fillNumber;
     
      // 4
      [[self.caverns lastObject] addObject:cell];
     
      // 5
      if (coordinate.x > 0) {
        [self floodFillCavern:array fromCoordinate:CGPointMake(coordinate.x - 1, coordinate.y)
          fillNumber:fillNumber];
      }
      if (coordinate.x < self.gridSize.width - 1) {
        [self floodFillCavern:array fromCoordinate:CGPointMake(coordinate.x + 1, coordinate.y)
          fillNumber:fillNumber];
      }
      if (coordinate.y > 0) {
        [self floodFillCavern:array fromCoordinate:CGPointMake(coordinate.x, coordinate.y - 1)
          fillNumber:fillNumber];
      }
      if (coordinate.y < self.gridSize.height - 1) {
        [self floodFillCavern:array fromCoordinate:CGPointMake(coordinate.x, coordinate.y + 1)
          fillNumber:fillNumber];
      }
    }

    The intent of this method is to be recursive. As you can see, it calls itself several times. Here’s a play-by-play of what it does:

    1. Use the grid coordinate passed to the method to get the CaveCell to test.
    2. If the cell you’re testing isn’t a floor cell, the method simply returns. When dealing with recursive methods, you always need some condition like this that will eventually stop the recursion, otherwise you’ll crash with an infinite loop.
    3. You change the cell’s type to the fillNumber that passed to the method. Each cavern will have a unique fill number, and this is how you’ll tell caverns apart later. It also ensures the cell is never tested again because it will no longer be considered a floor cell. This is the reason you’re performing the flood fill on a copy of the cave grid.
    4. The cell gets added to the last array in the list of caverns. The lastObject of the caverns array will be the current cavern where you’re performing the flood fill.
    5. Perform a recursive test for each cell in the von Neumann neighborhood. The test runs in the order west, east, north and south. Performance-wise, this is the smartest way since the flood fill goes from top to bottom.

    Still in Cave.m, add the second required method:

    - (void)identifyCaverns
    {
      // 1
      self.caverns = [NSMutableArray array];
     
      // 2
      NSMutableArray *floodFillArray = [NSMutableArray arrayWithCapacity:(NSUInteger)self.gridSize.height];
     
      for (NSUInteger y = 0; y < self.gridSize.height; y++) {
        NSMutableArray *floodFillArrayRow = [NSMutableArray arrayWithCapacity:(NSUInteger)self.gridSize.width];
     
        for (NSUInteger x = 0; x < self.gridSize.width; x++) {
          CaveCell *cellToCopy = (CaveCell *)self.grid[y][x];
          CaveCell *copiedCell = [[CaveCell alloc] initWithCoordinate:cellToCopy.coordinate];
          copiedCell.type = cellToCopy.type;
          [floodFillArrayRow addObject:copiedCell];
        }
     
        [floodFillArray addObject:floodFillArrayRow];
      }
     
      // 3
      NSInteger fillNumber = CaveCellTypeMax;
      for (NSUInteger y = 0; y < self.gridSize.height; y++) {
        for (NSUInteger x = 0; x < self.gridSize.width; x++) {
          if (((CaveCell *)floodFillArray[y][x]).type == CaveCellTypeFloor) {
            [self.caverns addObject:[NSMutableArray array]];
            [self floodFillCavern:floodFillArray fromCoordinate:CGPointMake(x, y) fillNumber:fillNumber];
            fillNumber++;
          }
        }
      }
     
      NSLog(@"Number of caverns in cave: %lu", (unsigned long)[self.caverns count]);
    }

    Now here’s a step-by-step :

    1. Create an instance of a new mutable array for the caverns in the cave. Each cavern will be a mutable array of CaveCells.
    2. Create a deep copy of the cave grid. Since the flood fill will change the type of the cell, you’ll need to work on a copy of the grid rather than the grid itself.
    3. Perform the flood fill by looping through each cell in the copy of the grid. If the cell is a floor tile, it means the cell has not been tested already, so a new cavern starts and the flood fill starts from the current cell.

    Now, add the following line to generateWithSeed:, right before the [self generateTiles]; line:

    [self identifyCaverns];

    Build and run. In the console, you should be able to see 22 tidy little caverns, for a cave generated with a seed of 0.

    Number of caverns

    You now know all the caverns in the cave and you’re left with a few choices with increasing difficulty:

    1. You can keep the cave as-is. If you work with destructible terrain, the player will be able to break, blast and bore through the walls to access the unconnected areas.
    2. You can remove all the unconnected caverns by filling all but the largest cavern with wall cells. The advantage is that the organic look of the cave remains while ensuring the knight can reach all parts of the cave. The downside is that you lose some playable area.
    3. You can connect the unconnected caverns to the largest of the caverns. The advantage is that you keep all the walkable areas of the original cave. The downside is that the connections between the caverns will be straight lines, which just doesn’t flatter organic look of a cave.

    In this tutorial, you’ll learn how to do options two and three. No matter which option you choose for the game, you’ll need to know which cavern is the largest, so you know where to put the entry and exit.

    All you have to do is count the number of CaveCell instances in each cavern array in the caverns. So, add the following method to Cave.m to do the counting:

    - (NSInteger)mainCavernIndex
    {
      NSInteger mainCavernIndex = -1;
      NSUInteger maxCavernSize = 0;
     
      for (NSUInteger i = 0; i < [self.caverns count]; i++) {
        NSArray *caveCells = (NSArray *)self.caverns[i];
        NSUInteger caveCellsCount = [caveCells count];
     
        if (caveCellsCount > maxCavernSize) {
          maxCavernSize = caveCellsCount;
          mainCavernIndex = i;
        }
      }
     
      return mainCavernIndex;
    }

    This method will simply loop through each array in caverns, checking which has the most instances of CaveCell. It returns an index that identifies the largest cavern, and henceforth shall be known as the ‘main cavern’.

    Removing Unconnected Caverns

    If you choose to remove all the disconnected caverns, then you need to fill them in with wall tiles.

    To do that, add this new method to Cave.m:

    - (void) removeDisconnectedCaverns
    {
      NSInteger mainCavernIndex = [self mainCavernIndex];
      NSUInteger cavernsCount = [self.caverns count];
     
      if (cavernsCount > 0) {
        for (NSUInteger i = 0; i < cavernsCount; i++) {
          if (i != mainCavernIndex) {
            NSArray *array = (NSArray *)self.caverns[i];
     
            for (CaveCell *cell in array) {
              ((CaveCell *)self.grid[(NSUInteger)cell.coordinate.y][(NSUInteger)cell.coordinate.x]).type =
                CaveCellTypeWall;
            }
          }
        }
      }
    }

    First, this method gets the index of the main cavern, then it uses a for loop to go through the caverns array. It turns all the CaveCell instances into walls within the caverns, except for those in the main cavern.

    To see your latest efforts in action, add the following line of code to generateWithSeed: just before the line that calls generateTiles:

    [self removeDisconnectedCaverns];

    Build and run now, and note that your cave no longer has any unconnected caverns.
    Removing unconnected caverns

    Tip: It might be difficult to actually see what has changed between runs. The following illustrations show exactly what happened.Before and after removing disconnected cavernsThese images were created by using spriteNodeWithColor:size: with a size of 2×2 points in place of using spriteNodeWithTexture: in generateTiles, and modifying sizeOfTiles accordingly.

    Where To Go From Here?

    Here is the finished example project from this tutorial series so far.

    Congratulations, you have generated your own random cave system using cellular automata! Your knight now has a mysterious cavern to explore.

    However, there’s more you can do! Stay tuned for the second and final part of the series, where you’ll learn how to connect unconnected caverns, place an exit and entrance into the cavern, add collision detection – and yes, there will be treasure.

    In the meantime, if you have any questions or comments, please join the forum discussion below!

    Procedural Level Generation in Games using a Cellular Automaton: Part 1 is a post from: Ray Wenderlich

    The post Procedural Level Generation in Games using a Cellular Automaton: Part 1 appeared first on Ray Wenderlich.


    Video Tutorial: Using LLDB in iOS Part 3: Backtraces, Threads, and Frames

    Procedural Level Generation in Games using a Cellular Automaton: Part 2

    $
    0
    0
    Learn how to add a procedural cave system into your game!

    Learn how to add a procedural cave system into your game!

    Welcome back to our 2-part tutorial series on procedural level generation in games using a cellular automaton. In other words, how to make cool caves! :]

    In the first part of the series, you learned how to use a cellular automaton similar to Conway’s Game of Life to transform random tiles into a cave system, and remove unconnected caves.

    In this second and final part of the series, you’ll learn how to connect unconnected caverns, place an exit and entrance into the cavern, add collision detection – and yes, there will be treasure.

    This tutorial will pick up where you left things off in the previous tutorial. If you don’t have it already, here is the example project where you left things off last time.

    Connecting Unconnected Caverns

    In the previous tutorial, you solved the problem of unconnected caverns by simply removing all unconnected caverns. But you have another option: connect the unconnected caverns to the main cavern.

    To do this, you will use A* path-finding to find a path from a random point in the unconnected cavern to a random point in the main cavern. This will also convert all cells in the path to floor cells.

    Note: If you’re new to A* path-finding, it might help you to skim through our A* path-finding tutorial to get familiar with the theory behind it.

    As a refresher, here’s a quick crash course of the algorithm.

    Movement costs

    During the algorithm, you will be calculating a movement cost F = G + H where:

    • G = The movement cost from the start space to this tile (i.e. how many tiles the hero needs to walk to get to that square).
    • H = The estimated cost to get from the tile to the target tile. Again, this is an estimate only; you can think of H as heuristic, like the “city block” estimate.

    Lists

    During the algorithm, you will need two lists:

    • Open list: Tiles currently under consideration
    • Closed list: Tiles already considered

    The algorithm

    And here is the algorithm itself:

    1. Get the tile on the open list, with the lowest F score. Call this S.
    2. Remove S from the open list, and add it to the closed list.
    3. For each square T in S‘s walkable adjacent tiles:
      1. If T is in the closed list: Ignore it.
      2. If T is not in the open list: Add it and compute its score.
      3. If T is already in the open list: Check if the F score is lower when we use the current generated path to get there. If it is, update its score and update its parent as well.
    4. When the target square is in the open list, add it to the closed list. Then you can walk backwards from the target square to the open list to get the shortest path.

    Demo

    Finally, here is an animated GIF demoing the algorithm:

    Key for lists (colors of tile borders): Open list green, Closed list red, final shortest path blue.

    Key for movement costs (numbers on each tile): G score lower left, H score lower right, and F score upper left.

    Cat-Maze

    Again, for a full walkthrough check out our A* path-finding tutorial.

    You’ll use a slightly modified version of the algorithm from our our A* path-finding tutorial to find the shortest, most inexpensive path between the two points. For the A* heuristic function, wall cells will earn a higher value than floor cells. Doing this will allow the A* path-finding to dig through walls, as a last resort.

    First, you’ll create an exact copy of the ShortestPathStep class from our A* path-finding tutorial.

    • Go to File\New\File…
    • Choose iOS\Cocoa Touch\Objective-C class, and click Next
    • Name the class ShortestPathStep
    • Make it a subclass of NSObject, click Next and then Create.

    Paste the following code into ShortestPathStep.h between @interface and @end:

    @property (assign, nonatomic) CGPoint position;
    @property (assign, nonatomic) NSInteger gScore;
    @property (assign, nonatomic) NSInteger hScore;
    @property (strong, nonatomic) ShortestPathStep *parent;
     
    - (instancetype)initWithPosition:(CGPoint)pos;
    - (NSInteger)fScore;

    Then paste the following code into ShortestPathStep.m between @implementation and @end:

    - (instancetype)initWithPosition:(CGPoint)pos
    {
      if ((self = [super init])) {
        _position = pos;
      }
      return self;
    }
     
    - (NSString *)description
    {
      return [NSString stringWithFormat:@"%@  pos=%@  g=%ld  h=%ld  f=%ld", [super description],
        NSStringFromCGPoint(self.position), (long)self.gScore, (long)self.hScore, (long)[self fScore]];
    }
     
    - (BOOL)isEqual:(ShortestPathStep *)other
    {
      return CGPointEqualToPoint(self.position, other.position);
    }
     
    - (NSInteger)fScore
    {
      return self.gScore + self.hScore;
    }

    There are no modifications to this class from the original A* path-finding tutorial.

    Import the ShortestPathStep header in Cave.m:

    #import "ShortestPathStep.h"

    To initiate the path-finding, you need a method to get a set of random coordinates from the main cavern and each of the unconnected caverns.

    Still in Cave.m, insert the following method:

    - (void)connectToMainCavern
    {
      NSUInteger mainCavernIndex = [self mainCavernIndex];
     
      NSArray *mainCavern = (NSArray *)self.caverns[mainCavernIndex];
     
      for (NSUInteger cavernIndex = 0; cavernIndex < [self.caverns count]; cavernIndex++) {
        if (cavernIndex != mainCavernIndex) {
          NSArray *originCavern = self.caverns[cavernIndex];
          CaveCell *originCell = (CaveCell *)originCavern[arc4random() % [originCavern count]];
          CaveCell *destinationCell = (CaveCell *)mainCavern[arc4random() % [mainCavern count]];
          [self createPathBetweenOrigin:originCell destination:destinationCell];
        }
      }
    }

    First, this method gets the main cavern index and array. Then it loops through the remaining caverns and selects a random cell inside both the smaller cavern and the main cavern. The originCell (from a disconnected cavern) and destinationCell (from the main cavern) are passed as parameters to createPathBetweenOrigin:destination:. You’ll be implementing this method soon, so do not worry about the compiler error for now.

    Before implementing createPathBetweenOrigin:destination:, you’ll need a few helper methods:

    1. A method to insert a ShortestPathStep into the open list at the appropriate position (ordered by F score).
    2. A method to compute the movement cost between adjacent cells..
    3. A method to compute the H score for a cell.

    Paste the following three methods in Cave.m:

    // Added inList parameter as this implementation does not use properties to store
    // open and closed lists.
    - (void)insertStep:(ShortestPathStep *)step inList:(NSMutableArray *)list
    {
      NSInteger stepFScore = [step fScore];
      NSInteger count = [list count];
      NSInteger i = 0;
     
      for (; i < count; i++) {
        if (stepFScore <= [[list objectAtIndex:i] fScore]) {
          break;
        }
      }
     
      [list insertObject:step atIndex:i];
    }
     
    - (NSInteger)costToMoveFromStep:(ShortestPathStep *)fromStep toAdjacentStep:(ShortestPathStep *)toStep
    {
      // Always returns one, as it is equally expensive to move either up, down, left or right.
      return 1;
    }
     
    - (NSInteger)computeHScoreFromCoordinate:(CGPoint)fromCoordinate toCoordinate:(CGPoint)toCoordinate
    {
      // Get the cell at the toCoordinate to calculate the hScore
      CaveCell *cell = [self caveCellFromGridCoordinate:toCoordinate];
     
      // It is 10 times more expensive to move through wall cells than floor cells.
      NSUInteger multiplier = cell.type = CaveCellTypeWall ? 10 : 1;
     
      return multiplier * (abs(toCoordinate.x - fromCoordinate.x) + abs(toCoordinate.y - fromCoordinate.y));
    }

    The inline comments in the above methods explain where these methods differ from the original Cocos2D tutorial in good detail, so be sure to read them through.

    Next, create a method to get all cell coordinates in the von Neumann neighborhood of a specific coordinate:

    - (NSArray *)adjacentCellsCoordinateForCellCoordinate:(CGPoint)cellCoordinate
    {
      NSMutableArray *tmp = [NSMutableArray arrayWithCapacity:4];
     
      // Top
      CGPoint p = CGPointMake(cellCoordinate.x, cellCoordinate.y - 1);
      if ([self isValidGridCoordinate:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
      }
     
      // Left
      p = CGPointMake(cellCoordinate.x - 1, cellCoordinate.y);
      if ([self isValidGridCoordinate:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
      }
     
      // Bottom
      p = CGPointMake(cellCoordinate.x, cellCoordinate.y + 1);
      if ([self isValidGridCoordinate:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
      }
     
      // Right
      p = CGPointMake(cellCoordinate.x + 1, cellCoordinate.y);
      if ([self isValidGridCoordinate:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
      }
     
      return [NSArray arrayWithArray:tmp];
    }

    This method gets all valid adjacent cell coordinates for the four possible cells in the von Neumann neighborhood and returns them as coordinates in an array. Unlike the original code, it returns all valid grid coordinates as it is possible to move over walls and floors alike.

    Now that all the helper methods are in place, add the following code to Cave.m:

    - (void)createPathBetweenOrigin:(CaveCell *)originCell destination:(CaveCell *)destinationCell
    {
      NSMutableArray *openSteps = [NSMutableArray array];
      NSMutableArray *closedSteps = [NSMutableArray array];
     
      [self insertStep:[[ShortestPathStep alloc] initWithPosition:originCell.coordinate] inList:openSteps];
     
      do {
        // Get the lowest F cost step.
        // Because the list is ordered, the first step is always the one with the lowest F cost.
        ShortestPathStep *currentStep = [openSteps firstObject];
     
        // Add the current step to the closed list
        [closedSteps addObject:currentStep];
     
        // Remove it from the open list
        [openSteps removeObjectAtIndex:0];
     
        // If the currentStep is the desired cell coordinate, we are done!
        if (CGPointEqualToPoint(currentStep.position, destinationCell.coordinate)) {
          // Turn the path into floors to connect the caverns
          do {
            if (currentStep.parent != nil) {
              CaveCell *cell = [self caveCellFromGridCoordinate:currentStep.position];
              cell.type = CaveCellTypeFloor;
            }
            currentStep = currentStep.parent; // Go backwards
          } while (currentStep != nil);
          break;
        }
     
        // Get the adjacent cell coordinates of the current step
        NSArray *adjSteps = [self adjacentCellsCoordinateForCellCoordinate:currentStep.position];
     
        for (NSValue *v in adjSteps) {
          ShortestPathStep *step = [[ShortestPathStep alloc] initWithPosition:[v CGPointValue]];
     
          // Check if the step isn't already in the closed set
          if ([closedSteps containsObject:step]) {
            continue; // ignore it
          }
     
          // Compute the cost form the current step to that step
          NSInteger moveCost = [self costToMoveFromStep:currentStep toAdjacentStep:step];
     
          // Check if the step is already in the open list
          NSUInteger index = [openSteps indexOfObject:step];
     
          if (index == NSNotFound) { // Not on the open list, so add it
     
            // Set the current step as the parent
            step.parent = currentStep;
     
            // The G score is equal to the parent G score plus the cost to move from the parent to it
            step.gScore = currentStep.gScore + moveCost;
     
            // Compute the H score, which is the estimated move cost to move from that step
            // to the desired cell coordinate
            step.hScore = [self computeHScoreFromCoordinate:step.position
              toCoordinate:destinationCell.coordinate];
     
            // Adding it with the function which is preserving the list ordered by F score
            [self insertStep:step inList:openSteps];
     
          } else { // Already in the open list
     
            // To retrieve the old one, which has its scores already computed
            step = [openSteps objectAtIndex:index];
     
            // Check to see if the G score for that step is lower if we use the current step to get there
            if ((currentStep.gScore + moveCost) < step.gScore) {
     
              // The G score is equal to the parent G score plus the cost to move the parent to it
              step.gScore = currentStep.gScore + moveCost;
     
              // Because the G score has changed, the F score may have changed too.
              // So to keep the open list ordered we have to remove the step, and re-insert it with
              // the insert function, which is preserving the list ordered by F score.
              ShortestPathStep *preservedStep = [[ShortestPathStep alloc] initWithPosition:step.position];
     
              // Remove the step from the open list
              [openSteps removeObjectAtIndex:index];
     
              // Re-insert the step to the open list
              [self insertStep:preservedStep inList:openSteps];
            }
          }
        }
     
      } while ([openSteps count] > 0);
    }
     
    - (void)constructPathFromStep:(ShortestPathStep *)step
    {
      do {
        if (step.parent != nil) {
          CaveCell *cell = [self caveCellFromGridCoordinate:step.position];
          cell.type = CaveCellTypeFloor;
        }
        step = step.parent; // Go backwards
      } while (step != nil);
    }

    This is the meat and bones of the A* path-finding algorithm. The implementation of this method is very much like how it is in the original tutorial, except for a few adaptations. Read the comments or go through the original tutorial to understand how it works.

    Have you read and understood the code? Good, time to put all this path-finding to good use. :]

    Now you’ve taken time to create two ways to construct one big connected cave. Why not make the Cave class customizable so you can choose one or the other?

    Start by adding a new public property to Cave.h:

    @property (assign, nonatomic) BOOL connectedCave;

    If this property is set to YES, then the app will use A* path-finding. Otherwise, it removes the disconnected caves. By default, a cave with all disconnected caves removed will generate, as this property will have the default value NO.

    Inside Cave.m, locate the following line in generateWithSeed::

    [self removeDisconnectedCaverns];

    and replace it with this code:

    if (self.connectedCave) {
      [self connectToMainCavern];
    } else {
      [self removeDisconnectedCaverns];
    }

    This code checks the value of the connectedCave property to determine if disconnected caverns should be removed or connected.

    If you run the code now, it will remove all disconnected caverns. Since you just set it up to connect all parts of the cave, you probably want to see it in action, right?

    Open MyScene.m and add the following line of code to initWithSize: just before the line that calls generateWithSeed: on _cave:

    _cave.connectedCave = YES;

    Build and run. Now you should see connections between all caverns, as shown in these before and after images:

    Before and after connecting disconnected caverns

    Now you can see that A* path-finding adds a lot of straight-lined corridors. You need to decide if you can live with awkward precision or if you'd prefer to remove the disconnected caverns.

    Tip: You can make the A* path-finding less destructive to the cave by selecting a cell from the main cavern that is closer to the disconnected cave. As it stands now, the method chooses a random cell in the main cavern, and it might be very far away from the cave to be connected.

    Placing an Entrance and an Exit

    Now you have a knight and a cave. Spelunking might be a nice pastime for modern adventurer, but a meaningless activity for your knight.

    If you were to drop somebody in the middle of a cave, their immediate goal is to find a way out. In the next phase, you'll start by adding an entry to drop the knight in the cave and an exit for him to find.

    Placing the starting point is straightforward. You'll find a random floor cell within the cave and make that cell an entrance cell.

    The exit is a bit trickier as you don't want it to be too close to the entrance. The strategy you'll use is to find another random cell, then calculate the distance between it and the entrance.

    If the distance is larger than or equal to your desired distance, then the randomly selected cell becomes an exit cell. Otherwise, the method selects another random cell and repeats the process.

    Start by adding three new public properties to Cave.h:

    @property (assign, nonatomic, readonly) CGPoint entrance;
    @property (assign, nonatomic, readonly) CGPoint exit;
    @property (assign, nonatomic) CGFloat minDistanceBetweenEntryAndExit;

    The entrance and exit properties give you the coordinates of the entrance and exit in the level, respectively. They are pixel coordinates for the center of these two cells. Later, it will make it easier for you to position the player sprite node.

    The minDistanceBetweenEntryAndExit property can be set to the minimum allowed distance between the entry and exit. This distance will be calculated using the Pythagorean theorem. The lower the value, the closer the exit can be to the entrance.

    You'll want to default the property values for these new properties. Open Cave.m and add the following lines of code to initWithAtlasNamed:gridSize: after the line that initializes _numberOfTransitionSteps:

    _entrance = CGPointZero;
    _exit = CGPointZero;
    _minDistanceBetweenEntryAndExit = 32.0f;

    The entrance and exit properties default to CGPointZero since these will be set later during dungeon creation, and minDistanceBetweenEntryAndExit defaults to 32.0f, as this gives a nice distance between the entrance and exit.

    The value for minDistanceBetweenEntryAndExit needs to be set before you create a cave, as the number is relative to the cave's grid size.

    You also need to be able to set a cell's type as an entrance or an exit. To do that, add the following types to the CaveCellType enumeration in CaveCell.h, between CaveCellTypeFloor and CaveCellTypeMax:

    CaveCellTypeEntry,
    CaveCellTypeExit,

    Next, add the following method to Cave.m:

    - (void)placeEntranceAndExit
    {
      // 1
      NSUInteger mainCavernIndex = [self mainCavernIndex];
      NSArray *mainCavern = (NSArray *)self.caverns[mainCavernIndex];
     
      // 2
      NSUInteger mainCavernCount = [mainCavern count];
      CaveCell *entranceCell = (CaveCell *)mainCavern[arc4random() % mainCavernCount];
     
      // 3
      [self caveCellFromGridCoordinate:entranceCell.coordinate].type = CaveCellTypeEntry;
      _entrance = [self positionForGridCoordinate:entranceCell.coordinate];
     
      CaveCell *exitCell = nil;
      CGFloat distance = 0.0f;
     
      do
      {
        // 4
        exitCell = (CaveCell *)mainCavern[arc4random() % mainCavernCount];
     
        // 5
        NSInteger a = (exitCell.coordinate.x - entranceCell.coordinate.x);
        NSInteger b = (exitCell.coordinate.y - entranceCell.coordinate.y);
        distance = sqrtf(a * a + b * b);
     
        NSLog(@"Distance: %f", distance);
      }
      while (distance < self.minDistanceBetweenEntryAndExit);
     
      // 6
      [self caveCellFromGridCoordinate:exitCell.coordinate].type = CaveCellTypeExit;
      _exit = [self positionForGridCoordinate:exitCell.coordinate];
    }

    There is a lot going on in this method, so let's go through it step-by-step:

    1. First, you select the main cavern array, as this is where you place the entrance and exit.
    2. Select a random cell within the main cavern. Remember that caverns only contain floor cells so there is no risk of placing the entrance atop a wall cell.
    3. Convert the random cell to an entrance cell and set the entrance property coordinates for that cell.
    4. Next, select another random cell in the main cavern for the exit. It might end up being the same cell you just chose, but the following check will take care of that.
    5. Based on the grid coordinates of entranceCell and exitCell, the distance between these two coordinates is calculated using the Pythagorean theorem. If the distance is less than the value of minDistanceBetweenEntryAndExit, then it loops back to select a new random cell.
    6. Convert exitCell into an exit cell and set the exit property coordinates for the exit cell.

    If you're a bit rusty on basic trigonometry, the following image describes how to calculate the distance between two points using the Pythagorean theorem:

    Calculate distance between Entrance and Exit

    The distance between two points (the hypotenuse) is the square root of adding the horizontal distance (a) squared and the vertical distance (b) squared.

    To learn more about the Pythagorean theorem, work through the Trigonometry for Game Programming tutorial on this site.

    Note: You can speed up these sorts of checks by removing the call to sqrt. To do that you just need to store minDistanceBetweenEntryAndExit as the squared distance rather than the actual distance.

    If you really need to know the true distance between the entrance and the exit, you couldn't do this trick, but in cases like this where you're simply comparing against a known distance, it works just fine.

    Place the entrance and exit at the time you generate the cave for best results. Inside Cave.m, add the following code to generateWithSeed: just before the line that calls generateTiles:

    [self identifyCaverns];
    [self placeEntranceAndExit];

    This simply re-runs the flood fill algorithm and places the entrance and exit. Here's a mini-challenge: Why run the flood fill algorithm a second time?

    Solution Inside: Solution SelectShow>

    Good job! Your cave now has an entrance and an exit. Now you need to make them visible to the player. This requires an update to generateTiles in Cave.m. Add the following two cases to the switch statement:

    case CaveCellTypeEntry:
      node = [SKSpriteNode spriteNodeWithTexture:[self.atlas textureNamed:@"tile4_0"]];
      break;
     
    case CaveCellTypeExit:
      node = [SKSpriteNode spriteNodeWithTexture:[self.atlas textureNamed:@"tile3_0"]];
      break;

    These cases simply create sprite nodes with either an entrance texture (stairs up) or an exit texture (stairs down), depending on what's appropriate for the cell type.

    Now you need to make sure the knight starts at the entrance of the cave, so open MyScene.m and change the line that initializes self.player.desiredPosition in initWithSize: so it looks like this:

    _player.desiredPosition = _cave.entrance;

    Build and run, and you'll see the knight standing at the entrance, ready to find the exit. Spend a short while walking around the cave to see if you can find the exit.

    Knight makes an entrance

    Did you find the exit yet?

    Tip: You can determine the direction to head to find the exit by inserting the following code at the end of placeEntranceAndExit in Cave.m -- just remember that origin (x = 0, y = 0) is at bottom-left:
    NSLog(@"Entrance is at: %@", NSStringFromCGPoint(entranceCell.coordinate));
    NSLog(@"Exit is at: %@", NSStringFromCGPoint(exitCell.coordinate));

    Treasure

    No knight is going to be thrilled about trudging through an unfamiliar cave without potential for some kind of reward. Treasure will help keep him motivated and coming back for more.

    Placing treasure can be as easy as randomly selecting a floor cell and placing a gold-filled chest there. But that might make it far too easy for the knight. You'll apply a different approach that will put treasure in less obvious places.

    You've already created a method to count how many walls surround a cell. By looping over all the cells in the finished cave system, you can determine how many walls surround each cell. Cells surrounded by several walls are often at the end of a corridor or in an isolated area; the perfect place to hide treasure.

    First, you'll add a new cell type to allow you to mark cells that contain treasure easily. Open CaveCell.h and add the following type to the CaveCellType enumeration, just above CaveCellTypeMax:

    CaveCellTypeTreasure,

    You'll use this new type for any cell that contains treasure.

    Add the following method to Cave.m:

    - (void)placeTreasure
    {
      NSUInteger treasureHiddenLimit = 4;
     
      for (NSUInteger y = 0; y < self.gridSize.height; y++) {
        for (NSUInteger x = 0; x < self.gridSize.width; x++) {
          CaveCell *cell = (CaveCell *)self.grid[y][x];
     
          if (cell.type == CaveCellTypeFloor) {
            NSUInteger mooreNeighborWallCount =
              [self countWallMooreNeighborsFromGridCoordinate:CGPointMake(x, y)];
     
            if (mooreNeighborWallCount > treasureHiddenLimit) {
              // Place treasure here
              cell.type = CaveCellTypeTreasure;
            }
          }
        }
      }
    }

    This method simply loops over all cells in the cave, and for each floor cell it checks if the number of walls in the Moore neighborhood is greater than treasureHiddenLimit. If so, the cell becomes a treasure cell.

    You still need to make the treasure visible, so add the following case to the switch statement in generateTiles in Cave.m:

    case CaveCellTypeTreasure:
    {
      node = [SKSpriteNode spriteNodeWithTexture:[self.atlas textureNamed:@"tile0_0"]];
     
      SKSpriteNode *treasure = [SKSpriteNode spriteNodeWithTexture:[self.atlas textureNamed:@"treasure"]];
      treasure.name = @"TREASURE";
      treasure.position = CGPointMake(0.0f, 0.0f);
      [node addChild:treasure];
     
      break;
    }

    If the cell type is equal to CaveCellTypeTreasure, then it creates a tile for the floor. Then it adds another SKSpriteNode, and assigns a treasure texture as a child to the floor tile. By adding the chest as a child, you can easily remove it when the knight collects the goods.

    Placing the treasure requires a single line of code to be added to generateWithSeed: in Cave.m. Add the following code just after the line that calls placeEntranceAndExit:

    [self placeTreasure];

    Build and run and send your knight on a treasure hunt.

    Looking for treasure

    You now have a fully functional cave generator that places entrance, exit and treasure procedurally. What more could you want? Well, maybe you should do something about the knight's superhuman ability to walk through walls.

    Collision Detection

    In the previous tutorial on procedural level generation, the internal physics engine was used to resolve collisions. In this tutorial, you'll take a different approach and implement your own custom collision detection and handling.

    There are many varieties of collision detection, and the math behind the methods can be pretty complicated. Lucky for you, this tutorial uses basic rectangular shapes for the cave and knight, so simple bounding box collision detection will suffice.

    In order to detect collisions between the knight and the cave, you need to be able to tell which cell(s) the knight currently occupies. In this case, the knight sprite is a bit smaller than a cell, so you know the knight will only ever occupy one, two or four cells.

    Cell occupancy

    Once you know which cell(s) the knight occupies, you can tell which are wall cells. For each wall cell, you'll resolve the collision based on the intersection between the knight and the wall.

    To do this, calculate the intersection between the rectangle for the knight and the rectangle for the cell.

    The following illustration shows how to resolve the collisions:

    How to resolve collisions

    1. The intersection between the knight and wall is higher than it is wide. Hence, the collision will resolve horizontally, which allows the knight to slide along the wall vertically.
    2. The intersection between the knight and the wall is wider than it is tall. The collision will resolve vertically, which allows the knight to slide along the wall horizontally.
    3. In case the intersection between the knight and wall is as high as it is wide, the collision will be resolved both horizontally and vertically.

    Once you've determined how to resolve the collision, you'll use the intersecting rectangle to move the knight out of the collision.

    For instance, if you determine you need to resolve horizontally, you move the knight horizontally in the opposite direction he is moving by an amount that corresponds to the width of the intersecting rectangle.

    Since you might need to resolve several wall collisions at once, the best practice is not to change the position of the knight directly. That would cause the knight to stutter while resolving the collisions.

    Instead, you'll change the desired position of the knight. Luckily, the Player class already includes such a property. :]

    Based on the explanation above, you'll need to add a few helper methods:

    1. A method to get an array of cells in the cave the knight currently occupies.
    2. Another method to resolve collisions between the knight and any wall cells. This method returns a vector to repel the knight in the correct direction based on the intersecting rectangle.

    You'll add the collision-handling code to MyScene. This is the most logical place, as this is where the game logic lives.

    Start by adding a forward declaration of CaveCell in Cave.h before @interface:

    @class CaveCell;

    Next, implement the two helper methods described above. You'll start with the method to return an array of cell(s) the knight is currently occupying.

    But before you can do that, you need to add two new methods to Cave:

    • One that will return the grid coordinate for a position (in points)
    • Another that returns the rectangle for a cell given a grid coordinate in the cave.

    Add these methods to Cave.m:

    - (CGPoint)gridCoordinateForPosition:(CGPoint)position
    {
      return CGPointMake((position.x / self.tileSize.width), (position.y / self.tileSize.height));
    }
     
    - (CGRect)caveCellRectFromGridCoordinate:(CGPoint)coordinate
    {
      if ([self isValidGridCoordinate:coordinate]) {
        CGPoint cellPosition = [self positionForGridCoordinate:coordinate];
     
        return CGRectMake(cellPosition.x - (self.tileSize.width / 2),
          cellPosition.y - (self.tileSize.height / 2),
          self.tileSize.width,
          self.tileSize.height);
      }
      return CGRectZero;
    }

    The first method simply calculates which grid coordinates correspond to the position by dividing the position coordinates with the width and height of a tile.

    The other method returns the rectangle for a cell at the coordinate passed as a parameter.

    Now you need to create a few public methods in the Cave class. Open Cave.h and add the following method declarations to the interface:

    - (CaveCell *)caveCellFromGridCoordinate:(CGPoint)coordinate;
    - (CGPoint)gridCoordinateForPosition:(CGPoint)position;
    - (CGRect)caveCellRectFromGridCoordinate:(CGPoint)coordinate;

    Next, open MyScene.m and add the following #import:

    #import "CaveCell.h"

    This allows MyScene to access information about a CaveCell object instance.

    Still in MyScene.m add the following method:

    - (NSArray *)getCaveCellsFromRect:(CGRect)rect
    {
      NSMutableArray *array = [NSMutableArray array];
     
      CaveCell *topLeft = [self.cave caveCellFromGridCoordinate:
        [self.cave gridCoordinateForPosition:rect.origin]];
     
      CaveCell *topRight = [self.cave caveCellFromGridCoordinate:
        [self.cave gridCoordinateForPosition:CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect))]];
     
      CaveCell *bottomLeft = [self.cave caveCellFromGridCoordinate:
        [self.cave gridCoordinateForPosition:CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect))]];
     
      CaveCell *bottomRight = [self.cave caveCellFromGridCoordinate:
        [self.cave gridCoordinateForPosition:CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect))]];
     
      if (topLeft && topLeft.type == CaveCellTypeWall) {
        [array addObject:topLeft];
      }
      if (topRight && topRight.type == CaveCellTypeWall && ![array containsObject:topRight]) {
        [array addObject:topRight];
      }
      if (bottomLeft && bottomLeft.type == CaveCellTypeWall && ![array containsObject:bottomLeft]) {
        [array addObject:bottomLeft];
      }
      if (bottomRight && bottomRight.type == CaveCellTypeWall && ![array containsObject:bottomRight]) {
        [array addObject:bottomRight];
      }
     
      return array;
    }

    This method accepts a CGRect and finds any cells that intersect it. First it finds the CaveCell at each corner of rect and stores them in topLeft, topRight, bottomLeft, and bottomRight.

    For each of these cells, you check if it's a wall and ensure it hasn't already been added to array. That's because the four cells could potentially all be the same cell if the knight fits exactly within the rectangle of a single cell.

    Now that you know the unique wall cell(s) the knight currently occupies, add the second method, which will allow you to resolve all the collisions. Add the following to MyScene.m:

    - (CGPoint)intersectionRepelDistanceBetweenRect:(CGRect)playerRect andRect:(CGRect)cellRect
    {
      if (CGRectIntersectsRect(playerRect, cellRect)) {
        // 1
        NSInteger signX = CGRectGetMaxX(playerRect) > CGRectGetMaxX(cellRect) ? 1 : -1;
        NSInteger signY = CGRectGetMaxY(playerRect) > CGRectGetMaxY(cellRect) ? 1 : -1;
     
        // 2
        CGRect intersectionRect = CGRectIntersection(playerRect, cellRect);
     
        // 3
        if (CGRectGetWidth(intersectionRect) < CGRectGetHeight(intersectionRect)) {
          // If the width is less than the height, resolve the collision horizontally
          return CGPointMake(CGRectGetWidth(intersectionRect) * signX, 0.0f);
        } else if (CGRectGetWidth(intersectionRect) > CGRectGetHeight(intersectionRect)) {
          // If the width is greater than the height, resolve the collision vertically
          return CGPointMake(0.0f, CGRectGetHeight(intersectionRect) * signY);
        } else {
          // If the width and height of the intersection are equal, then resolve collision
          // both horizontally and vertically
          return CGPointMake(CGRectGetWidth(intersectionRect) * signX,
                             CGRectGetHeight(intersectionRect) * signY);
        }
      }
      // 4
      return CGPointZero;
    }

    We'll cover this line-by-line:

    1. You compare the right and bottom corners of the two rectangles to determine the direction of the intersection. This way you'll know what direction the knight was going when the player collided with the wall cell, so that you can reverse the movement.
    2. The handy built-in function CGRectIntersection calculates the intersection between the two rectangles.
    3. Based on the intersection rectangle, you return the appropriate direction to resolve the collision. Remember the images describing this earlier?
    4. If the knight and cell did not intersect, then return CGPointZero.

    You'll now put these two methods to good use. Add the following code to update: in MyScene.m, just after the comment // Insert code to detect collision between player and walls here:

    NSArray *cells = [self getCaveCellsFromRect:self.player.boundingRect];
     
    for (CaveCell *cell in cells) {
      CGPoint repel = [self intersectionRepelDistanceBetweenRect:self.player.boundingRect
        andRect:[self.cave caveCellRectFromGridCoordinate:cell.coordinate]];
     
      self.player.desiredPosition = CGPointMake(self.player.desiredPosition.x + repel.x,
        self.player.desiredPosition.y + repel.y);
    }

    Build and run. Voila! The knight no longer has free reign of the cave and superhuman abilities...or does he? Seems he's found his way to the hinterlands.

    There are still some places he can squeeze through because there isn't yet a border of walls surrounding the cave. This is easy to fix.

    That did not just happen

    Open Cave.m and add the following method:

    - (BOOL)isEdgeAtGridCoordinate:(CGPoint)coordinate
    {
      return ((NSUInteger)coordinate.x == 0 ||
        (NSUInteger)coordinate.x == (NSUInteger)self.gridSize.width - 1 ||
        (NSUInteger)coordinate.y == 0 ||
        (NSUInteger)coordinate.y == (NSUInteger)self.gridSize.height - 1);
    }

    This method returns YES if a given grid coordinate is at the edge of the grid and NO if the grid coordinate isn't at the edge of the grid.

    Still inside Cave.m, replace the line that sets cell.type in initializeGrid with the following code:

    if ([self isEdgeAtGridCoordinate:coordinate]) {
      cell.type = CaveCellTypeWall;
    } else {
      cell.type = [self randomNumberBetween0and1] < self.chanceToBecomeWall ? CaveCellTypeWall :
        CaveCellTypeFloor;
    }

    This will ensure all edge cells become walls, but leave it to chance if all other cells should become walls or floors.

    Do another build and run. Haha!! The knight is a captive of your cave, well, except for the exit.

    A trapped knight

    Handling treasure chests and exit

    The collision handling between the knight and cave is in place, but you still need to create the code for handling the collisions between the knight and the treasure chests, as well as define what happens when the knight reaches the exit.

    You'll handle this collisions in a similar way to how you handled collisions with the walls. The only major difference is the exit and treasure chests should not block the knight from walking through them.

    Your strategy is to determine the cell type underneath the center point of the knight. If the cell type is either a CaveCellTypeExit or CaveCellTypeTreasure, then you'll handle the collisions by taking an appropriate action.

    Add the following code to the update: method in MyScene.m just after the comment // Insert code to detect if player reached exit or found treasure here:

    CaveCell *cell = [self.cave caveCellFromGridCoordinate:
      [self.cave gridCoordinateForPosition:self.player.position]];
     
    switch (cell.type) {
      case CaveCellTypeExit:
        NSLog(@"Found the exit");
        break;
     
      case CaveCellTypeTreasure:
        NSLog(@"Found treasure");
        break;
     
      default:
        break;
    }

    First, it determines the type of cell underneath the player. Depending on the cell type it writes a different log statement to the console.

    Build and run the project again, and move the knight over the exit and/or treasure cells to see that these collisions are detected.

    Now that you know the collision logic works as expected, add the code to handle the collisions. First, add this method to MyScene.m to resolve the collision with the exit:

    - (void)resolveExit
    {
      // Disable the joystick to ensure the player cannot move around after reaching the exit
      self.isExitingLevel = YES;
     
      // Create actions to play the sound file for reaching the exit and add a block to transition
      // to the next cave
      SKAction *soundAction = [SKAction playSoundFileNamed:@"fanfare.mp3" waitForCompletion:NO];
      SKAction *blockAction = [SKAction runBlock:^{
        [self.view presentScene:[[MyScene alloc] initWithSize:self.size] transition:[SKTransition
          doorsCloseVerticalWithDuration:0.5f]];
      }];
      SKAction *exitAnimAction = [SKAction sequence:@[[SKAction group:@[soundAction]], blockAction]];
     
      // Run the action sequence
      [self.player runAction:exitAnimAction];
    }

    This is basic Sprite Kit code that plays a sound and presents a new scene when the knight emerges from the cave. The first line is important: it sets isExitingLevel to YES, which update: checks to disable the knight's movement.

    Go back to update: and replace the line NSLog(@"Found the exit"); with the following:

    [self resolveExit];

    Do a build and run, and lead your knight to the exit. Once you pass through it, you'll hear a victorious fanfare and a new random cave will generate.

    Epic Loot!

    The only thing left to do is to add the code to resolve collisions with treasure chests. Add this final method to MyScene.m:

    - (void)resolveTreasureInCell:(CaveCell *)cell
    {
      // Make this cell into a floor
      cell.type = CaveCellTypeFloor;
     
      // Calculate the position of the cell within the cave
      CGPoint cellPosition = CGPointMake(
        cell.coordinate.x * self.cave.tileSize.width + self.cave.tileSize.width / 2,
        (cell.coordinate.y * self.cave.tileSize.height + self.cave.tileSize.height / 2));
     
      // Get the node at the point of this cell
      SKNode *node = [self.cave nodeAtPoint:cellPosition];
     
      if (node) {
        // Get the treasure child node
        if ([node.name isEqualToString:@"TREASURE"]) {
          node = node.parent;
        }
     
        // Remove the treasure child sprite node
        [node removeAllChildren];
     
        // Play a sound effect for picking up the treasure
        [node runAction:[SKAction playSoundFileNamed:@"treasure.wav" waitForCompletion:NO]];
      }
    }

    First, the cell converts to a floor cell, which prevents future collisions with this cell. Then it calculates the position of the cell within the cave, which gets the treasure chest sprite node from the cell. The treasure sprite is removed and the game plays a sound file to signal the knight found the treasure.

    Finally, change update: in MyScene.m by replacing the line NSLog(@"Found treasure"); with the following:

    [self resolveTreasureInCell:cell];

    Build and run. Take the knight on an adventure in the cave and collect as much treasure you can before emerging into the next level of the cave.

    Where to Go From Here?

    Congratulations! Now that you're at the end of this tutorial, you should have a good understanding of how to implement cellular automaton to create cave-like levels for games.

    You also know a thing or two about how to overcome some of the challenges of using a cellular automaton. If you want to see a copy of the game in its entirety, download the final project. It has all the code from the above tutorial.

    To take your learnings a step further, experiment with your cave generation code and find ways to improve it.

    For instance, you can combine the A* path-finding with removing very small disconnected caverns. This, combined with finding a cell in the main cavern nearby the disconnected cavern, will make the path-finding a lot less destructive. If you want to preserve more of the original structure of the cavern, this is will help.

    Also, try to make the tiles in the cave a bit more random, and you can start with some of the extra textures included in the texture atlas. Our book iOS Games by Tutorials has some great examples on how to do this, so be sure to pick up a copy to catapult your skills to the next level.

    If you have any comments or suggestions related to this tutorial, please join the discussion below.

    Procedural Level Generation in Games using a Cellular Automaton: Part 2 is a post from: Ray Wenderlich

    The post Procedural Level Generation in Games using a Cellular Automaton: Part 2 appeared first on Ray Wenderlich.

    Video Tutorial: Using LLDB in iOS Part 4: Watchpoint, Script, Command

    Building an iOS App like Siri

    $
    0
    0
    Learn how to build an iOS app like Siri

    Learn how to build an iOS app like Siri

    Siri is a voice-based personal assistant that takes voice commands and performs tasks like sending messages, making phone calls, setting alarms, searching on the web, finding directions and more.

    Unfortunately at the time of writing this tutorial there isn’t an official API by Apple, but there are few third party frameworks that allow developers to include functionality similar to Siri.

    In this tutorial, you’ll learn how to use the Nuance Dragon Mobile SDK (SpeechKit), which is one of the leading frameworks in this arena. Specifically, you’ll learn:

    • The key concepts of SpeechKit framework
    • The advantages of SpeechKit when compared to other frameworks
    • How to perform speech recognition and text-to-speech synthesis using SpeechKit APIs.

    You’ll use this knowledge to build a Siri-like application to help your users find nearby restaurants, narrowed down by cuisine or category. Here’s a demo video of the app you will be building:

    What is SpeechKit?

    The SpeechKit framework is a high-level framework with two major components for developers: the speech recognizer and the text-to-speech synthesizer.

    The framework carries out the following processes:

    • The audio component manages the audio system for recording and playback to give user feedback.
    • The networking component manages the connection to the server and automatically re-establishes timed-out connections.
    • The end-of-speech detector determines when the user stops speaking and automatically stops recording.
    • The encoding component manages the streaming audio’s compression to reduce bandwidth requirements and decrease latency.

    SpeechKit follows a server-based architecture and relies on the Nuance speech server for voice recognition and text-to-speech synthesis.

    • For voice recognition, the SKRecognizer class sends audio streams to the server which then returns a list of text results.
    • To convert text to speech, the SKVocalizer class sends the text to a server and receives an audio playback.

    SpeechKit supports 38 languages for speech recognition and 40 languages for the text-to-speech synthesis. Both male and female voices are available for many languages.

    SpeechKit Framework Quick Reference

    The SpeechKit iOS framework has the following four classes and three protocols that it uses for speech recognition and text-to-speech synthesis.

    Class References

    • SKRecognizer: This is the primary class for voice recognition. It records the user’s voice, transmits it to Nuance Speech server and obtains lists of matching texts. Only one recognition process happens at any point of time. Generate a new instance of this class for any subsequent recognition.
    • SKRecognition: This object contains voice recognition results, scores corresponding to each recognition result and suggestions for the user. Suggestions can also ask the user to speak slowly or loudly depending on the environment in which user is speaking.
    • SKVocalizer: This class is for text-to-speech synthesis, and it ensures an application can be initialized to speak a language or voice. It supports 40 different languages and speaks in both male and female voices.
    • SpeechKit: This class configures the SpeechKit subsystems, maintains a connection with speech server, and initializes the required audio components that record a user’s voice or playback text-to-speech. This class doesn’t provide any instance, and hence, shouldn’t be initialized.

    Protocol References

    • SKRecognizerDelegate: These delegate methods maintain the flow of the recognition process. They detect when the recognition process begins and finishes, as well as when the app receives results or an error from the server.
    • SKVocalizerDelegate: These methods provide information regarding the speech process. Essentially, they are optional as their primary use is to detect error. The vocalizer queues sequential speech requests, hence, it is not necessary to know when the speech request has finished.
    • SpeechKitDelegate: This protocol allows the delegate object to observe the state of the SpeechKit process. Primarily it’s used to monitor if the process is being destroyed.

    Getting Started

    Start by downloading the starter project for this tutorial.

    Open Main.storyboard and you will see that I have pre-created the user interface for you so you can stay focused on SpeechKit:

    PrecreatedUI

    The view of ViewController has a table view as a subview where you’ll display search results. Each TableView cell has two imageViews for thumbnail and rating images, and two labels where the app will display the name and address of a restaurant.

    Build and run the application, and you should see a view with a textField and a mic button. The tableView underneath will not yet have any contents. That’s your job!

    Starter App Screenshot

    The app you’re building will help make your users’ lives easier by making it simple for them to find somewhere to eat and drink. No longer will they have to rely on old-fashioned typed out searches or local knowledge, now they’ll just be able to speak what they want into their iPhone and your app will save the day.

    Your app will suggest nearby restaurants based on the type of cuisine the user craves, or by a category the user defines. Sounds simple enough, but your app will need to perform five primary tasks:

    • Get the user’s current location
    • Take a user’s voice input to search restaurants by cuisine or category
    • Use the Yelp API to look for restaurants that match
    • Display results
    • Provide voice feedback to the user

    Aside from the user interface, the starter project also includes the code to obtain the user’s current location and connect to Yelp’s API. Cool!

    Now you need to get your own Yelp API authentication token and secret key, as well as a Nuance application key.

    Getting a Yelp API Token

    Go to the Yelp Developer portal and click on Manage API Access.

    Yelp API Token

    Log In using your Yelp developer account and password. If you haven’t registered already, go ahead and Sign Up for a new account.

    Yelp API Token

    Fill in the details and continue.

    Yelp API TokenYelp API Token

    Check your inbox for an email from Yelp. Click on the verification link to verify your email.

    Yelp API Token

    Once you’ve signed up, Log In to your Yelp developer account and click the Manage API Access link again.

    Fill up the details on API Access screen and submit for an API token.
    Yelp API Token

    By default, you’ll have an API v1.0 token, but you’ll need an API v2.0 in this project. Click on Request API v2.0 Key to generate API v2.0 token.
    Yelp API Token

    Take note of the consumer Key, consumer secret, token and token secret on this screen.

    Yelp API Token Secret

    If you already have a Yelp developer account, then log in and just click on Manage API Access Keys to find your API keys.

    Yelp API Token Secret

    Back in Xcode, open OAuthAPIConstants.h which is under Yelp_API_Request group in the sample project and update the API consumer key, consumer secret, token and token secret.

    Getting a Nuance Development Key

    Next, go to the Nuance Mobile Developer portal, and click on Register to create a new account.

    Nuance Token

    Fill out the details and continue.

    Nuance Token

    Provide a brief summary of the application you’re going to build, and remember to select iOS as the platform. You can choose multiple platforms if you have big plans to for cross-platform development.

    Nuance Token

    Feel free to leave the bottom section on this screen empty.

    Nuance Token

    Click on Get Started to create your account. You’ll receive an email at this point. Follow the steps to activate your account and login.

    Nuance Token

    Once you’ve successfully logged in, you can download the iOS Mobile SDK.

    Nuance Token

    You’ll receive another email with your SpeechKit Application key and setup details. Be sure to take a note of the Host, Port, AppID and App Key.

    Nuance Token

    There’s one last step before you can start coding – you need to add the SpeechKit framework to your project.

    Adding the SpeechKit Framework

    Start by extracting the DragonMobileSDK zip file that you downloaded when you registered with Nuance. The extracted directory will have the SpeechKit framework named as SpeechKit.framework, along with some sample projects.

    Next, select the YelpNearby project file in the Project Navigator and then select Build Phases. Expand the Link Binary with Libraries section and click the add (+) button.

    Adding_SpeechKit_Framework

    In the pop-up window, click on Add Other… button, and then locate SpeechKit.framework in the directory that you extracted in step-1. Click Open to add the framework to the sample project.

    SpeechKit Framework

    Allright – you’re finally done with the initial setup, time to code! :]

    Speech Recognition with SKRecognizer

    Open ViewController.h in Class group, then import SpeechKit.h framework header:

    #import <SpeechKit/SpeechKit.h>

    Also declare that the ViewController class is going to use SpeechKitDelegate and SKRecognizerDelegate:

    @interface ViewController : UIViewController <UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource, SpeechKitDelegate, SKRecognizerDelegate>

    Finally, declare an SKRecognizer property:

    @property (strong, nonatomic) SKRecognizer* voiceSearch;

    Next, switch to ViewController.m. Open the email from Nuance or refer to the Development Key that you got from the Nuance Developer Portal, and add your SpeechKitApplicationKey before the @implementation section:

    const unsigned char SpeechKitApplicationKey[] = {INSERT_YOUR_APPLICATION_KEY_HERE};

    Be sure to replace the INSERT_YOUR_APPLICATION_KEY_HERE with your unique application key that you got in the email earlier. It should be a long list of hex numbers.

    Next open AppDelegate.h and import the SpeechKit.h framework header:

    #import <SpeechKit/SpeechKit.h>

    Also replace setupSpeechKitConnection method in AppDelegate.m with the following:

    - (void)setupSpeechKitConnection {
         [SpeechKit setupWithID:INSERT_YOUR_APPLICATION_ID_HERE
         host:INSERT_YOUR_HOST_ADDRESS_HERE
         port:INSERT_YOUR_HOST_PORT_HERE
         useSSL:NO
         delegate:nil];
     
         // Set earcons to play
         SKEarcon* earconStart	= [SKEarcon earconWithName:@"earcon_listening.wav"];
         SKEarcon* earconStop	= [SKEarcon earconWithName:@"earcon_done_listening.wav"];
         SKEarcon* earconCancel	= [SKEarcon earconWithName:@"earcon_cancel.wav"];
     
         [SpeechKit setEarcon:earconStart forType:SKStartRecordingEarconType];
         [SpeechKit setEarcon:earconStop forType:SKStopRecordingEarconType];
         [SpeechKit setEarcon:earconCancel forType:SKCancelRecordingEarconType];     
    }

    Input the actual APPLICATION_ID, HOST_ADDRESS and PORT you have received from Nuance while setting up SpeechKit (it’s easiest to just copy/paste the code from the email).

    The code you added here is new, so let’s review it a bit.

    • setupWithID:host:port:useSSL:delegate: initiates the necessary underlying components of SpeechKit framework. Upon calling this method, the app connects with the speech server and receives authorization. This provides the basis to perform recognitions and vocalizations.
    • setEarcon:forType: configures an earcon, which is a distinctive sound that represents specific events. In this case, the earcons play the respective audio cue when SpeechKit starts, stops and cancels a recording.

    Next, you’ll add the three earcons that will play during the recognition process.

    To do this, select Supporting Files, right click and select Add Files to “YelpNearby”…, then locate DragonMobileRecognizer sample project that’s part of DragonMobileSDK.

    Earcons

    Select the earcon files that end with .wav, make sure to Copy items into destination group’s folder (if needed) is checked, and click Add to add the files to your project.

    Add_Earcons_2

    Back in ViewController.m, replace the implementation of viewDidLoad method with the following:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
     
        self.messageLabel.text = @"Tap on the mic";
        self.activityIndicator.hidden = YES;
     
        if (!self.tableViewDisplayDataArray) {
            self.tableViewDisplayDataArray = [[NSMutableArray alloc] init];
        }
     
        self.appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
        [self.appDelegate updateCurrentLocation];
        [self.appDelegate setupSpeechKitConnection];
     
        self.searchTextField.returnKeyType = UIReturnKeySearch;
    }

    What does that do? updateCurrentLocation updates the user’s current location after the view loads, so the app always has the user’s current location. You don’t have to set up SpeechKit manually, because it’s configured by setupSpeechKitConnection.

    Next, replace the recordButtonTapped: method with the following:

    - (IBAction)recordButtonTapped:(id)sender {
        self.recordButton.selected = !self.recordButton.isSelected;
     
        // This will initialize a new speech recognizer instance
        if (self.recordButton.isSelected) {
            self.voiceSearch = [[SKRecognizer alloc] initWithType:SKSearchRecognizerType
                                                        detection:SKShortEndOfSpeechDetection
                                                         language:@"en_US"
                                                         delegate:self];
        }
     
        // This will stop existing speech recognizer processes
        else {
            if (self.voiceSearch) {
                [self.voiceSearch stopRecording];
                [self.voiceSearch cancel];
            }
        }
    }

    recordButtonTapped: is called when user taps on the Mic button

    Let’s review the method creates a new SKRecognizer instance:

    - (id)initWithType:(NSString *)type detection:(SKEndOfSpeechDetection)detection language:(NSString *)language delegate:(id <SKRecognizerDelegate>)delegate;

    This method takes 4 parameters:

    • type: This allows the server to anticipate the type of phrases the user is likely to say and select an appropriate vocabulary of words. Note: When a user pulls up speech recognition, usually he or she intends to search for a specific thing or dictate a command to the device, such as taking a note or posting something to social media. All of these actions are defined in SKSearchRecognizerType and SKDictationRecognizerType respectively.
    • detection: This automatically detects when speech stops. Possible values can be SKNoEndOfSpeechDetection, SKShortEndOfSpeechDetection, SKLongEndOfSpeechDetection which define ‘do not detect the end of speech’, ‘detect end of short phrase’ and ‘detect end of longer phrase,’ respectively.
    • language: This definies the language spoken by user and is expressed in ISO 639 code followed by ISO 3166-1 country code.
    • delegate: The delegate, which is the receiver for recognition responses.

    stopRecording stops the existing recording and streaming audio to the speech server. cancel stops all speech requests, even those that are pending.

    Next, add the following SKRecognizerDelegate methods in ViewController.m:

    # pragma mark - SKRecognizer Delegate Methods
     
    - (void)recognizerDidBeginRecording:(SKRecognizer *)recognizer {
            self.messageLabel.text = @"Listening..";
    }
     
    - (void)recognizerDidFinishRecording:(SKRecognizer *)recognizer {
            self.messageLabel.text = @"Done Listening..";
    }

    The code in recognizerDidBegingRecording: and recognizerDidFinishRecording: methods update the status message to let the user know whether the application is listening for a voice or has finished listening.

    The most important delegate method in this whole implementation is didFinishWithResults:, which is called when the recognition process successfully completes. Add the following code to implement this method:

    - (void)recognizer:(SKRecognizer *)recognizer didFinishWithResults:(SKRecognition *)results {
            long numOfResults = [results.results count];
     
            if (numOfResults > 0) {
               // update the text of text field with best result from SpeechKit
               self.searchTextField.text = [results firstResult];
            }
     
            self.recordButton.selected = !self.recordButton.isSelected;
     
            if (self.voiceSearch) {
                [self.voiceSearch cancel];
            }
    }

    In the above method, the results object contains an array of possible results. It places the best result at index 0, or returns an empty array if no error occurred but no speech was detected. Then it updates the text of the searchTextField with the best result.

    Once this process finishes, it has to cancel the recognition request in order to stop SKRecognizer from listening further.

    But what do you do if the user’s speech is unclear because they are on a busy street, mumbling indiscriminately because they are hungry beyond measure, or there is another error of some kind?

    If the recognition process completes with an error, the app needs to handle it and let the user know something went wrong. Add the following snippets to alert the user when there’s an error:

    - (void)recognizer:(SKRecognizer *)recognizer didFinishWithError:(NSError *)error suggestion:(NSString *)suggestion {
            self.recordButton.selected = NO;
            self.messageLabel.text = @"Connection error";
            self.activityIndicator.hidden = YES;
     
     
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error"
                                                            message:[error localizedDescription]
                                                           delegate:nil
                                                  cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
            [alert show];
    }

    Build and run the application now on a device (the SDK does not work properly on the simulator).

    Once the application launches, tap on the Mic icon and say something clearly in English. SpeechKit will detect it and display the result. Since this is your first time, why not start with a polite “Hello?”

    Starter App Screenshot

    Note: If when you try to record it immediately says “Cancelled” and shows an error like “recorder is null” or “[NMSP_ERROR] check status Error: 696e6974 init -> line: 485″, this probably means either something is wrong with your SpeechKit keys, or the SpeechKit servers are down. Double check your keys, and/or try again later.

    Using Yelp Search to Find Matching Restaurants

    Once SpeechKit can recognize a voice, you need to identify the search keyword and find matching restaurants using Yelp’s API.

    To do this, start by opening ViewController.h and import the YelpAPIService.h header that contains the definition of YelpAPIService class:

    #import "YelpAPIService.h"

    Next, declare that ViewController class will use YelpAPIServiceDelegate:

    @interface ViewController : UIViewController <UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource, SpeechKitDelegate, SKRecognizerDelegate, YelpAPIServiceDelegate>

    The YelpAPIServiceDelegate provides the loadResultWithDataArray: to detect when the Yelp search finishes and provides an array of matching restaurants.

    Next, declare a few properties and methods:

    @property (strong, nonatomic) YelpAPIService *yelpService;
    @property (strong, nonatomic) NSString* searchCriteria;
     
    - (NSString *)getYelpCategoryFromSearchText;
    - (void)findNearByRestaurantsFromYelpbyCategory:(NSString *)categoryFilter;

    This declares a property for the YelpAPIService included in the starter project to interact with the Yelp API, a property for the search string, and two methods that will find Yelp search categories and matching restaurants.

    Next, switch to ViewController.m and implement getYelpCategoryFromSearchText as follows:

    - (NSString *)getYelpCategoryFromSearchText {
        NSString *categoryFilter;
     
        if ([[self.searchTextField.text componentsSeparatedByString:@" restaurant"] count] > 1) {
            NSCharacterSet *separator = [NSCharacterSet whitespaceAndNewlineCharacterSet];
            NSArray *trimmedWordArray = [[[self.searchTextField.text componentsSeparatedByString:@"restaurant"] firstObject] componentsSeparatedByCharactersInSet:separator];
     
            if ([trimmedWordArray count] > 2) {
                int objectIndex = (int)[trimmedWordArray count] - 2;
                categoryFilter = [trimmedWordArray objectAtIndex:objectIndex];
            }
     
            else {
                categoryFilter = [trimmedWordArray objectAtIndex:0];
            }
        }
     
        else if (([[self.searchTextField.text componentsSeparatedByString:@" restaurant"] count] <= 1)
                 && self.searchTextField.text &&  self.searchTextField.text.length > 0){
            categoryFilter = self.searchTextField.text;
        }
     
        return categoryFilter;
    }

    getYelpCategoryFromSearchText extracts a category or keyword from search text, by looking for a particular pattern. For example, if the user says, “Japanese restaurants nearby” or “nearby Japanese restaurants” or “Japanese restaurant” or “Japanese restaurants” it’ll detect the keyword “Japanese” and pass that to the Yelp API.

    The code in the above method splits the best possible search result by space and by taking the word that precedes ‘restaurant.’ For a more complex application, a complete set of grammar may be specified to fit the context or search category. For many applications, the search text is whatever the user says.

    Next add this new method:

    - (void)findNearByRestaurantsFromYelpbyCategory:(NSString *)categoryFilter {
        if (categoryFilter && categoryFilter.length > 0) {
            if (([CLLocationManager authorizationStatus] != kCLAuthorizationStatusDenied)
                && self.appDelegate.currentUserLocation &&
                self.appDelegate.currentUserLocation.coordinate.latitude) {
     
                [self.tableViewDisplayDataArray removeAllObjects];
                [self.resultTableView reloadData];
     
                self.messageLabel.text = @"Fetching results..";
                self.activityIndicator.hidden = NO;
     
                self.yelpService = [[YelpAPIService alloc] init];
                self.yelpService.delegate = self;
     
                self.searchCriteria = categoryFilter;
     
                [self.yelpService searchNearByRestaurantsByFilter:[categoryFilter lowercaseString] atLatitude:self.appDelegate.currentUserLocation.coordinate.latitude andLongitude:self.appDelegate.currentUserLocation.coordinate.longitude];
            }
     
            else {
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Location is Disabled"
                                                                message:@"Enable it in settings and try again"
                                                               delegate:nil
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
                [alert show];
            }
        }
    }

    This method accepts a search category and uses the Yelp API to look for nearby restaurants that might quell the user’s hunger pangs. It’s usually a cuisine or category of restaurant, for example Chinese, Japanese, Barbecue, Sandwiches, Indian, etc. that the method passes, as well as the user’s latitude and longitude.

    Almost done. Add the following line of code in the recognizer:didFinishWithResults:results delegate method below the line self.recordButton.selected = !self.recordButton.isSelected;:

    // This will extract category filter from search text
    NSString *yelpCategoryFilter = [self getYelpCategoryFromSearchText];
     
    // This will find nearby restaurants by category
    [self findNearByRestaurantsFromYelpbyCategory:yelpCategoryFilter];

    The above code makes use of the methods that you implemented in steps 1 & 2; they use them to get a search category and execute a Yelp search.

    YelpAPI calls loadResultWithDataArray: once it has a response, so as a final step let’s implement that.

    # pragma mark - Yelp API Delegate Method
     
    -(void)loadResultWithDataArray:(NSArray *)resultArray {
        self.messageLabel.text = @"Tap on the mic";
        self.activityIndicator.hidden = YES;
     
        self.tableViewDisplayDataArray = [resultArray mutableCopy];
        [self.resultTableView reloadData];
    }

    Once the application has Yelp’s response, it reloads the tableView with the results. cellForRowAtIndexPath: is already implemented in the sample project, as it displays a thumbnail, name, address and rating of each restaurant as received from Yelp.

    Build & Run. Once the application launches, tap on the Mic icon and speak sentences like ‘Japanese Restaurants”> or “Chinese Restaurants nearby” or whatever kind of restaurant you’d like the app to find for you.

    Final App Screenshot

    Note: If you don’t get any results, it could be there are no restaurants in Yelp’s database nearby your location. Try to choose a restaurant type you are sure is nearby you.

    Text-to-speech Synthesis Using SKVocalizer

    Now you’re almost there! For this next exercise, you will learn how to use SKVoicalizerDelegate for text-to-speech synthesis.

    In ViewController.h, declare that ViewController is going to use SKVocalizerDelegate. The ViewController delegate declaration should look like this:

    @interface ViewController : UIViewController <UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource, SpeechKitDelegate, SKRecognizerDelegate, YelpAPIServiceDelegate, SKVocalizerDelegate>

    Declare these two properties:

    @property (strong, nonatomic) SKVocalizer* vocalizer;
    @property BOOL isSpeaking;

    This declares a property for the vocalizer, and a BOOL that will keep track of the status of text-to-speech process.

    Next, in ViewController.m add the following code in the else section of recordButtonTapped::

        if (self.isSpeaking) {
             [self.vocalizer cancel];
             self.isSpeaking = NO;
        }

    When the user taps the record button, the above codes will stop the current speech — if there’s one in progress — and cancel all pending speech requests.

    Now, add the following code at the end of loadResultWithDataArray:resultArray::

        if (self.isSpeaking) {
        [self.vocalizer cancel];
    }
     
    self.isSpeaking = YES;
    // 1
    self.vocalizer = [[SKVocalizer alloc] initWithLanguage:@"en_US" delegate:self];
     
    if ([self.tableViewDisplayDataArray count] > 0) {
        // 2
        [self.vocalizer speakString:[NSString stringWithFormat:@"I found %lu %@ restaurants",
                                     (unsigned long)[self.tableViewDisplayDataArray count],
                                     self.searchCriteria]];
    }
     
    else {
        [self.vocalizer speakString:[NSString stringWithFormat:@"I could not find any %@ restaurants",
                                     self.searchCriteria]];
    }

    These lines of code configure a new SKVocalizer. Why? The text-to-speech synthesis uses SKVocalizer to make your application speak text, and with this app it’s the number of restaurants it found.

    This happens in 2 steps:

    1. First, you need to initialize the vocalizer object using initWithLanguage:language delegate:.
    2. Second, you make the vocalizer object speak something using speakString: method. You’ve already added the code required to initialize and speak the text in previous step.

    Next, add the SKVocalizer delegate methods to cancel the vocalizer if there’s any error.

    - (void)vocalizer:(SKVocalizer *)vocalizer willBeginSpeakingString:(NSString *)text {
        self.isSpeaking = YES;
    }
     
    - (void)vocalizer:(SKVocalizer *)vocalizer didFinishSpeakingString:(NSString *)text withError:(NSError *)error {
       if (error !=nil) {
          UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error"
    			                              message:[error localizedDescription]
    			                             delegate:nil
    		                            cancelButtonTitle:@"OK"
    		                            otherButtonTitles:nil];
          [alert show];
     
          if (self.isSpeaking) {
             [self.vocalizer cancel];
          }
        }
     
        self.isSpeaking = NO;
    }

    These methods are called when the vocalizer starts and stops speaking, respectively. Note you set the isSpeaking flag appropriately here.

    Guess what – you’re finally done! Build and run, tap on the Mic icon and ask the app to find restaurants, for example, “Japanese Restaurants” or “Chinese Restaurants nearby.” The results should be similar to your previous results, but this time SKVocalizer will make your application say how many restaurants it found.

    Final App Screenshot

    Note: If you don’t hear SpeechKit saying anything, make sure your device isn’t in silent mode, and that the volume is turned up. This is an easy mistake to make because the initial messages that come from earcons like ‘listening’, ‘done listening’ will play even if your device is in silent mode, which might make you not realize your device is in silent mode.

    Watch out – your app now talks back!

    Comparison with Other SDKs

    Remember that SpeechKit is just one of many frameworks you can use for speech detection and vocalization. There are a number of other frameworks you may want to consider:

    • OpenEars: It’s FREE but only has support for English and Spanish.
    • CeedVocal: It’s available in 6 languages (English, French, Dutch, German, Spanish and Italian). It’s not free, but you can buy licences per application or pay a one-time fee of 6000 Euros for an unlimited number of applications. Each license is good for one language, but you can add additional languages as needed. Try working with the trial version first.
    • iSpeech: This SDK has support for 20 languages as of now, and is free for mobile apps.

    Both OpenEars and CEEDVocal share the issue of low function in a noisy environment. Out of all these frameworks Nuance’s SpeechKit supports more languages, understands different accents and remains useful when there is a lot of background noise.

    However, there are two scenarios where you may need to consider other frameworks. If your application needs to process speech recognition in offline mode, then you might like OpenEars. All other frameworks require network connectivity.

    SpeechKit requires that port 51001 be left open. Some non-Windows firewalls, and various antivirus software may disable port 51001. This will cause a connection error while using SpeechKit. If that occurs, you need to configure the software to open the port.

    Where To Go From Here?

    Here is the finished sample project with all of the code from the above tutorial.

    Congratulations, you have successfully integrated Nuance Mobile SDK and built an iOS application like Siri!No doubt you’ve worked up an appetite, so why not test out your cool new app and see if it can help you find somewhere grab a victory snack or frosty beverage?

    You can dive deeper and find more about Nuance Mobile SDK under the resources tab in the Nuance Mobile Developer portal. Make sure you check out the iOS developer guide and iOS API reference to learn more about the speech recognition process.

    If you have any questions, comments or find a unique requirement about your voice recognition application, feel free to leave a comment here and I will be happy to help.

    Building an iOS App like Siri is a post from: Ray Wenderlich

    The post Building an iOS App like Siri appeared first on Ray Wenderlich.

    Video Tutorial: Using LLDB in iOS Part 5: Finding Help and Creating Aliases

    Reminder: Free Live Tech Talk (Procedural Level Generation in Games) Tomorrow (Tuesday)!

    $
    0
    0
    Free live tech talk (Procedural Level Generation in Games) tomorrow!

    Free live tech talk (Procedural Level Generation in Games) tomorrow!

    This is a reminder that we are having a free live tech talk on Procedural Level Generation in Games (Tuesday May 6), and you’re all invited! Here are the details:

    • When: Tuesday, May 6th at 2:00 PM EST – 3:00 PM EST
    • What: Procedural Level Generation in Games Tech Talk followed by live Q&A (come w/ questions!)
    • Who: Kim Pedersen (Tutorial Team member), Wes Paugh (Developer of 100 Rogues)
    • Where: Google Hangouts Event Page
    • Why: For learning and fun!
    • How: Visit the event page and a video URL should be posted. Follow the instructions there to submit your Q&A (via text) as the talk runs.

    We hope to see some of you at the tech talk, and we hope you enjoy!

    Reminder: Free Live Tech Talk (Procedural Level Generation in Games) Tomorrow (Tuesday)! is a post from: Ray Wenderlich

    The post Reminder: Free Live Tech Talk (Procedural Level Generation in Games) Tomorrow (Tuesday)! appeared first on Ray Wenderlich.

    What Every iOS Developer Needs to Know about Facebook’s F8 Developer Conference

    $
    0
    0
    Everything you need to know about Facebook's recent developer conference: F8!

    Everything you need to know about Facebook’s recent developer conference: F8!

    This past Wednesday, I had the incredible opportunity to attend Facebook’s F8 developers conference in San Francisco along with about 1,700 other developers, designers and marketers.

    A lot has happened since the last F8 conference in 2011:

    • Facebook became a publicly traded company
    • Facebook acquired several companies including WhatsApp, Parse, Oculus VR and Instagram
    • Facebook celebrated their 10th year as an organization

    This year the conference had a strong focus on apps, so there was a lot of material highly relevant to iOS developers. In this article, I’ll give you a recap of everything you need to know!

    We’ll take a look at the Parse and Facebook announcements relevant to iOS developers, I’ll discuss the most interesting session I attended, and conclude with my thoughts on the overall experience of the conference.

    Let’s dive right in!

    Parse Announcements

    Parse

    The first set of announcements relate to Facebook’s back-end-as-a-service: Parse.

    Parse is very near and dear to my heart. I built my very first app on Parse nearly two and a half years ago, and since then I’ve used their tools and services in various other apps I’ve worked on.

    Their team is incredibly smart, and it’s clear they care about the developers using their product as they are constantly releasing new products and continually updating their existing set of apps.

    It broke my heart when Facebook acquired Parse in 2013 because I thought Facebook intended to dismantle Parse and absorb their workforce and technologies. However, F8 showed Parse to be stronger than ever with Parse CEO Ilya Sukhar sharing the stage with Mark Zuckerberg himself.

    Let’s take a look at four major announcements from Parse: AppLinks, Using Parse Offline, Free Tier Level, and Growth Analytics.

    Note: If you are new to Parse, you might want to check out our Parse Tutorial to learn the basics first.

    One of Parse’s most exciting announcements is their new open-source offering AppLinks. This lets developers deep link content between apps and quickly return to the user’s originating app.

    This concept isn’t necessarily new; it’s been around for some time on both iOS and Android. To deep link in iOS, an app must register for a particular URL scheme such as youtube:// and then pass data to the target app using an instance of NSURL.

    However, this requires that both apps understand and agree upon the structure of the data in the URL, and that both apps implement URL routers. Furthermore, this doesn’t work cross-platform, and doesn’t have a central registry; App Links solves this problem.

    Some organizations like Urx and DeepLink have already made forays into deep linking; some have even questioned why Parse would release this with WWDC just a few weeks away, as this quote from Twitter shows:

    In any case, AppLinks looks like an awesome tool that I look forward to playing around with. To learn more about AppLinks, check out the official AppLinks site.

    Using Parse Offline

    In addition to AppLinks, Parse announced the addition of local storage to their SDK. Basically this allows you to choose to “pin” some of your objects to a local datastore, so that you can access them anytime (even if you lose an internet connection). Here’s an example snippet provided by Parse:

    // Pin ParseQuery results
    List<ParseObject> objects = query.find(); // Online ParseQuery results
    ParseObject.pinAllInBackground(objects);
     
    // Query the Local Datastore
    ParseQuery<ParseObject> query = ParseQuery.get("Feed")
        .fromLocalDatastore()
        .whereEquals("starred", true)
        .findInBackground(new FindCallback() {
            public void done(List<ParseObject> objects, ParseException e) {
                // Update UI
            }
        });

    Unfortunately this functionality is only available in the Android SDK at the moment (horrors!), but Parse assures us that it will be available for iOS very soon.

    My biggest issue with Parse has always been their lack of support for local data storage. To their credit, Parse provides an amazing service that lets developers focus on their apps instead of spending time and effort building the backend to support their apps.

    However, once you’ve released your minimum viable product and you’re ready to provide a richer experience to your users, adding offline support with Parse becomes difficult. Your choices are limited to relying on web caching, which is spotty at best, or writing your own sync engine, neither of which are terribly attractive.

    I haven’t been able to find any implementation details for Local Data on either iOS or Android. I’m assuming it’s built on top of SQLite, but at this point I can’t be certain.

    To learn more about Parse local storage, check out the official announcement.

    Free Tier Level

    Parse is also expanding their free tier level, offering up to 30 API requests per second, 20GB file storage, 1 million push notifications a month, and full access to Parse Analytics.

    I guess being acquired by Facebook is beneficial for everyone! :]

    To learn more about the Free Tier Level, check out Parse’s plans page.

    Growth Analytics

    Growth Analytics is the newest addition to Parse’s already robust analytics package. This allows you to measure active user, installations, retention data, and more with easy-to-read graphs. It looks like this feature is automatically turned on and might not even require the latest SDK.

    Maintaining your user base is a tricky problem for app developers, so I’m glad to see that Parse is giving developers more power to monitor their own apps. It’s beginning to feel like you could ditch other analytics tools like Flurry if you’re already using Parse in your app.

    To learn more about Growth Analytics and Parse Analytics in general, check out the Parse Analytics page.

    Facebook Announcements

    Parse wasn’t the only group with groundbreaking announcements at F8 — Facebook had their share of news for us as well.

    In this section, I’ll highlight three key announcements: API versioning, Facebook Login updates, the Audience Network, and FbStart.

    API Versioning

    Mark Zuckerburg started his keynote with a promise to developers that Facebook is dedicated to providing a highly performant and stable platform to power apps across the world.

    Mark Zuckerberg presenting during the keynote.

    Mark Zuckerberg presenting during the keynote.

    On top of that, he also gave some assurances to developers who are fearful of becoming too dependent on Facebook’s platform.

    Facebook will now begin versioning their APIs; this means that the app you build against the Facebook API today won’t break when Facebook launches new tools or features. They also guarantee two years of support and maintenance for all of their APIs.

    If that weren’t enough, Facebook is now promising to fix any critical bugs in their APIs within 48 hours.

    I hope that Facebook can follow through on these lofty goals, because I would love to integrate Facebook into more of my apps without the constant fear that each Facebook update will render my apps useless.

    To learn more about API versioning, check out Facebook’s official documentation on the subject.

    Facebook Login Updates

    Zuckerberg admitted that users tend to fear the “Login with Facebook” button; they want more control over access permissions to ensure their personal data is kept safe. To this end, Facebook is introducing two important updates to Facebook Login.

    First, the mechanism by which users give apps permission to access their personal data is changing. Previously, apps would ask for permission, in a step-by-step fashion, to use different Facebook features such as posting on your behalf or accessing your friends list.

    Facebook’s modified scheme now displays a more detailed list of features the app would like to access, along with giving users the ability to toggle access to those features on and off as they please.

    Second, Facebook announced a brand new feature called Anonymous Login. Instead of giving the app access to your Facebook account on login, you can now log into an app anonymously using Facebook.

    Login-Anonymous

    This is a huge improvement over requiring Facebook, Twitter, or an email address to login and start using an app. Now you can log into an app anonymously and give it a whirl before you make the decision to surrender access to all of your data. Kudos, Facebook!

    To learn more about the Facebook Login updates, check out Facebook’s announcement post.

    Audience Network

    Facebook is both celebrated and condemned for serving as an ad platform.

    It’s a familiar scenario: each time you log in, your poor news feed ends up spattered with ads all over the place. Facebook admits that irrelevant ads create a poor user experience, so they’ve been working on some advanced tools to help make viewing ads a bit more pleasant.

    At F8 they announced a new SDK called the Audience Network that allows you to integrate highly tailored ads into your apps. The goal is to allow advertisers to target people with highly customized interests, such as “Movie Lovers with iPhones”. The result is two-fold: more clicks for advertisers and a better user experience for consumers.

    You can either use the Audience Network as a publisher (integrating the SDK into your apps to earn money), or as an advertiser (placing ads inside other apps).

    I really hope Audience Networks lives up to its promise, because I haven’t yet found a way to make money with my own movie app, and this feature might be just the magic my app needs!

    To gain a little more insight into the Facebook ad network, I attended a session that discussed increasing user engagement with your app by creating ads on Facebook that have deep links to content within your app.

    It turns out it’s quite a simple process: you add the Facebook Ads SDK to your app, set up an ad campaign with specific content targeted to a group of Facebook users, and (hopefully!) drive users to your app. You can even target the subset of Facebook users that have installed your app. This is a huge opportunity for developers who want to increase user retention!

    To learn more about the Audience Network or apply to join its beta, visit the official Facebook page.

    FbStart

    FbStart is a new program to help small startups grow their businesses. You can apply to access to tons of free tools and services, such as Parse Credit, an Adobe Creative Cloud subscription, customer service and prototyping tools, and more.

    There are two tracks to the program: a bootstrap track (for those just starting out) and an accelerate track (for those trying to scale their apps to the next level).

    It appears the program hasn’t launched yet, but if this sounds like something that’s interesting you can sign up to get notified when it launches here.

    Making Paper

    My favorite session of F8 featured a few of the team members from Facebook’s latest app, Paper.

    FacebookPaper

    Part of the session focused on the team’s struggles with performance issues resulting from the sheer amount of data being dynamically updated and displayed on the screen while still supporting beautiful and complex animations.

    Their first step to address these issues was to offload some work to a background thread, which is usually the first thing developers do when they hit UI performance issues.

    However, the Paper team took it a step further and used background threads for something I didn’t even think was possible: assembling and drawing the UI.

    Most seasoned iOS developers know that Core Animation and UIKit are considered “hands off” when working on background threads. The Paper team created what I term a “virtual UIKit” that assembles and draws the interface in the background to use as the content to CALayer.

    It’s a simple solution, in theory, but I imagine it’s several thousand lines of code that took a talented team of engineers some time to make it work correctly.

    An open source solution to the above issue is apparently in the works, and that’s on top of all the other open source tools that came out of Paper, including Pop, Tweaks, and Origami.

    You can join the Facebook group of the Paper engineering team just as I did, where you can ask questions about their tools and processes and see what other goodies they’re working on.

    Overall Experience

    It was obvious that Facebook put a lot of effort into organizing this year’s F8 conference. There were a ton of amazing displays including an Oculus VR experience and a huge centerpiece with dimensional projections, shown below:

    Facebook even created an F8 app so attendees could get some basic information on the conference and build their own session schedule. Once you created your personal schedule, the app would send you a push notification reminder when a session was about to begin.

    The conference lanyard had some sort of embedded RFID to scan people into rooms; I’m assuming this was for the purpose of getting feedback from attendees.

    The F8 conference badge.

    All in all, F8 used technology in some really cool ways.

    I have to send out a huge thanks to Ashley Smith who pieced a lot of the conference together!

    Closing Thoughts

    From the keynote to the swag bag, from Diplo to meeting the Paper team, I had a blast at this year’s F8.

    I’m really excited about the commitments Facebook has made to the developer community, especially the overarching focus on mobile integration. Thankfully, Parse is continuing to innovate and grow, and it doesn’t look like they’ll be slowing down anytime soon.

    The next F8 conference will take place March 25th, 2015 in San Francisco. I’d definitely recommend that anyone who is considering working with any part of Facebook in their apps should make it a point to attend.

    I now have a new tools I need to play around with, new people I can call friends…and a lot of sleep to catch up on. :] I hope you enjoyed this recap, and I hope to see you at the next F8!

    What Every iOS Developer Needs to Know about Facebook’s F8 Developer Conference is a post from: Ray Wenderlich

    The post What Every iOS Developer Needs to Know about Facebook’s F8 Developer Conference appeared first on Ray Wenderlich.


    Video Tutorial: Using LLDB in iOS Part 6: Extending LLDB

    A Sneak Peek: Scroll View School

    $
    0
    0

    Check out this 15-sec video giving a sneak peek of our upcoming 14-part video tutorial series titled “Scroll View School”:


    This will be coming up next after our current 9-part LLDB series.

    Huge thanks to @d0tc0m, @bballentine, @ruud, and @charlie for the idea for this Scroll View School series, and keep the suggestions coming! :]

    I can’t wait – I hope you all enjoy!

    A Sneak Peek: Scroll View School is a post from: Ray Wenderlich

    The post A Sneak Peek: Scroll View School appeared first on Ray Wenderlich.

    Video Tutorial: Using LLDB in iOS Part 7: Custom Summaries

    Procedural Level Generation in Games Tech Talk Video

    $
    0
    0

    The first Tuesday of each month, one of the members of the team gives a Tech Talk, and by popular request we’ve started to stream these live.

    Today in our May Tech Talk, Kim Pedersen (Tutorial Team member) and Wes Paugh (Developer of 100 Rogues) gave an excellent talk and Q&A on Procedural Level Generation in Games.

    Here’s the video for anyone who didn’t get a chance to watch!

    Helpful Links

    Here are some handy links to learn more about Procedural Level Generation in Games:

    Want to Join Us Next Month?

    Thanks again Kim and Wes for giving a great talk and Q&A, having the guts to present to a live audience :] And thank you to everyone who attended – we hope you enjoyed it!

    Next month, our June Tech talk will be a special session right after the WWDC keynote titled WWDC Keynote: Podcasters React, where podcasters Mic Pringle, Tammy Coron, Jake Gundersen, and Felipe Laso Marsetti will be sharing their live reactions to the announcements – and you can share yours too with the live Q&A!

    We will be broadcasting this talk live on Monday, Jun 2 at 3:00 PM EST, so if you want to join us sign up here! As you watch the talk, you can submit any Q&A you may have live.

    Hope to see some of you there! :]

    Procedural Level Generation in Games Tech Talk Video is a post from: Ray Wenderlich

    The post Procedural Level Generation in Games Tech Talk Video appeared first on Ray Wenderlich.

    Video Tutorial: Using LLDB in iOS Part 7: Using Chisel

    2D Game Art: The raywenderlich.com Podcast Episode 6

    $
    0
    0
    Episode 6: 2D Game Art

    Episode 6: 2D Game Art

    In this episode, we chat with special guests Mike Berg and Vicki Wenderlich about 2D Game Art: tools, hiring artists, costs, learning, and more.

    We also chat with tech editor Ryan Nystrom about Facebook F8 and the related announcements: Facebook Anonymous Login, App Links, Audience Network, Origami, Pop, and more.

    [Subscribe in iTunes] [RSS Feed]

    Here’s what is covered in this episode:

    • News: Facebook F8
    • What’s new in raywenderlich.com: Best new tutorials and sneak peek of coming month
    • Tech Talk: 2D Game Art

    Links and References

    Our Sponsor

    • Syncano: Making it easier to synchronize and scale data across devices and servers – in real time.

    News: Facebook F8

    What’s New on raywenderlich.com

    Tech Talk: 2D Game Art

    Contact Us

    Where To Go From Here?

    We hope you enjoyed this podcast! We have an episode each month, so be sure to subscribe in iTunes to get access as soon as it comes out.

    We’d love to hear what you think about the podcast, and any suggestions on what you’d like to hear in future episodes. Feel free to drop a comment here, or email us anytime at podcast@raywenderlich.com!

    2D Game Art: The raywenderlich.com Podcast Episode 6 is a post from: Ray Wenderlich

    The post 2D Game Art: The raywenderlich.com Podcast Episode 6 appeared first on Ray Wenderlich.

    How to Make a Narrated Book Using AVSpeechSynthesizer in iOS 7

    $
    0
    0

    Narrated Book with Speech Control

    Your UI With Speech Control Buttons

    With the introduction of the PageViewController, Apple has made it easy for developers to make their own book apps. Unfortunately, in the busy schedule of nine-to-five living, reading can be more of a luxury. Wouldn’t be to nice to have a book narrated to you while you multitask?

    With the introduction of Siri, Apple has taunted developers with the implication of dynamic spoken text, but with the release of iOS 7, Apple has finally opened the door.

    Introducing, AVSpeechSynthesizer. Or Siri-Synthesizer for short :]

    In this tutorial, you’ll create a narrated book. Each page of the book will display text while simultaneously speaking the text. Audio narration is a splendid way to make your book app stand out from all the others on iTunes, while also accommodating those with visual impairments. Offering an audio book app can also make your work more appealing to a broader audience, since they allow people to “read” while they exercise, cook or get a little work done.

    As you create your narrated book, you’ll learn:

    • How to make an iOS device speak text using AVSpeechSynthesizer and AVSpeechUtterance.
    • How to make this synthesized speech sound more natural by modifying AVSpeechUtterance properties like pitch and rate.

    AVSpeechSynthesizer may not win any awards for voice acting, but you can use it relatively easily to enhance functionality in apps you develop in the future.

    Note: If you are interested in developing children’s books on the iPad using Sprite Kit, check out Tammy Coron’s excellent tutorial over here: How to Create an Interactive Children’s Book for the iPad

    Getting Started with AVSpeechSynthesizer

    Start by downloading the Starter Project. Open the project in Xcode by navigating into the NarratedBookUsingAVSpeech Starter directory and double-clicking on the NarratedBookUsingAVSpeech.xcodeproj project file. You should see something similar to the image below:

    First Open in Xcode

    Build and run the project. You will see the following in the simulator:

    First Run of Your App Page 1 - After Swiping Left-to-right

    Your first book is a nursery rhyme about squirrels. It’s not exactly Amazon Top Selling material, but it will do for the purposes of this tutorial. Use your mouse to swipe from right-to-left in the simulator, and you’ll move to the next page as below.

    Second Page of App

    Use your mouse to swipe from left-to-right in the simulator, and you’ll return to the first page. Wow, you already have a functioning book. Nice work!

    Understanding the Plumbing

    Note: At the end of this tutorial, there are a few challenges for you. This next section covers the sample project so you can take those challenges, but if you are not interested, feel free to skip to the next section.

    The starter project has two important classes:

    1. Models: These store your book as a single Book object and its collection of Page objects.
    2. Presentation: These present your models on the screen and respond to user interaction (e.g. swipes).

    If you choose to build on this project to make your own books, its important you understand how these work. Open RWTBook.h and examine its structure.

    @interface RWTBook : NSObject
     
    //1
    @property (nonatomic, copy, readonly) NSArray *pages;
     
    //2
    + (instancetype)bookWithPages:(NSArray*)pages;
    //3
    + (instancetype)testBook;
     
    @end
    1. The pages property stores an array of Page objects, each representing a single page in the book.
    2. bookWithPages: is a convenience method to initialize and return a book with the given array of Page objects.
    3. testBook creates your book for testing purposes. You’ll start writing and reading your own books soon enough, but testBook is a simple book that is perfect to get you started.

    Open RWTPage.h and examine its structure.

    //1
    extern NSString* const RWTPageAttributesKeyUtterances;
    extern NSString* const RWTPageAttributesKeyBackgroundImage;
     
    @interface RWTPage : NSObject
     
    //2
    @property (nonatomic, strong, readonly) NSString *displayText;
    @property (nonatomic, strong, readonly) UIImage *backgroundImage;
     
    //3
    + (instancetype)pageWithAttributes:(NSDictionary*)attributes;
    @end
    1. Accesses the constants for dictionary look-ups for each page. The RWTPageAttributesKeyUtterances constant corresponds to the text on each page of the book. The RWTPageAttributesKeyBackgroundImage constant returns each background image for the page
    2. The displayText property stores the text of the page that your book presents on-screen, and the backgroundImage stores the image behind the text.
    3. pageWithAttributes: initializes and returns a page with the given dictionary of attributes.

    Finally, open RWTPageViewController.m and examine its structure:

    #pragma mark - Class Extension
     
    // 1
    @interface RWTPageViewController ()
    @property (nonatomic, strong) RWTBook *book;
    @property (nonatomic, assign) NSUInteger currentPageIndex;
    @end
     
    @implementation RWTPageViewController
     
    #pragma mark - Lifecycle
     
    // 2
    - (void)viewDidLoad
    {
      [super viewDidLoad];
     
      [self setupBook:[RWTBook testBook]];
     
      UISwipeGestureRecognizer *swipeNext = [[UISwipeGestureRecognizer alloc]
                                              initWithTarget:self
                                                      action:@selector(gotoNextPage)];
      swipeNext.direction = UISwipeGestureRecognizerDirectionLeft;
      [self.view addGestureRecognizer:swipeNext];
     
      UISwipeGestureRecognizer *swipePrevious = [[UISwipeGestureRecognizer alloc]
                                                  initWithTarget:self
                                                          action:@selector(gotoPreviousPage)];
      swipePrevious.direction = UISwipeGestureRecognizerDirectionRight;
      [self.view addGestureRecognizer:swipePrevious];
    }
     
    #pragma mark - Private
     
    // 3
    - (RWTPage*)currentPage
    {
      return [self.book.pages objectAtIndex:self.currentPageIndex];
    }
     
    // 4
    - (void)setupBook:(RWTBook*)newBook
    {
      self.book = newBook;
      self.currentPageIndex = 0;
      [self setupForCurrentPage];
    }
     
    // 5
    - (void)setupForCurrentPage
    {
      self.pageTextLabel.text = [self currentPage].displayText;
      self.pageImageView.image = [self currentPage].backgroundImage;
    }
     
    // 6
    - (void)gotoNextPage
    {
      if ([self.book.pages count] == 0 || self.currentPageIndex == [self.book.pages count] - 1) {
        return;
      }
     
      self.currentPageIndex += 1;
      [self setupForCurrentPage];
    }
     
    // 7
    - (void)gotoPreviousPage
    {
      if (self.currentPageIndex == 0) {
        return;
      }
     
      self.currentPageIndex -= 1;
      [self setupForCurrentPage];
    }
    @end

    Here’s what this code does:

    1. The book property stores the current book and, the currentPageIndex property stores the index of the current page in book.pages.
    2. Sets up the page display once your view loads, then adds gesture recognizers so you can swipe forwards and backwards through the book’s pages.
    3. Returns the current page within the current book.
    4. Sets the book property and makes sure you start at the first page.
    5. Set up the UI for the current page.
    6. Go to the next page, if applicable, and set it up. It’s invoked by the swipeNext gesture recognizer you created in viewDidLoad.
    7. Go to the previous page, if there is one, and set it up. This is invoked by the swipePrevious gesture recognizer you created in viewDidLoad.

    To Speak or Not to Speak!

    That is the question.

    Open RWTPageViewController.m and underneath #import "RWTPage.h", add the following line:

    @import AVFoundation;

    iOS speech support is in the AVFoundation framework so you must import the AVFoundation module.

    Note: The @import will both import and link the AVFoundation framework. To learn more about @import as well as some other new Objective-C language features in iOS 7, check out the article: What’s New in Objective-C and Foundation in iOS 7.

    Add the following line just below the declaration of the currentPageIndex property in the RWTPageViewController class extension:

    @property (nonatomic, strong) AVSpeechSynthesizer *synthesizer;

    You’ve just added the speech synthesizer that will speak the words in each page.

    Think of the AVSpeechSynthesizer you just added to your view controller as the person doing the speaking. AVSpeechUtterance instances represent the chunks of text the synthesizer speaks.

    Note: An AVSpeechUtterance can be a single word like “Whisky” or an entire sentence, such as, “Whisky, frisky, hippidity hop.”

    Add the following code just before the @end at the bottom of RWTPageViewController.m

    #pragma mark - Speech Management
     
    - (void)speakNextUtterance
    {
      AVSpeechUtterance *nextUtterance = [[AVSpeechUtterance alloc]
                                           initWithString:[self currentPage].displayText];
      [self.synthesizer speakUtterance:nextUtterance];
    }

    You’ve created an utterance to speak, and told the synthesizer to speak it.

    Now add the following code just below speakNextUtterance

    - (void)startSpeaking
    {
      if (!self.synthesizer) {
        self.synthesizer = [[AVSpeechSynthesizer alloc] init];
      }
     
      [self speakNextUtterance];
    }

    This code initializes the synthesizer property if it’s not already initialized. Then it invokes speakNextUtterance to speak.

    Add the following line of code to the very end of viewDidLoad, gotoNextPage and gotoPreviousPage

      [self startSpeaking];

    Your additions ensure that speech starts when the book loads, as well as when the user advances to the next or previous page.

    Build and run and listen to the dulcet tones of AVSpeechSyntesizer.

    Note: If you don’t hear anything, check the volume on your Mac or iOS device (wherever you’re running the app). You might need to swipe between pages to start speech again if you missed it.

    Also note: if you are running this project in the simulator, be prepared to have your console filled with cryptic error messages. This appears only to happen in the simulator. They will not print out when used on a device.

    Once you’ve confirmed that you can hear speech, try building and running again, but this time, swipe from right-to-left before the first page finishes talking. What do you notice?

    The synthesizer will start speaking the second page’s text once it’s completed the first page. That’s not what users will expect; they’ll expect that swiping to another page will stop speech for the current page and start it for the next page. This glitch isn’t so worrisome for short pages like nursery ryhmes, but imagine what could happen with very long pages…

    Breaking Speech into Parts

    One reliable principle of software engineering is to keep data and code separate. It makes testing your code easier, and it makes it easier to run your code on different input data. Moreover, keeping data out of code allows you to download new data at runtime. For example, wouldn’t it be grand if your book app could download new books at runtime?

    You’re currently using a simple test book Book.testBook to test your code. You’re about to change that by storing books in and reading them from Apple’s plist (XML) format files.

    Open Supporting Files\WhirlySquirrelly.plist and you’ll see something like the following

    WhirlySquirrelly.plist

    You can also see the raw data structure by Right-Clicking on Supporting Files\WhirlySquirrelly.plist and selecting Open As\Source Code.

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>bookPages</key>
      <array>
        <!-- First page -->
        <dict>
          <key>backgroundImage</key>
          <string>PageBackgroundImage.jpg</string>
          <key>utterances</key>
          <array>
            <dict>
              <key>utteranceProperties</key>
              <dict>
                <key>pitchMultiplier</key>
                <real>1</real>
                <key>rate</key>
                <real>1.2</real>
              </dict>
              <key>utteranceString</key>
              <string>Whisky,</string>
            </dict>
            ...
          </array>
        </dict>
        <!-- Second page -->
        <dict>
          <key>backgroundImage</key>
          <string>PageBackgroundImage.jpg</string>
          <key>utterances</key>
          <array>
            <dict>
              <key>utteranceProperties</key>
              <dict>
                <key>pitchMultiplier</key>
                <real>1.2</real>
                <key>rate</key>
                <real>1.3</real>
              </dict>
              <key>utteranceString</key>
              <string>Whirly,</string>
            </dict>
            ...
          </array>
        </dict>
      </array>
    </dict>
    </plist>

    It’s nice to have a high-level view of my data structures. The data structure in Supporting Files\WhirlySquirrelly.plist is outlined as follows (where {} indicates a dictionary and [] an array):

    Book {
      bookPages => [
      	{FirstPage
                    backgroundImage => "Name of background image file",
      		utterances => [
      			{ utteranceString     => "what to say first",
      			  utteranceProperties => { how to say it }
      			},
      			{ utteranceString     => "what to say next",
      			  utteranceProperties => { how to say it }
      			}
      		]
      	},
      	{SecondPage
                    backgroundImage => "Name of background image file",
      		utterances => [
      			{ utteranceString     => "what to say last",
      			  utteranceProperties => { how to say it }
      			}
      		]
      	}
      ]
    }
    

    Behold the power of ASCII art! :]

    Supporting Files\WhirlySquirrelly.plist breaks up the text into one-utterance-per-word. The virtue of doing this is that you can control the speech properties’ pitch (high voice or low voice) and rate (slow or fast-talking) for each word.

    The reason your synthesizer sounds so mechanical, like a robot from a cheesy 1950′s sci-fi movie, is that its diction is too uniform. To make your synthesizer speak more like a human, you’ll need to control the pitch and meter, which will vary its diction.

    Parsing Power

    You’ll parse Supporting Files\WhirlySquirrelly.plist into a RWTBook object. Open RWTBook.h and add the following line right after the declaration of bookWithPages:

      + (instancetype)bookWithContentsOfFile:(NSString*)path;

    This method will read a file like Supporting Files\WhirlySquirrelly.plist, then initialize and return a RWTBook instance that holds the file’s data.

    Open RWTBook.m and add the following code right below #import "RWTPage.h"

    #pragma mark - External Constants
     
    NSString* const RWTBookAttributesKeyBookPages = @"bookPages";

    This is the key you’ll use to retrieve the book’s pages from files like Supporting Files\WhirlySquirrelly.plist.

    With RWTBook.m still open, add the following code at the bottom of the file, just before the @end.

     
    #pragma mark - Private
     
    + (instancetype)bookWithContentsOfFile:(NSString*)path
    {
      // 1
      NSDictionary *bookAttributes = [NSDictionary dictionaryWithContentsOfFile:path];
      if (!bookAttributes) {
        return nil;
      }
     
      // 2
      NSMutableArray *pages = [NSMutableArray arrayWithCapacity:2];
      for (NSDictionary *pageAttributes in [bookAttributes objectForKey:RWTBookAttributesKeyBookPages]) {
        RWTPage *page = [RWTPage pageWithAttributes:pageAttributes];
        if (page) {
          [pages addObject:page];
        }
      }
     
      // 3
      return [self bookWithPages:pages];
    }

    Here’s what your new code does:

    1. Reads and initializes a dictionary of book attributes from the given path. This is where your code reads Supporting Files\WhirlySquirrelly.plist.
    2. Creates a new Page object for each dictionary of page attributes under the book attributes.
    3. Returns a new book using the handy bookWithPages: provided in the starter project.

    Open RWTPageViewController.m and navigate to viewDidLoad. Replace the line

      [self setupBook:[RWTBook testBook]];

    with

      NSString *path = [[NSBundle mainBundle] pathForResource:@"WhirlySquirrelly" ofType:@"plist"];
      [self setupBook:[RWTBook bookWithContentsOfFile:path]];

    Your new code locates WhirlySquirrelly.plist and creates a book from it by using bookWithContentsOfFile:.

    You’re almost ready to run your new code. Open RWTPage.m and add the following code below the #import "RWTPage.h"

    @import AVFoundation;

    Now you can reference AVSpeechUtterance in this file.

    Add the following constant definitions just below the definition of RWTPageAttributesKeyBackgroundImage

    NSString* const RWTUtteranceAttributesKeyUtteranceString = @"utteranceString";
    NSString* const RWTUtteranceAttributesKeyUtteranceProperties = @"utteranceProperties";

    These are the keys you’ll use to parse out individual AVSpeechUtterance attributes from a plist.

    Replace pageWithAttributes: with the following

    + (instancetype)pageWithAttributes:(NSDictionary*)attributes
    {
      RWTPage *page = [[RWTPage alloc] init];
     
      if ([[attributes objectForKey:RWTPageAttributesKeyUtterances] isKindOfClass:[NSString class]]) {
        // 1
        page.displayText = [attributes objectForKey:RWTPageAttributesKeyUtterances];
        page.backgroundImage = [attributes objectForKey:RWTPageAttributesKeyBackgroundImage];
      } else if ([[attributes objectForKey:RWTPageAttributesKeyUtterances] isKindOfClass:[NSArray class]]) {
        // 2
        NSMutableArray *utterances = [NSMutableArray arrayWithCapacity:31];
        NSMutableString *displayText = [NSMutableString stringWithCapacity:101];
     
        // 3
        for (NSDictionary *utteranceAttributes in [attributes objectForKey:RWTPageAttributesKeyUtterances]) {
          // 4
          NSString *utteranceString =
                     [utteranceAttributes objectForKey:RWTUtteranceAttributesKeyUtteranceString];
          NSDictionary *utteranceProperties =
                         [utteranceAttributes objectForKey:RWTUtteranceAttributesKeyUtteranceProperties];
     
          // 5
          AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:utteranceString];
          // 6
          [utterance setValuesForKeysWithDictionary:utteranceProperties];
     
          if (utterance) {
            // 7
            [utterances addObject:utterance];
            [displayText appendString:utteranceString];
          }
        }
     
        // 8
        page.displayText = displayText;
        page.backgroundImage = [UIImage imageNamed:[attributes objectForKey:RWTPageAttributesKeyBackgroundImage]];
      }
     
      return page;
    }

    Here’s what your new code does:

    1. Handles the case like RWTBook.testBook where a page’s utterances are a single NSString. Sets the display text and background image.
    2. Handles the case like Supporting Files\WhirlySquirrelly.plist where a page’s utterances are an NSArray of NSDictionary. Accumulates all the utterances and display text.
    3. Loop over the individual utterances for the page.
    4. Grabs the individual utterance’s utteranceString and utteranceProperties.
    5. Create a new AVSpeechUtterance to speak utteranceString.
    6. Set the new utterance’s properties using Key Value Coding (KVC). Although not openly documented by Apple, AVSpeechUtterance responds to the selector setValuesForKeysWithDictionary: so you can use it to set all the utteranceProperties in one fell swoop. Conveniently, this means you can add new utterance properties to your plist without needing to write new setter invocation code; setValuesForKeysWithDictionary: will handle the new properties automatically. That is, of course, provided the corresponding properties exist on AVSpeechUtterance and are writable.
    7. Accumulate the utterance and display text.
    8. Set the display text and background image.

    Build and run and listen to the speech.

    You’ve constructed each RWTPage.displayText from the combined utteranceStrings for the page in the plist. So, your page view displays the entire page’s text.
    However, remember that RWTPageViewController.speakNextUtterance creates a single AVSpeechUtterance for the entire RWTPage.displayText. The result is that it overlooks your carefully parsed utterance properties.

    In order to modify how each utterance is spoken, you need to synthesize each page’s text as individual utterances. If only there were some way to observe and control how and when AVSpeechSynthesizer speaks. Hmmm…

    Be a Good Delegate and Listen

    Your speech synthesizer AVSpeechSynthesizer has a delegate AVSpeechSynthesizerDelegate that is informed of various important events and actions in the speech synthesizer’s lifecycle. You’ll implement some of these delegate methods to make speech sound more natural by using the utterance properties included in WhirlySquirrelly.plist.

    Open RWTPage.h and add the following code after the declaration of displayText

      @property (nonatomic, strong, readonly) NSArray *utterances;

    Open RWTPage.m and add the following code after the declaration of displayText

      @property (nonatomic, strong, readwrite) NSArray *utterances;

    Note: You’re following a best-practice here by declaring properties in the header file as readonly and in the implementation file as readwrite. This makes sure that only object itself that can set its properties.

    Replace pageWithAttribute: with the following code

    + (instancetype)pageWithAttributes:(NSDictionary*)attributes
    {
      RWTPage *page = [[RWTPage alloc] init];
     
      if ([[attributes objectForKey:RWTPageAttributesKeyUtterances] isKindOfClass:[NSString class]]) {
        page.displayText = [attributes objectForKey:RWTPageAttributesKeyUtterances];
        page.backgroundImage = [attributes objectForKey:RWTPageAttributesKeyBackgroundImage];
        // 1
        page.utterances  = @[[[AVSpeechUtterance alloc] initWithString:page.displayText]];
      } else if ([[attributes objectForKey:RWTPageAttributesKeyUtterances] isKindOfClass:[NSArray class]]) {
        NSMutableArray *utterances = [NSMutableArray arrayWithCapacity:31];
        NSMutableString *displayText = [NSMutableString stringWithCapacity:101];
     
        for (NSDictionary *utteranceAttributes in [attributes objectForKey:RWTPageAttributesKeyUtterances]) {
          NSString *utteranceString =
                     [utteranceAttributes objectForKey:RWTUtteranceAttributesKeyUtteranceString];
          NSDictionary *utteranceProperties =
                         [utteranceAttributes objectForKey:RWTUtteranceAttributesKeyUtteranceProperties];
     
          AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:utteranceString];
          [utterance setValuesForKeysWithDictionary:utteranceProperties];
     
          if (utterance) {
            [utterances addObject:utterance];
            [displayText appendString:utteranceString];
          }
        }
     
        page.displayText = displayText;
        page.backgroundImage = [UIImage imageNamed:[attributes objectForKey:RWTPageAttributesKeyBackgroundImage]];
        // 2
        page.utterances  = [utterances copy];
      }
     
      return page;
    }

    The only new code is in sections 1 and 2, which set the page.utterances property for the NSString case and the same property for the NSArray case, respectively.

    Open RWTPageViewController.h and replace its contents below the header comments with

    #import <UIKit/UIKit.h>
    @import AVFoundation;
     
    // 1
    @interface RWTPageViewController : UIViewController<AVSpeechSynthesizerDelegate>
     
    @property (nonatomic, weak) IBOutlet UILabel *pageTextLabel;
    @property (nonatomic, weak) IBOutlet UIImageView *pageImageView;
     
    @end

    In Section 1, you declared that RWTPageViewController conforms to the AVSpeechSynthesizerDelegate protocol.

    Open RWTPageViewController.m and add the following property declaration just below the declaration of the synthesizer property

      @property (nonatomic, assign) NSUInteger nextSpeechIndex;

    You’ll use this new property to track which element of RWTPage.utterances to speak next.

    Replace setupForCurrentPage with

    - (void)setupForCurrentPage
    {
      self.pageTextLabel.text = [self currentPage].displayText;
      self.pageImageView.image = [self currentPage].backgroundImage;
      self.nextSpeechIndex = 0;
    }

    Replace speakNextUtterance with

    - (void)speakNextUtterance
    {
      // 1
      if (self.nextSpeechIndex < [[self currentPage].utterances count]) {
        // 2
        AVSpeechUtterance *utterance = [[self currentPage].utterances objectAtIndex:self.nextSpeechIndex];
        self.nextSpeechIndex    += 1;
     
        // 3
        [self.synthesizer speakUtterance:utterance];
      }
    }
    1. In Section 1, you’re ensuring that nextSpeechUtterance is in range.
    2. At Section 2 you’re getting the current utterance and advancing the index.
    3. Finally, in Section 3, you’re speaking the utterance.

    Build and run.What happens now? You should only hear “Whisky,” the first word, spoken on each page. That’s because you still need to implement some AVSpeechSynthesizerDelegate methods to queue up the next utterance for speech when the synthesizer finishes speaking the current utterance.

    Replace startSpeaking with

    - (void)startSpeaking
    {
      if (!self.synthesizer) {
        self.synthesizer = [[AVSpeechSynthesizer alloc] init];
        // 1
        self.synthesizer.delegate = self;
      }
     
      [self speakNextUtterance];
    }

    In Section 1, you’ve made your view controller a delegate of your synthesizer.

    Add the following code at the end of RWTPageViewController.m, just before the @end

     
    #pragma mark - AVSpeechSynthesizerDelegate Protocol
     
    - (void)speechSynthesizer:(AVSpeechSynthesizer*)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance*)utterance
    {
      NSUInteger indexOfUtterance = [[self currentPage].utterances indexOfObject:utterance];
      if (indexOfUtterance == NSNotFound) {
        return;
      }
     
      [self speakNextUtterance];
    }

    Your new code queues up the next utterance when the synthesizer finishes speaking the current utterance.

    Build and run. You’ll now hear a couple of differences:

    • You queue up the next utterance when the current utterance is spoken, so that every word on a page is verbalize.
    • When you swipe to the next or previous page, the current page’s text is no longer spoken.
    • Speech sounds much more natural, thanks to the utteranceProperties in Supporting Files\WhirlySquirrelly.plist. Your humble tutorial author toiled over these to hand-tune the speech.

    Control: You Must Learn Control

    Master Yoda was wise: control is important. Now that your book speaks each utterance individually, you’re going to add buttons to your UI so you can make real time adjustments to the pitch and rate of your synthesizer’s speech.

    Still in RWTPageViewController.m, add the following property declarations right after the declaration of the nextSpeechIndex property

    @property (nonatomic, assign) float currentPitchMultiplier;
    @property (nonatomic, assign) float currentRate;

    To set these new properties, add the following methods right after the body of gotoPreviousPage:

    - (void)lowerPitch
    {
      if (self.currentPitchMultiplier > 0.5f) {
        self.currentPitchMultiplier = MAX(self.currentPitchMultiplier * 0.8f, 0.5f);
      }
    }
     
    - (void)raisePitch
    {
      if (self.currentPitchMultiplier < 2.0f) {
        self.currentPitchMultiplier = MIN(self.currentPitchMultiplier * 1.2f, 2.0f);
      }
    }
     
    - (void)lowerRate
    {
      if (self.currentRate > AVSpeechUtteranceMinimumSpeechRate) {
        self.currentRate = MAX(self.currentRate * 0.8f, AVSpeechUtteranceMinimumSpeechRate);
      }
    }
     
    - (void)raiseRate
    {
      if (self.currentRate < AVSpeechUtteranceMaximumSpeechRate) {
        self.currentRate = MIN(self.currentRate * 1.2f, AVSpeechUtteranceMaximumSpeechRate);
      }
    }
     
    -(void) speakAgain
    {
        if (self.nextSpeechIndex == [[self currentPage].utterances count]) {
          self.nextSpeechIndex = 0;
          [self speakNextUtterance];
        }
    }

    These methods are the actions that connect to your speech control buttons.

    • lowerPitch: and raisePitch: lower and raise the speech pitch, respectively, by up to 20% for each invocation, within the range [0.5f, 2.0f].
    • lowerRate: and raiseRate" lower and raise the speech rate, respectively, by up to 20% for each invocation, within the range [AVSpeechUtteranceMinimumSpeechRate, AVSpeechUtteranceMaximumSpeechRate].
    • speakAgain: resets the internal index of the current spoken word, then repeats the message on the screen.

    Create the buttons by adding the following methods right after the body of raiseRate

    -(void) addSpeechControlWithFrame: (CGRect) frame title:(NSString *) title action:(SEL) selector {
      UIButton *controlButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
      controlButton.frame = frame;
      controlButton.backgroundColor = [UIColor colorWithWhite:0.9f alpha:1.0f];
      [controlButton setTitle:title forState:UIControlStateNormal];
      [controlButton addTarget:self
                     action:selector
           forControlEvents:UIControlEventTouchUpInside];
      [self.view addSubview:controlButton];
    }
     
    - (void)addSpeechControls
    {
      [self addSpeechControlWithFrame:CGRectMake(52, 485, 150, 50) 
                                title:@"Lower Pitch" 
                               action:@selector(lowerPitch)];
      [self addSpeechControlWithFrame:CGRectMake(222, 485, 150, 50) 
                                title:@"Raise Pitch" 
                               action:@selector(raisePitch)];
      [self addSpeechControlWithFrame:CGRectMake(422, 485, 150, 50) 
                                title:@"Lower Rate" 
                               action:@selector(lowerRate)];
      [self addSpeechControlWithFrame:CGRectMake(592, 485, 150, 50) 
                                title:@"Raise Rate" 
                               action:@selector(raiseRate)];
      [self addSpeechControlWithFrame:CGRectMake(506, 555, 150, 50) 
                                title:@"Speak Again" 
                               action:@selector(speakAgain)];
     
    }

    addSpeechControlWithFrame: is a convenience method to add buttons to the view that links each of them with methods to alter the spoken text on demand.

    Note: You could also create these buttons in Main.storyboard and wire up their actions in RWTPageViewController. But that would be too easy, and there is a more functional approach.

    Add the following code in viewDidLoad before [self startSpeaking]:

     
      // 1
      self.currentPitchMultiplier = 1.0f;
      self.currentRate = AVSpeechUtteranceDefaultSpeechRate;
     
      // 2
      [self addSpeechControls];

    Section 1 sets your new speech properties to default values, and section 2 adds your speech controls.

    As the last step, replace speakNextUtterance with the following

    - (void)speakNextUtterance
    {
      if (self.nextSpeechIndex < [[self currentPage].utterances count]) {
        AVSpeechUtterance *utterance = [[self currentPage].utterances objectAtIndex:self.nextSpeechIndex];
        self.nextSpeechIndex    += 1;
     
        // 1
        utterance.pitchMultiplier = self.currentPitchMultiplier;
        // 2
        utterance.rate = self.currentRate;
     
        [self.synthesizer speakUtterance:utterance];
      }
    }

    The new code sets the pitchMultiplier and rate of the next utterance to the values you set while clicking the nifty new lower/raise buttons.

    Build and run. You should see something like below.

    Narrated Book with Speech Control

    Try clicking or tapping the various buttons while it’s speaking, and take note of how it changes the sound of the speech. Yoda would be proud; you’re not a Jedi yet, but are becoming a master of AVSpeechSynthesizer.

    Where to Go From Here?

    Here is the final Completed Project for this tutorial.

    I encourage you to experiment with your narrated book app. If you want a few ideas for how to tweak your app, here are some:

    Compete for the Most Natural Sounding WhirlySquirrelly.plist

    Hint: Try your hand at fine tuning WhirlySquirrelly.plist and upload your most-natural-sounding version of the plist to the comments/forum. We’ll judge the winner and give him or her praise in the comments.

    Add the Ability for the User to Select Which Book They Read and Hear

    Hint: Add a “Choose Book” button to your UI that displays a UIPopoverController with a list of alternate books. Tapping on a book in this list should reset the book in your RWTPageViewController and present the new book.

    Add the Ability to Download New Books from a Webserver

    Hint: Store your books as plists on your own webserver or a service like AWS S3 or Heroku. Create a webservice call to list the urls of all available books and another webservice call to fetch a single book. Link this with the book, choosing functionality you added in the previous item above.

    Add Word Highlighting the Corresponds with the Speech

    Hint: Use the AVSpeechSynthesizerDelegate method speechSynthesizer:didStartSpeechUtterance: to highlight the passed utterance, and speechSynthesizer:didFinishSpeechUtterance: to unhighlight it. You should use the pageTextLabel.attributedText property to set the entire text, using an NSAttributedString to add different foreground color and font properties to the highlighted and unhighlighted sections of the text.

    Add the Ability to Display a Title Page Before All Other Pages

    Hint: Add another view controller before your RWTPageViewController. You’ll have to place both view controllers in a UINavigationController and update your Main.storyboard file to use the new view controllers. Alternately, you could redesign the Page class hierarchy into speakable and non-speakable pages, and modify RWTPageViewController to handle different page types appropriately.

    How to Make a Narrated Book Using AVSpeechSynthesizer in iOS 7 is a post from: Ray Wenderlich

    The post How to Make a Narrated Book Using AVSpeechSynthesizer in iOS 7 appeared first on Ray Wenderlich.


    Video Tutorial: Using LLDB in iOS Part 9: Using Quicklook

    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.

    Video Tutorial: Scroll View School: Introduction

    How to Make a Game Like Candy Crush: Part 2

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

    Learn to make a tasty match-3 game

    Welcome back to our how to make a game like Candy Crush tutorial series!

    This is the second part of a two-part series that teaches you how to make a match-3 game like Candy Crush Saga or Bejeweled. Your game is called Cookie Crunch Adventure and it’s delicious!

    In the first part of the tutorial, you loaded a level shape from a JSON file, placed cookie sprites on the screen and implemented the logic for detecting swipes and swapping cookies.

    In this second and final part, you’ll implement the rest of the game rules, add loads of animations and polish Cookie Crunch Adventure to a top-10-quality shine. I’m getting hungry just thinking about it!

    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. You also need a copy of the resources zip (this is the same file from Part One).

    Let’s crunch some cookies!

    Getting Started

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

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

    This is the sequence of events:

    Game flow

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

    Finding the Chains

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

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

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

    Replace the contents of RWTChain.h with this:

    @class RWTCookie;
     
    typedef NS_ENUM(NSUInteger, ChainType) {
      ChainTypeHorizontal,
      ChainTypeVertical,
    };
     
    @interface RWTChain : NSObject
     
    @property (strong, nonatomic, readonly) NSArray *cookies;
     
    @property (assign, nonatomic) ChainType chainType;
     
    - (void)addCookie:(RWTCookie *)cookie;
     
    @end

    A chain has a list of cookie objects and a type: It’s either horizontal (a row of cookies) or vertical (a column). If you feel adventurous, you can also add more complex chain types, such as L- and T-shapes.

    It will be pretty simple to implement this class. Open RWTChain.m and replace the contents with the following:

    #import "RWTChain.h"
     
    @implementation RWTChain {
      NSMutableArray *_cookies;
    }
     
    - (void)addCookie:(RWTCookie *)cookie {
      if (_cookies == nil) {
        _cookies = [NSMutableArray array];
      }
      [_cookies addObject:cookie];
    }
     
    - (NSArray *)cookies {
      return _cookies;
    }
     
    - (NSString *)description {
      return [NSString stringWithFormat:@"type:%ld cookies:%@", (long)self.chainType, self.cookies];
    }
     
    @end

    Notice how the header declares the cookies property as an NSArray but the implementation declares the backing instance variable as an NSMutableArray. That’s a common trick when you don’t want users of a class to modify the array; they can only read from it.

    Also, there is a reason you’re using an array here to store the cookie objects and not an NSSet: 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.

    To start putting these new chain objects to good use, open RWTLevel.h and add the following import:

    #import "RWTChain.h"

    Next, add a public method declaration:

    - (NSSet *)removeMatches;

    Switch over to the implementation in RWTLevel.m. Before you get to removeMatches, you need a couple of helper methods to do the heavy lifting of finding chains.

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

    Finding chains

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

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

    Add this method to RWTLevel.m to scan for horizontal cookie matches:

    - (NSSet *)detectHorizontalMatches {
      // 1
      NSMutableSet *set = [NSMutableSet set];
     
      // 2
      for (NSInteger row = 0; row < NumRows; row++) {
        for (NSInteger column = 0; column < NumColumns - 2; ) {
     
          // 3
          if (_cookies[column][row] != nil) {
            NSUInteger matchType = _cookies[column][row].cookieType;
     
            // 4
            if (_cookies[column + 1][row].cookieType == matchType
             && _cookies[column + 2][row].cookieType == matchType) {
              // 5
              RWTChain *chain = [[RWTChain alloc] init];
              chain.chainType = ChainTypeHorizontal;
              do {
                [chain addCookie:_cookies[column][row]];
                column += 1;
              }
              while (column < NumColumns && _cookies[column][row].cookieType == matchType);
     
              [set addObject:chain];
              continue;
            }
          }
     
          // 6
          column += 1;
        }
      }
      return set;
    }

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

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

    Note: If there’s a gap in the grid, it comes up as a cookieType of 0, which will never match a real cookie. So the logic above also works if you wanted to make a level with empty squares. Neat!

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

    - (NSSet *)detectVerticalMatches {
      NSMutableSet *set = [NSMutableSet set];
     
      for (NSInteger column = 0; column < NumColumns; column++) {
        for (NSInteger row = 0; row < NumRows - 2; ) {
          if (_cookies[column][row] != nil) {
            NSUInteger matchType = _cookies[column][row].cookieType;
     
            if (_cookies[column][row + 1].cookieType == matchType
             && _cookies[column][row + 2].cookieType == matchType) {
     
              RWTChain *chain = [[RWTChain alloc] init];
              chain.chainType = ChainTypeVertical;
              do {
                [chain addCookie:_cookies[column][row]];
                row += 1;
              }
              while (row < NumRows && _cookies[column][row].cookieType == matchType);
     
              [set addObject:chain];
              continue;
            }
          }
          row += 1;
        }
      }
      return set;
    }

    The vertical version has the same kind of logic, but loops by column in the outer for 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:

    - (NSSet *)removeMatches {
      NSSet *horizontalChains = [self detectHorizontalMatches];
      NSSet *verticalChains = [self detectVerticalMatches];
     
      NSLog(@"Horizontal matches: %@", horizontalChains);
      NSLog(@"Vertical matches: %@", verticalChains);
     
      return [horizontalChains setByAddingObjectsFromSet: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 RWTViewController.m. Add this helper method:

    - (void)handleMatches {
      NSSet *chains = [self.level removeMatches];
      // TODO: do something with the set
    }

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

    In the swipe handler block in viewDidLoad, change the call to animateSwap:completion: to this:

    [self.scene animateSwap:swap completion:^{
      [self handleMatches];
    }];

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

    List of matches

    Removing the Chains

    RWTLevel’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—that is, remove the RWTCookie objects from the array for the 2-D grid. When that’s done, you can tell RWTMyScene to animate the sprites for these cookies out of existence.

    eatcookies

    Removing the cookies from the model is simple enough. Add the following method to RWTLevel.m:

    - (void)removeCookies:(NSSet *)chains {
      for (RWTChain *chain in chains) {
        for (RWTCookie *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 RWTChain object is the only owner of the RWTCookie object. When the chain gets deallocated, so will these cookie objects.

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

    [self removeCookies:horizontalChains];
    [self removeCookies:verticalChains];

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

    - (void)animateMatchedCookies:(NSSet *)chains completion:(dispatch_block_t)completion {
     
      for (RWTChain *chain in chains) {
        for (RWTCookie *cookie in chain.cookies) {
     
          // 1
          if (cookie.sprite != nil) {
     
            // 2
            SKAction *scaleAction = [SKAction scaleTo:0.1 duration:0.3];
            scaleAction.timingMode = SKActionTimingEaseOut;
            [cookie.sprite runAction:[SKAction sequence:@[scaleAction, [SKAction removeFromParent]]]];
     
            // 3
            cookie.sprite = nil;
          }
        }
      }
     
      [self runAction:self.matchSound];
     
      // 4
      [self runAction:[SKAction sequence:@[
        [SKAction waitForDuration:0.3],
        [SKAction runBlock:completion]
        ]]];
    }

    This loops through all the chains and all the cookies in each chain, and then triggers the animations. Here’s how it all works, step by step:

    1. The same RWTCookie could be part of two chains (one horizontal and one vertical), but you only want to add one animation to the sprite. This check ensures that you only animate the sprite once.
    2. You put a scaling animation on the cookie sprite to shrink its size. When the animation is done, you remove the sprite from the cookie layer.
    3. You remove the link between the RWTCookie and its sprite as soon as you’ve added the animation. This simple trick prevents the situation described in point 1.
    4. You only continue with the rest of the game after the animations finish.

    Open RWTMyScene.h and add the method signature:

    - (void)animateMatchedCookies:(NSSet *)chains completion:(dispatch_block_t)completion;

    Open RWTViewController.m and change handleMatches to call this new animation:

    - (void)handleMatches {
      NSSet *chains = [self.level removeMatches];
     
      [self.scene animateMatchedCookies:chains completion:^{
        self.view.userInteractionEnabled = YES;
      }];
    }

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

    Match animation

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

    Dropping Cookies Into Empty Tiles

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

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

    Add this method signature to RWTLevel.h:

    - (NSArray *)fillHoles;

    Add the implementation of fillHoles to RWTLevel.m:

    - (NSArray *)fillHoles {
      NSMutableArray *columns = [NSMutableArray array];
     
      // 1
      for (NSInteger column = 0; column < NumColumns; column++) {
     
        NSMutableArray *array;
        for (NSInteger row = 0; row < NumRows; row++) {
     
          // 2
          if (_tiles[column][row] != nil && _cookies[column][row] == nil) {
     
            // 3
            for (NSInteger lookup = row + 1; lookup < NumRows; lookup++) {
              RWTCookie *cookie = _cookies[column][lookup];
              if (cookie != nil) {
                // 4
                _cookies[column][lookup] = nil;
                _cookies[column][row] = cookie;
                cookie.row = row;
     
                // 5
                if (array == nil) {
                  array = [NSMutableArray array];
                  [columns addObject:array];
                }
                [array addObject:cookie];
     
                // 6
                break;
              }
            }
          }
        }
      }
      return columns;
    }

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

    Filling holes

    Here is how it all works, step by step:

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

    At the end, the method returns an array containing all the cookies that have been moved down, organized by column. You’ve already updated the data model for these cookies with the new positions, but the sprites need to catch up. RWTMyScene will animate the sprites and RWTViewController is the in-between object to coordinate between the the model (RWTLevel ) and the view (RWTMyScene).

    Open RWTMyScene.h and add this method signature:

    - (void)animateFallingCookies:(NSArray *)columns completion:(dispatch_block_t)completion;

    Switch to RWTMyScene.m and add the method implementation:

    - (void)animateFallingCookies:(NSArray *)columns completion:(dispatch_block_t)completion {
      // 1
      __block NSTimeInterval longestDuration = 0;
     
      for (NSArray *array in columns) {
        [array enumerateObjectsUsingBlock:^(RWTCookie *cookie, NSUInteger idx, BOOL *stop) {
          CGPoint newPosition = [self pointForColumn:cookie.column row:cookie.row];
     
          // 2
          NSTimeInterval delay = 0.05 + 0.15*idx;
     
          // 3
          NSTimeInterval duration = ((cookie.sprite.position.y - newPosition.y) / TileHeight) * 0.1;
     
          // 4
          longestDuration = MAX(longestDuration, duration + delay);
     
          // 5
          SKAction *moveAction = [SKAction moveTo:newPosition duration:duration];
          moveAction.timingMode = SKActionTimingEaseOut;
          [cookie.sprite runAction:[SKAction sequence:@[
            [SKAction waitForDuration:delay],
            [SKAction group:@[moveAction, self.fallingCookieSound]]]]];
        }];
      }
     
      // 6
      [self runAction:[SKAction sequence:@[
        [SKAction waitForDuration:longestDuration],
        [SKAction runBlock:completion]
        ]]];
    }

    Here’s how this works:

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

    Now you can tie it all together. Open RWTViewController.m. Replace the contents of handleMatches with the following:

    NSSet *chains = [self.level removeMatches];
    [self.scene animateMatchedCookies:chains completion:^{
      NSArray *columns = [self.level fillHoles];
      [self.scene animateFallingCookies:columns completion:^{
        self.view.userInteractionEnabled = YES;
      }];
    }];

    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.

    Try it out!

    Falling animation

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

    Adding New Cookies

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

    Holes at top

    You need to top up these columns with new cookies. Add a new method declaration to RWTLevel.h:

    - (NSArray *)topUpCookies;

    And add the implementation to RWTLevel.m:

    - (NSArray *)topUpCookies {
      NSMutableArray *columns = [NSMutableArray array];
     
      NSUInteger cookieType = 0;
     
      for (NSInteger column = 0; column < NumColumns; column++) {
     
        NSMutableArray *array;
     
        // 1
        for (NSInteger row = NumRows - 1; row >= 0 && _cookies[column][row] == nil; row--) {
     
          // 2
          if (_tiles[column][row] != nil) {
     
            // 3
            NSUInteger newCookieType;
            do {
              newCookieType = arc4random_uniform(NumCookieTypes) + 1;
            } while (newCookieType == cookieType);
            cookieType = newCookieType;
     
            // 4
            RWTCookie *cookie = [self createCookieAtColumn:column row:row withType:cookieType];
     
            // 5
            if (array == nil) {
              array = [NSMutableArray array];
              [columns addObject:array];
            }
            [array addObject:cookie];
          }
        }
      }
      return columns;
    }

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

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

    Here’s how it works, step by step:

    1. You loop through the column from top to bottom. This for loop ends when _cookies[column][row] is not nil—that is, when it has found a cookie.
    2. You ignore gaps in the level, because you only need to fill up grid squares that have a tile.
    3. You randomly create a new cookie type. It can’t be equal to the type of the last new cookie, to prevent too many “freebie” matches.
    4. You create the new RWTCookie object. This uses the createCookieAtColumn:row:withType: method that you added in Part One.
    5. You add the cookie to the array for this column. You’re lazily creating the arrays, so the allocation only happens if a column has holes.

    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.

    Open RWTMyScene.h and add the method signature for the new animation method:

    - (void)animateNewCookies:(NSArray *)columns completion:(dispatch_block_t)completion;

    Switch to RWTMyScene.m and add the implementation:

    - (void)animateNewCookies:(NSArray *)columns completion:(dispatch_block_t)completion {
      // 1
      __block NSTimeInterval longestDuration = 0;
     
      for (NSArray *array in columns) {
     
        // 2
        NSInteger startRow = ((RWTCookie *)[array firstObject]).row + 1;
     
        [array enumerateObjectsUsingBlock:^(RWTCookie *cookie, NSUInteger idx, BOOL *stop) {
     
          // 3
          SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:[cookie spriteName]];
          sprite.position = [self pointForColumn:cookie.column row:startRow];
          [self.cookiesLayer addChild:sprite];
          cookie.sprite = sprite;
     
          // 4
          NSTimeInterval delay = 0.1 + 0.2*([array count] - idx - 1);
     
          // 5
          NSTimeInterval duration = (startRow - cookie.row) * 0.1;
          longestDuration = MAX(longestDuration, duration + delay);
     
          // 6
          CGPoint newPosition = [self pointForColumn:cookie.column row:cookie.row];
          SKAction *moveAction = [SKAction moveTo:newPosition duration:duration];
          moveAction.timingMode = SKActionTimingEaseOut;
          cookie.sprite.alpha = 0;
          [cookie.sprite runAction:[SKAction sequence:@[
            [SKAction waitForDuration:delay],
            [SKAction group:@[
              [SKAction fadeInWithDuration:0.05], moveAction, self.addCookieSound]]]]];
        }];
      }
     
      // 7
      [self runAction:[SKAction sequence:@[
        [SKAction waitForDuration:longestDuration],
        [SKAction runBlock:completion]
        ]]];
    }

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

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

    Finally, in RWTViewController.m, replace the chain of completion blocks in handleMatches with the following:

    [self.scene animateMatchedCookies:chains completion:^{
      NSArray *columns = [self.level fillHoles];
      [self.scene animateFallingCookies:columns completion:^{
        NSArray *columns = [self.level topUpCookies];
        [self.scene animateNewCookies:columns completion:^{
          self.view.userInteractionEnabled = YES;
        }];
      }];
    }];

    Try it out!

    Adding new cookies

    A Cascade of Cookies

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

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

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

    In RWTViewController.m, 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) {
      [self 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:

    - (void)beginNextTurn {
      self.view.userInteractionEnabled = YES;
    }

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

    Cascade

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

    Solution Inside: Solution SelectShow>

    The logic for this sits in RWTLevel.m, in detectPossibleSwaps, but this method is not public yet, so add its method signature to RWTLevel.h:

    - (void)detectPossibleSwaps;

    And call it from beginNextTurn in RWTViewController.m:

    - (void)beginNextTurn {
      [self.level detectPossibleSwaps];
      self.view.userInteractionEnabled = YES;
    }

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

    Scoring Points

    In Cookie Crunch Adventure, the player’s objective is to score a certain number of points within a maximum number of swaps. Both of these values come from the JSON level file. The game should show these numbers on the screen so the player knows how well she’s doing.

    First, add the following properties to RWTViewController.m, in the class extension at the top:

    @property (assign, nonatomic) NSUInteger movesLeft;
    @property (assign, nonatomic) NSUInteger score;
     
    @property (weak, nonatomic) IBOutlet UILabel *targetLabel;
    @property (weak, nonatomic) IBOutlet UILabel *movesLabel;
    @property (weak, nonatomic) IBOutlet UILabel *scoreLabel;

    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:

    View controller with labels

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

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

    Because the target score and the maximum number of moves are stored in the JSON level file, you should load them into RWTLevel. Add the following to RWTLevel.h, under the @interface line:

    @property (assign, nonatomic) NSUInteger targetScore;
    @property (assign, nonatomic) NSUInteger maximumMoves;

    These properties will store the values from the JSON data.

    In RWTLevel.m, add these two lines to the bottom of the if block in initWithFile::

    self.targetScore = [dictionary[@"targetScore"] unsignedIntegerValue];
    self.maximumMoves = [dictionary[@"moves"] unsignedIntegerValue];

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

    Back in RWTViewController.m, add the following method:

    - (void)updateLabels {
      self.targetLabel.text = [NSString stringWithFormat:@"%lu", (long)self.level.targetScore];
      self.movesLabel.text = [NSString stringWithFormat:@"%lu", (long)self.movesLeft];
      self.scoreLabel.text = [NSString stringWithFormat:@"%lu", (long)self.score];
    }

    You’ll call this method after every turn to update the text inside the labels. You use the %lu format specifier and cast to (long) to make this code work the same way on 32-bit and 64-bit systems.

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

    self.movesLeft = self.level.maximumMoves;
    self.score = 0;
    [self updateLabels];

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

    Game with labels

    The scoring rules are simple:

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

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

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

    Add the following to RWTChain.h:

    @property (assign, nonatomic) NSUInteger score;

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

    - (void)calculateScores:(NSSet *)chains {
      for (RWTChain *chain in chains) {
        chain.score = 60 * ([chain.cookies count] - 2);
      }
    }

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

    [self calculateScores:horizontalChains];
    [self 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 RWTChain objects, you can update the player’s score and display it onscreen.

    This happens in RWTViewController.m. Inside handleMatches, just before the call to fillHoles, add the following lines:

    for (RWTChain *chain in chains) {
      self.score += chain.score;
    }
    [self updateLabels];

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

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

    Score

    Animating Point Values

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

    - (void)animateScoreForChain:(RWTChain *)chain {
      // Figure out what the midpoint of the chain is.
      RWTCookie *firstCookie = [chain.cookies firstObject];
      RWTCookie *lastCookie = [chain.cookies lastObject];
      CGPoint centerPosition = CGPointMake(
        (firstCookie.sprite.position.x + lastCookie.sprite.position.x)/2,
        (firstCookie.sprite.position.y + lastCookie.sprite.position.y)/2 - 8);
     
      // Add a label for the score that slowly floats up.
      SKLabelNode *scoreLabel = [SKLabelNode labelNodeWithFontNamed:@"GillSans-BoldItalic"];
      scoreLabel.fontSize = 16;
      scoreLabel.text = [NSString stringWithFormat:@"%lu", (long)chain.score];
      scoreLabel.position = centerPosition;
      scoreLabel.zPosition = 300;
      [self.cookiesLayer addChild:scoreLabel];
     
      SKAction *moveAction = [SKAction moveBy:CGVectorMake(0, 3) duration:0.7];
      moveAction.timingMode = SKActionTimingEaseOut;
      [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:completion:, in between the two for loops:

    for (RWTChain *chain in chains) {
     
      // Add this line:
      [self animateScoreForChain:chain];
     
      for (RWTCookie *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.

    RWTMyScene already has a method for that, preloadResources, so add the following line to it:

    [SKLabelNode labelNodeWithFontNamed:@"GillSans-BoldItalic"];

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

    Floating score

    Combos!

    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 her 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 RWTLevel.m, add the following private property:

    @property (assign, nonatomic) NSUInteger comboMultiplier;

    Update calculateScores: to:

    - (void)calculateScores:(NSSet *)chains {
      for (RWTChain *chain in chains) {
        chain.score = 60 * ([chain.cookies count] - 2) * self.comboMultiplier;
        self.comboMultiplier++;
      }
    }

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

    - (void)resetComboMultiplier {
      self.comboMultiplier = 1;
    }

    And add its signature to RWTLevel.h:

    - (void)resetComboMultiplier;

    Open RWTViewController.m and find beginGame. Add this line just before the call to shuffle:

    [self.level resetComboMultiplier];

    Add the same line at the top of beginNextTurn:

    [self.level resetComboMultiplier];

    And now you have combos. Try it out!

    Combo

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

    Solution Inside: Solution SelectShow>

    Winning and Losing

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

    Create a new method in RWTViewController.m:

    - (void)decrementMoves{
      self.movesLeft--;
      [self 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:

    [self decrementMoves];

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

    Moves

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

    First, though, the storyboard needs some work.

    The Look of Victory or Defeat

    Open Main.storyboard and drag an image view into the view. Make it 320×150 points and center it vertically.

    Image view in storyboard

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

    Make sure to disable Auto Layout in the File inspector, the first tab on the right. Then go to the Size inspector and make the Autosizing mask for the image view look like this:

    Autosizing mask image view

    This will keep the image centered regardless of the screen size.

    Now connect this image view to a new outlet on RWTViewController.m named gameOverPanel.

    @property (weak, nonatomic) IBOutlet UIImageView *gameOverPanel;

    Also, add a private property for a gesture recognizer:

    @property (strong, nonatomic) UITapGestureRecognizer *tapGestureRecognizer;

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

    self.gameOverPanel.hidden = YES;

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

    - (void)showGameOver {
      self.gameOverPanel.hidden = NO;
      self.scene.userInteractionEnabled = NO;
     
      self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideGameOver)];
      [self.view addGestureRecognizer:self.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:

    - (void)hideGameOver {
      [self.view removeGestureRecognizer:self.tapGestureRecognizer];
      self.tapGestureRecognizer = nil;
     
      self.gameOverPanel.hidden = YES;
      self.scene.userInteractionEnabled = YES;
     
      [self 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 (self.score >= self.level.targetScore) {
      self.gameOverPanel.image = [UIImage imageNamed:@"LevelComplete"];
      [self showGameOver];
    } else if (self.movesLeft == 0) {
      self.gameOverPanel.image = [UIImage imageNamed:@"GameOver"];
      [self showGameOver];
    }

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

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

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

    Level complete

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

    Animating the Transitions

    It looks a bit messy with this banner on top of all those cookies, so let’s add a little animation here as well. Add these two methods to RWTMyScene.m:

    - (void)animateGameOver {
      SKAction *action = [SKAction moveBy:CGVectorMake(0, -self.size.height) duration:0.3];
      action.timingMode = SKActionTimingEaseIn;
      [self.gameLayer runAction:action];
    }
     
    - (void)animateBeginGame {
      self.gameLayer.hidden = NO;
     
      self.gameLayer.position = CGPointMake(0, self.size.height);
      SKAction *action = [SKAction moveBy:CGVectorMake(0, -self.size.height) duration:0.3];
      action.timingMode = SKActionTimingEaseOut;
      [self.gameLayer runAction:action];
    }

    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 RWTMyScene.m in initWithSize:, immediately after you create the gameLayer node:

    self.gameLayer.hidden = YES;

    You’re also going to call these methods from RWTViewController, so open RWTMyScene.h and add the method signatures there:

    - (void)animateGameOver;
    - (void)animateBeginGame;

    Now open RWTViewController.m and call animateGameOver as the first thing in showGameOver:

    [self.scene animateGameOver];

    Finally, in RWTViewController.m’s beginGame, just before the call to shuffle, call animateBeginGame:

    [self.scene animateBeginGame];

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

    Too many cookies

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

    Add this new method to RWTMyScene.m to perform the cleanup:

    - (void)removeAllCookieSprites {
      [self.cookiesLayer removeAllChildren];
    }

    Declare it in RWTMyScene.h as well:

    - (void)removeAllCookieSprites;

    And call it as the very first thing from shuffle inside RWTViewController.m:

    [self.scene removeAllCookieSprites];

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

    Manual Shuffling

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

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

    shufflecomic

    Add an outlet property in RWTViewController.m:

    @property (weak, nonatomic) IBOutlet UIButton *shuffleButton;

    And add an action method:

    - (IBAction)shuffleButtonPressed:(id)sender {
      [self shuffle];
      [self decrementMoves];
    }

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

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

    self.shuffleButton.hidden = YES;

    And in hideGameOver, put it back on the screen again:

    self.shuffleButton.hidden = NO;

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

    Shuffle button storyboard

    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.

    Set the autosizing to make this button stick to the bottom of the screen so it will also work on 3.5-inch phones:

    Autosizing shuffle button

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

    Try it out!

    Shuffle button in the game

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

    The shuffle is a bit abrupt, so let’s make the new cookies appear with a cute animation. In RWTMyScene.m, go to addSpritesForCookies: and add the following lines inside the for loop, after the existing code:

    cookie.sprite.alpha = 0;
    cookie.sprite.xScale = cookie.sprite.yScale = 0.5;
     
    [cookie.sprite runAction:[SKAction sequence:@[
      [SKAction waitForDuration:0.25 withRange:0.5],
      [SKAction group:@[
        [SKAction fadeInWithDuration:0.25],
        [SKAction scaleTo:1.0 duration:0.25]
      ]]]]];

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

    Shuffle animation

    Bring on the Muzak

    Let’s give the player some smooth, relaxing music to listen to while she crunches cookies. Add this line to the top of RWTViewController.m:

    @import AVFoundation;

    This uses the new @import syntax to include the AVFoundation framework. The main advantage of @import over #import is that you don’t need to add the framework to your project separately; Xcode will add it for you automatically.

    Add the following private property:

    @property (strong, nonatomic) AVAudioPlayer *backgroundMusic;

    Add these lines to viewDidLoad, just before beginGame:

    NSURL *url = [[NSBundle mainBundle] URLForResource:@"Mining by Moonlight" withExtension:@"mp3"];
    self.backgroundMusic = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
    self.backgroundMusic.numberOfLoops = -1;
    [self.backgroundMusic play];

    This loads the background music MP3 and sets it to loop forever. That gives the game a whole lot more swing!

    Drawing Better Tiles

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

    Border comparison

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

    Masked sprite comparison

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

    In RWTMyScene.m, add two new private properties:

    @property (strong, nonatomic) SKCropNode *cropLayer;
    @property (strong, nonatomic) SKNode *maskLayer;

    In initWithSize:, add these lines below the code that creates the tilesLayer:

    self.cropLayer = [SKCropNode node];
    [self.gameLayer addChild:self.cropLayer];
     
    self.maskLayer = [SKNode node];
    self.maskLayer.position = layerPosition;
    self.cropLayer.maskNode = self.maskLayer;

    This creates 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:

    [self.gameLayer addChild:self.cookiesLayer];

    With this:

    [self.cropLayer addChild:self.cookiesLayer];

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

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

    • Replace @"Tile" with @"MaskTile"
    • Replace self.tilesLayer with self.maskLayer

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

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

    Cookie is cropped

    Tip: If you want to see what the mask layer looks like, add this line to initWithSize:
    [self.cropLayer addChild:self.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 (NSInteger row = 0; row <= NumRows; row++) {
      for (NSInteger column = 0; column <= NumColumns; column++) {
     
        BOOL topLeft     = (column > 0) && (row < NumRows)
                                        && [self.level tileAtColumn:column - 1 row:row];
     
        BOOL bottomLeft  = (column > 0) && (row > 0)
                                        && [self.level tileAtColumn:column - 1 row:row - 1];
     
        BOOL topRight    = (column < NumColumns) && (row < NumRows)
                                                 && [self.level tileAtColumn:column row:row];
     
        BOOL bottomRight = (column < NumColumns) && (row > 0)
                                                 && [self.level tileAtColumn:column row:row - 1];
     
        // The tiles are named from 0 to 15, according to the bitmask that is
        // made by combining these four values.
        NSUInteger value = topLeft | topRight << 1 | bottomLeft << 2 | bottomRight << 3;
     
        // Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn.
        if (value != 0 && value != 6 && value != 9) {
          NSString *name = [NSString stringWithFormat:@"Tile_%lu", (long)value];
          SKSpriteNode *tileNode = [SKSpriteNode spriteNodeWithImageNamed:name];
          CGPoint point = [self pointForColumn:column row:row];
          point.x -= TileWidth/2;
          point.y -= TileHeight/2;
          tileNode.position = point;
          [self.tilesLayer addChild:tileNode];
        }
      }
    }

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

    Solution Inside: Solution SelectShow>

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

    Final game

    Where to Go From Here?

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

    You can download the final Xcode project here.

    Here are ideas for other features you could add:

    • Special cookies when the player matches a certain shape. For example, Candy Crush Saga gives you a cookie that can clear an entire row when you match a 4-in-a-row chain.
    • Detection of special chains, such as L- or T-shapes, that reward the player with bonus points or special power-ups.
    • Boosts, or power-ups the player can use any time she wants. For example, one boost might remove all the cookies of one type from the screen at once.
    • Jelly levels: On these levels, some tiles are covered in jelly. You have X moves to remove all the jelly. This is where the RWTTile 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 NO to remove the jelly.
    • Hints: If the player doesn’t make a move for two seconds, light up a pair of cookies that make a valid swap.
    • Automatically move the player to the next level if she completes the current one.
    • Shuffle the cookies automatically if there are no possible moves.

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

    Credits: Artwork by Vicki Wenderlich. 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.

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

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

    Video Tutorial: Scroll View School Part 1: Getting Started

    Viewing all 4374 articles
    Browse latest View live


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