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

How to Make a Line Drawing Game with Sprite Kit

$
0
0
Learn how to make a line drawing game like Flight Control!

Learn how to make a line drawing game like Flight Control!

In 2009, Firemint introduced the line drawing game to the world when they released the incredibly popular Flight Control.

In a line drawing game, you trace a line with your finger and then sprites follow the line that you drew.

In this tutorial, you’ll learn how to write your own line drawing game with Sprite Kit. Instead of being a mere clone of Flight Control, you will create a game called “Hogville” where your goal is to bring some cute and tired pigs to food and shelter.

This tutorial assumes you have some experience with Sprite Kit. While you don’t have to be an expert, you should know the basics, like how to create sprites and run actions on them. If you’ve got a big question mark in place of that knowledge, take some time to work through Ray’s Sprite Kit Tutorial for Beginners before proceeding.

Getting Started

To get started, download the starter project.

I created this starter project using the Sprite Kit Game template and set it to run in landscape mode. I also added all the artwork you’ll need – and a big thanks to Vicki Wenderlich for providing the art!

Build and run, and you should see a blank screen in landscape as a starting point:

StarterProject

Now you can get right to the business of adding your game elements and developing the line drawing gameplay.

Adding the Background… and a Pig!

After a long day of being a pig, all you want is some food and a bed—a pile of hay will do! It’s hard work rolling in the mud all day. In Hogville, it will be your player’s job to give the pigs what they want by drawing the lines to steer them home.

Before you start drawing lines, though, you need a pig to follow them. Your pig would be a bit unhappy floating in a black void, so you’ll also add a background to give the poor pig some familiar surroundings.

Open MyScene.m and find initWithSize:. Inside the if statement, add the following code:

SKSpriteNode *bg = [SKSpriteNode spriteNodeWithImageNamed:@"bg_2_grassy"];
bg.anchorPoint = CGPointZero;
[self addChild:bg];
 
SKSpriteNode *pig = [SKSpriteNode spriteNodeWithImageNamed:@"pig_1"];
pig.position = CGPointMake(self.size.width / 2.0f, self.size.height / 2.0f);
[self addChild:pig];

This adds the background image and a pig sprite. You place the lower-left corner of the background image in the lower-left corner of the scene by setting bg‘s anchorPoint to (0, 0) and using its default position.

Build and run your game and behold your plump, rosy pig in the middle of a sunny, green field.

First_Run

Moving the Sprite

Next you need to create a class for the pig sprite. This will contain the path which the pig should follow, along with methods to make the pig follow this path over time.

To create a class for the pig sprite, go to File\New\File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next. Name the class Pig, make it a subclass of SKSpriteNode, click Next and then Create.

Open Pig.h and add the following two method declarations to the interface:

- (void)addPointToMove:(CGPoint)point; 
- (void)move:(NSNumber *)dt;

Now open Pig.m and add the following variable before the @implemenation section:

static const int POINTS_PER_SEC = 80;

This constant defines the speed of the pig as 80 points per second.

Next, declare two instance variables by adding the following code immediately after the @implementation line:

{
  NSMutableArray *_wayPoints;
  CGPoint _velocity;
}

_wayPoints will do what its name suggests and store all the points along which the pig should move. _velocity will store the pig’s current speed and direction.

Next, implement initWithImageNamed: and initialize _waypoints inside it:

- (instancetype)initWithImageNamed:(NSString *)name {
  if(self = [super initWithImageNamed:name]) {
    _wayPoints = [NSMutableArray array];       
  }
 
  return self;
}

Now that you’ve initialized _wayPoints, you need a method to add waypoints to it. Implement addPointToMove: by adding the following code to Pig.m:

- (void)addPointToMove:(CGPoint)point {
  [_wayPoints addObject:[NSValue valueWithCGPoint:point]];
}

This method simply adds the given point to the _wayPoints array. In order to store a CGPoint in an NSArray, you use NSValue‘s valueWithCGPoint method to store the CGPoint in an object.

Now begin implementing move: by adding the following code to Pig.m:

- (void)move:(NSNumber *)dt {
  CGPoint currentPosition = self.position;
  CGPoint newPosition;
 
  //1
  if([_wayPoints count] > 0) {
    CGPoint targetPoint = [[_wayPoints firstObject] CGPointValue];
 
    //2 TODO: Add movement logic here
 
    //3
    if(CGRectContainsPoint(self.frame, targetPoint)) {
      [_wayPoints removeObjectAtIndex:0];
    }
  }
}

You will call this method each frame to move the pig a little bit along its path. Here’s how this part of the method works:

  1. First you check to ensure there are waypoints left in the array. For the moment, the pig stops moving when it reaches the final point of the path. Later, you’ll make the pig a little smarter so it continues walking in its last direction even when no waypoints remain.
  2. This comment marks where you’ll put the code that updates the pig’s position. You’ll add that code next.
  3. Finally, you check if the pig has reached the waypoint by seeing if the pig’s frame contains the targetPoint. In this case, you remove the point from the array so that your next call to move: will use the next point. Note that it’s important to check if the frame contains the target point (rather than checking if the position equals the target point), effectively stopping the pig when he’s “close enough”. That makes some of the calculations later a bit easier.

You added that final if statement in the above code because the pig isn’t guaranteed to reach the waypoint in just one call to move:. That makes sense, because the pig needs to move at a constant speed, a little each frame.

Why? Let’s assume you have the first waypoint in the upper-left corner at (0, 50) and the second point at (300, 50). Something like this can happen if the player moves their finger very fast over the screen.

If you took the simple approach of setting the position to the first point in the array and then to the second point in the array, your pig would appear to teleport from one waypoint to the next. Have you ever seen a teleporting pig? I’m sure even Captain Kirk can’t make that claim.

Beam me up, piggy!

With the logic to process the waypoints in place, it’s time to add the code that calculates and updates the pig’s new position along the path between the waypoints. In move:, replace the //2 TODO: Add movement logic here comment with the following code:

//1
CGPoint offset = CGPointMake(targetPoint.x - currentPosition.x, targetPoint.y - currentPosition.y);
CGFloat length = sqrtf(offset.x * offset.x + offset.y * offset.y);
CGPoint direction = CGPointMake(offset.x / length, offset.y / length);
_velocity = CGPointMake(direction.x * POINTS_PER_SEC, direction.y * POINTS_PER_SEC);
 
//2
newPosition = CGPointMake(currentPosition.x + _velocity.x * [dt doubleValue], 
                          currentPosition.y + _velocity.y * [dt doubleValue]);
self.position = newPosition;

Here’s what you’re doing with the code you just added:

  1. You calculate a vector that points in the direction the pig should travel and has a length representing the distance the pig should move in dt seconds.

    To calculate the vector, first you find the difference between the pig’s current location and the next waypoint and store it as offset, a CGPoint representing the differences in both the x and y directions.

    As you can see in the following image, the distance between the two points is the length of the hypotenuse of the right triangle formed between the pig’s current position and the waypoint.

    velocity

    You divide offset‘s components by length to create a normalized vector (a vector of length 1) that points in the direction of the waypoint and you store it in direction.

    Finally, you multiply direction by POINTS_PER_SEC and store it in _velocity, which now represents a vector pointing in the direction the pig should travel, with a length that is the distance the pig should travel in one second.

  2. You calculate the pig’s new position by multiplying _velocity by dt and adding the result to the pig’s current position. Because _velocity stores the distance the pig should travel in one second and dt holds the number of seconds that have passed since the last call to move:, multiplying the two results in the distance the pig should travel in dt seconds.

You’re done here for the moment. It’s time to use your new class and move the pig.

Responding to Touches

Open MyScene.m and add the following import so the scene can access your new class:

#import "Pig.h"

Find this line in initWithSize::

SKSpriteNode *pig = [SKSpriteNode spriteNodeWithImageNamed:@"pig_1"];

Replace the above line with the following:

Pig *pig = [[Pig alloc] initWithImageNamed:@"pig_1"];
pig.name = @"pig";

You have simply replaced SKSpriteNode with your new subclass, Pig, and given it a name. You will use this name when you process new touches to identify pig nodes.

Add the following instance variables to MyScene, just below the @implementation line:

{
  Pig *_movingPig;
  NSTimeInterval _lastUpdateTime;
  NSTimeInterval _dt;
}

_movingPig will hold a reference to the pig the user wants to move. _lastUpdateTime will store the time of the last call to update: and _dt will store the time elapsed between the two most recent calls to update:.

A few steps remain before you get to see your pig move. Add the following code inside touchesBegan:withEvent::

CGPoint touchPoint = [[touches anyObject] locationInNode:self.scene];
SKNode *node = [self nodeAtPoint:touchPoint];
 
if([node.name isEqualToString:@"pig"]) {
  [(Pig *)node addPointToMove:touchPoint];
  _movingPig = (Pig *)node;
}

What happens here? First, you find the location of the touch within the scene. After that, you use nodeAtPoint: to identify the node at that location. The if statement uses the node’s name to see if the user touched a pig or something else, such as the background.

Note: You use the name property of SKNode to check for the pig. This is like UIView‘s tag property: a simple way to identify a node without needing to store a reference. Later, you’ll see another use case for the name property.

If the user touched a pig, you add touchPoint as a waypoint and set _movingPig to the touched node. You’ll need this reference in the next method to add more points to the path.

To draw a path, after the first touch the user needs to move their finger while continuously touching the screen. Add the following implementation of touchesMoved:withEvent: to add more waypoints:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  CGPoint touchPoint = [[touches anyObject] locationInNode:self.scene];    
  if(_movingPig) {
    [_movingPig addPointToMove:touchPoint];
  }
}

This is a simple method. You get the next position of the user’s finger and if you found a pig in touchesBegan:withEvent:, as indicated by a non-nil _movingPig value, you add the position to this pig as the next waypoint.

So far, you can store a path for the pig—now let’s make the pig follow this path. Add the following code to update: inside MyScene.m:

_dt = currentTime - _lastUpdateTime;
_lastUpdateTime = currentTime;
 
[self enumerateChildNodesWithName:@"pig" usingBlock:^(SKNode *pig, BOOL *stop) {
  [(Pig *)pig move:@(_dt)];
}];
  1. First, you calculate the time since the last call to update: and store it in _dt. Then, you assign currentTime to _lastUpdateTime so you have it for the next call.
  2. Here is the other use case for the name property. You use SKScene‘s method enumerateChildNodesWithName:usingBlock: to enumerate over all nodes with the name pig. On these nodes, you call move:, passing _dt as the argument. Since SKNode has no method called move:, you cast it to Pig to make Xcode and the compiler happy.

Now build and run, and let the pig follow your finger as you draw a path.

Pig following mouse

The pig doesn’t face in the direction it’s moving, but otherwise this is a good result!

But wait a minute—isn’t this a line drawing game? So where is the line?

Drawing Lines

Believe it or not, there is only one important step left to complete a line drawing game prototype that you can expand. Drawing the lines!

At the moment, only the pig knows the path it wants to travel, but the scene also needs to know this path to draw it. The solution to this problem is a new method for your Pig class.

Open Pig.h and add the following method declaration to the interface:

- (CGPathRef)createPathToMove;

Now open Pig.m and implement this new method as follows:

- (CGPathRef)createPathToMove {
  //1
  CGMutablePathRef ref = CGPathCreateMutable();
 
  //2
  for(int i = 0; i < [_wayPoints count]; ++i) {
    CGPoint p = [_wayPoints[i] CGPointValue];
 
    //3
    if(i == 0) {
      CGPathMoveToPoint(ref, NULL, p.x, p.y);
    } else {
      CGPathAddLineToPoint(ref, NULL, p.x, p.y);
    }
  }
 
  return ref;
}
  1. First, you create a mutable CGPathRef so you can add points to it.
  2. This for loop iterates over all the stored waypoints to build the path.
  3. Here you check if the path is just starting, indicated by an i value of zero. If so, you move to the point’s location; otherwise, you add a line to the point. If this is confusing, think about how you would draw a path with pen and paper. CGPathMoveToPoint() is the moment you put the pen on the paper after moving it to the starting point, while CGPathAddLineToPoint() is the actual drawing with the pen on the paper.
  4. At the end, you return the path.
Note: Are you thinking about memory leaks? You’re correct that ARC does not support CGPath objects, so you need to call CGPathRelease() when you’re done with your path. You’ll do that soon!

Open MyScene.m and add this method to draw the pig’s path:

- (void)drawLines {
  //1
  [self enumerateChildNodesWithName:@"line" usingBlock:^(SKNode *node, BOOL *stop) {
    [node removeFromParent];
  }];
 
  //2
  [self enumerateChildNodesWithName:@"pig" usingBlock:^(SKNode *node, BOOL *stop) {
    //3
    SKShapeNode *lineNode = [SKShapeNode node];
    lineNode.name = @"line";
    lineNode.strokeColor = [SKColor grayColor];
    lineNode.lineWidth = 1.0f;
    //4
    CGPathRef path = [(Pig *)node createPathToMove];
    lineNode.path = path;
    CGPathRelease(path);
    [self addChild:lineNode];
  }];
}

Here’s what’s happening:

  1. You’ll redraw the path every frame, so first you remove any old line nodes. To do so, you enumerate over all nodes with the name “line” and remove them from the scene.
  2. After that, you enumerate over all the pigs in your scene using the same method.
  3. For each pig, you create an SKShapeNode and name it “line”. Next you set the stroke color of the shape to gray and the width to 1. You can use any color you want, but I think gray will be visible on the most backgrounds.
  4. You use the method you just added to Pig to create a new path and assign it to lineNode‘s path property. Then you call CGPathRelease to free the path’s memory. If you forgot to do that, you would create a memory leak that would eventually crash your app. Finally, you add lineNode to your scene so that the scene will render it.

At last, to draw the path, add this line at the end of update: in MyScene.m:

[self drawLines];

Build and run, ready your finger and watch as the game draws your path onscreen—and hopefully, your pig follows it!

Drawn_Lines

Continuous Movement

Actually, the pig stops moving after it reaches the last waypoint. For Hogville, it makes more sense and will be more challenging if the pigs keep moving. Otherwise, you could simply draw a path to the pigs straight to their pens and the game would be over!

Let’s correct this. Open Pig.m and add the following code at the end of move::

else {
  newPosition = CGPointMake(currentPosition.x + _velocity.x * [dt doubleValue],
                            currentPosition.y + _velocity.y * [dt doubleValue]);
}

This code simply continues to move the pig with its most recently calculated velocity when it runs out of waypoints. Remember, velocity includes both direction and speed.

Now find this line inside move::

self.position = newPosition;

Move the above line to the end of the method so that after the if-else statements.

Build and run, and watch your pig moving and moving and moving… until you can’t see it anymore. This little piggy went off screen!

Moving_Offscreen

What you need is a method to check if the pig is about to leave the screen and if so, switch its direction.

Also in Pig.m, add this method:

- (CGPoint)checkBoundaries:(CGPoint)point {
  //1
  CGPoint newVelocity = _velocity;
  CGPoint newPosition = point;
 
  //2
  CGPoint bottomLeft = CGPointZero;
  CGPoint topRight = CGPointMake(self.scene.size.width, self.scene.size.height);
 
  //3
  if (newPosition.x <= bottomLeft.x) {
    newPosition.x = bottomLeft.x;
    newVelocity.x = -newVelocity.x;
  } 
  else if (newPosition.x >= topRight.x) {
    newPosition.x = topRight.x;
    newVelocity.x = -newVelocity.x;
  }
 
  if (newPosition.y <= bottomLeft.y) {
    newPosition.y = bottomLeft.y;
    newVelocity.y = -newVelocity.y;
  }
  else if (newPosition.y >= topRight.y) {
    newPosition.y = topRight.y;
    newVelocity.y = -newVelocity.y;
  }
 
  //4
  _velocity = newVelocity;
 
  return newPosition;
}

This looks more complicated than it is, so let’s look more closely:

  1. First, you assign the current velocity and point to local variables.
  2. Here you define the important points on the screen. You can use bottomLeft to check if the pig is moving off screen from the left or bottom sides and topRight to check the top and right sides of the screen. You perform these checks inside the following if statements, one for each side of the screen.
  3. The first if statement checks the x value of newPosition. If this value is zero or less, the pig is leaving the screen from the left side. To avoid this, you set the pig’s x-position to the left boundary—zero—and reverse the x-component of the velocity so the pig starts moving in the opposite direction. The other if statements do the same for the remaining three bounds of the screen.
  4. At the end, you change _velocity to whatever value you calculated and then return newPosition.

To make use of this new method, change the last line in move: from this:

self.position = newPosition;

To this:

self.position = [self checkBoundaries:newPosition];

Build and run your project again and watch the pig bounce off of the screen’s borders. You’ve got yourself a pig pen!

Bouncing_Pig

Rotating the Sprite

Before adding the actual gameplay, there are some minor improvements you should make. First, the pig doesn’t rotate at all, which looks a bit weird. Like most animals, pigs normally walk facing forward rather than backward or to the side!

Rotate the pig so that it faces the direction it’s moving by adding the following line to the end of move: in Pig.m:

self.zRotation = atan2f(_velocity.y, _velocity.x) + M_PI_2;

atan2f returns the angle between the x-axis and the given point. You add M_PI_2 because the pig image you use faces down and the value you receive from atan2f assumes the image faces to the right. Therefore, by adding M_PI_2, you rotate the pig by 90° counterclockwise.

A rotating pig

Note: If this is too much math for you, check out our open source SKTUtils library. The library includes a ton of helper functions that abstracts common math like this; for example it includes a CGPointToAngle method that would be helpful here.

Animating the Sprite

Now that the pig faces the direction it’s moving, it’s time to add a pretty animation. Add the following new instance variable in Pig.m‘s @implementation section:

SKAction *_moveAnimation;

Next, go to initWithImageNamed: and add the following lines directly after the line that creates _wayPoints:

SKTexture *texture1 = [SKTexture textureWithImageNamed:@"pig_1"];
SKTexture *texture2 = [SKTexture textureWithImageNamed:@"pig_2"];
SKTexture *texture3 = [SKTexture textureWithImageNamed:@"pig_3"];
_moveAnimation = [SKAction animateWithTextures:@[texture1, texture2, texture3] timePerFrame:0.1];

This creates three textures, each of which represents a frame of the animation. The last line creates an SKAction that animates the pig’s movement using the three textures and stores the action in _moveAnimation. The game will show each frame for 0.1 seconds, resulting in a nice walking animation for the pig.

The last step is to run the animation. In Pig.m, go to move: and add the following lines at the beginning of the method:

if(![self actionForKey:@"moveAction"]) {
  [self runAction:_moveAnimation withKey:@"moveAction"];
}

First, you check if an animation named “moveAction” is already running and add it if there is no such animation. This ensures that the game adds the animation only once. Without this check, you wouldn’t see the animation because it would start from the beginning every time you called move:.

Build and run, and watch your pig animate around the screen as you direct it.

Move_Animation1

Your Gameplay Strategy

Let’s pause to think about how the gameplay will work and what you need to achieve it.

The basic idea behind Hogville is that pigs will appear at random positions on the left side of the screen and from there, move in random directions. The time between the appearances of each new pig will decrease until it reaches a threshold defined by you.

The player must herd all of these pigs to a trough of food where they can eat and after that, bring them to a barn where they can sleep. If any two pigs collide, the game is over.

Here are the needed steps:

  1. Add sprites for the food trough and barn.
  2. Spawn pigs over time.
  3. Add collision handling.
  4. Add logic that controls when a pig needs food or is ready to sleep.
  5. Handle win/lose conditions.

You will go step by step through this list to finish your game. Don’t panic—this tutorial is here to guide you!

Completing the Scene

To complete the first step, open MyScene.h and add the following property to the interface:

@property (nonatomic, strong) SKSpriteNode *homeNode;

homeNode will display an image of a barn and act as the final goal for your pigs. You will need to access this property later in the Pig class, so you make it a public property instead of a private variable.

Then open MyScene.m and add the following instance variable to the @implementation section:

NSTimeInterval _currentSpawnTime;

You’ll use _currentSpawnTime to track the wait time between pig spawns. After each new pig appears, you’ll reduce this value to speed up the next spawn.

Add this new method in MyScene.m to set up the sprites in your scene:

- (void)loadLevel {
  //1
  SKSpriteNode *bg = [SKSpriteNode spriteNodeWithImageNamed:@"bg_2_grassy"];
  bg.anchorPoint = CGPointZero;
  [self addChild:bg];
 
  //2
  SKSpriteNode *foodNode = [SKSpriteNode spriteNodeWithImageNamed:@"trough_3_full"];
  foodNode.name = @"food";
  foodNode.zPosition = 0;
  foodNode.position = CGPointMake(250.0f, 200.0f);
  // More code later
 
  [self addChild:foodNode];
 
  //3
  self.homeNode = [SKSpriteNode spriteNodeWithImageNamed:@"barn"];
  self.homeNode.name = @"home";
  self.homeNode.zPosition = 0;
  self.homeNode.position = CGPointMake(380.0f, 20.0f);
  [self addChild:self.homeNode];
  _currentSpawnTime = 5.0;
}

Let’s take a tour of this code:

  1. This is simply the code to create the background that you already have in initWithSize:. You’ll remove it from that method soon.
  2. Here you create an SKSpriteNode for the trough and give it the name “food”. You place the node near the center of the screen and add it to the scene.
  3. This creates the barn and positions it in the lower-right corner of your scene. Finally, you set the current spawn time to five seconds. You could make the game easier or harder by increasing or decreasing the initial spawn time.

Now replace all of the code within the if statement of initWithSize: with the following call to loadLevel:

[self loadLevel];

Build and run, and you’ll see the stage is now set:

Background added

But no pigs are anywhere to be found! Let’s bring in some grunts.

Spawning Pigs

To spawn some pigs, add the following new method to MyScene.m:

- (void)spawnAnimal {   
  //1
  _currentSpawnTime -= 0.2;
 
  //2
  if(_currentSpawnTime < 1.0) {
    _currentSpawnTime = 1.0;
  }
 
  //3
  Pig *pig = [[Pig alloc] initWithImageNamed:@"pig_1"];
  pig.position = CGPointMake(20.0f, arc4random() % 300);
  pig.name = @"pig";
  pig.zPosition = 1;
 
  [self addChild:pig];
 
  //4
  [self runAction:
   [SKAction sequence:@[[SKAction waitForDuration:_currentSpawnTime], 
                        [SKAction performSelector:@selector(spawnAnimal) onTarget:self]]]];
}

Here’s a step-by-step breakdown of the code above:

  1. This decreases the time between spawns by 0.2 seconds every time the game spawns a pig.
  2. You ensure the spawn time never falls below one second, because anything faster than that would make the game too difficult, and if it hit zero, things would probably break.
  3. Here you create a pig and add it to the scene like you did before in initWithSize:. Now you set the pig’s position with a fixed x-value of 20 and a random y-value that ranges between zero and 299. Setting the pig’s zPosition to 1 makes sure the pig renders on top of the lines in the scene, which you’ve added with the default zPosition of zero.
  4. This runs an action sequence. A sequence performs a set of actions in order, one at a time. The result is that the performSelector action calls spawnAnimal again after waitForDuration waits for _currentSpawnTime seconds. Because you reduce _currentSpawnTime each time you call this method, you end up calling spawnAnimal with less and less of a delay.

Now add a call to your method in MyScene.m in initWithSize:, immediately after the call to [self loadLevel];:

[self spawnAnimal];

Build and run, and watch the pigs appear faster and faster over time.

Spawning_Pigs

Detecting Collisions

As you can see in the image above, the pigs move through the trough, barn and even other pigs.

pigs_gone_wild

This is a sign your game needs collision detection! Fortunately, you don’t have to create it from scratch. Sprite Kit includes a physics engine that you can use for collision detection.

Note: This tutorial assumes you have some experience using Sprite Kit’s built-in physics engine. If you don’t, work your way through this Sprite Kit tutorial for beginners before proceeding. You’ll also find a good introduction in the book iOS Games by Tutorials, as well as more advanced chapters about the built-in physics engine.

First, add some physics categories to your scene. Open MyScene.h and add the following typedef above the @interface part:

typedef NS_OPTIONS(uint32_t, LDPhysicsCategory) {
  LDPhysicsCategoryAnimal = 1 << 0,
  LDPhysicsCategoryFood = 1 << 1,
};

This creates two categories, one for each type of physics body you will have. You can use these categories to detect collisions between different physics bodies. There are two types of collisions that can occur in this game, those between two pigs and those between a pig and the food trough.

Now open MyScene.m and add a category extension to your scene.

@interface MyScene () <SKPhysicsContactDelegate>
 
@end

This tells the compiler that your scene is implementing the SKPhysicsContactDelegate protocol.

Now find initWithSize: and add this right before the call to loadLevel:

self.physicsWorld.gravity = CGVectorMake(0.0f, 0.0f);
self.physicsWorld.contactDelegate = self;

This configures your physics world. The first line disables gravity in your scene and the second registers your scene as the contact delegate of the physics world. Sprite Kit notifies this delegate whenever two appropriately configured physics bodies begin to touch or stop touching.

To process these collision events, add the following method to MyScene.m:

- (void)didBeginContact:(SKPhysicsContact *)contact {
  //1
  SKNode *firstNode = contact.bodyA.node;
  SKNode *secondNode = contact.bodyB.node;
 
  //2
  uint32_t collision = firstNode.physicsBody.categoryBitMask | secondNode.physicsBody.categoryBitMask;
 
  //3
  if(collision == (LDPhysicsCategoryAnimal | LDPhysicsCategoryAnimal)) {
    NSLog(@"Animal collision detected");
  } else if(collision == (LDPhysicsCategoryAnimal | LDPhysicsCategoryFood)) {
    NSLog(@"Food collision detected.");
  } else {
    NSLog(@"Error: Unknown collision category %d", collision);
  }
}

Let’s break down what’s happening above:

  1. These two lines give you the nodes that just collided. There is no specific order for the nodes, so you have to check the objects yourself if you care which is which.
  2. You perform a bitwise-OR of the categories of the two collided nodes and store it in collision.
  3. Here you figure out what kind of collision occurred by comparing collision with the bit mask for an animal/animal or animal/food collision. For the moment, you simply log the detected collision to the console.

There’s something essential missing, though. Can you guess what it is?

Adding Physics Bodies

To detect collisions, you have to configure a physics body on any sprite you want to engage in collisions. Start by configuring the trough in MyScene.m by replacing the comment //More code later in loadLevel with the following code:

foodNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:foodNode.size];
foodNode.physicsBody.categoryBitMask = LDPhysicsCategoryFood;
foodNode.physicsBody.dynamic = NO;

Your physics body needs geometry to define its shape. In this case, a rectangle with the node’s size will suffice. Since this is the food trough, you assign the corresponding category to it. The last line tells the physics engine not to move this object. That is, things can bounce off of the object, but no force will affect the object itself.

Note: In this game, collisions will not affect the motion of any object—you will use the physics engine only for collision detection.

The second node that needs a physics body is the pig. Open Pig.m and import the scene in order to access the category enum:

#import "MyScene.h"

Then add the following to initWithImageNamed:, after the line that initializes _moveAnimation:

self.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:self.size.width / 2.0f];
self.physicsBody.categoryBitMask = LDPhysicsCategoryAnimal;
self.physicsBody.contactTestBitMask = LDPhysicsCategoryAnimal | LDPhysicsCategoryFood;
self.physicsBody.collisionBitMask = kNilOptions;

This is similar to what you did for the trough, except you use a circular physics shape because it fits the pig better than a rectangle. You set the category to indicate the object is an animal and you set contactTestBitMask to indicate that this node should produce contact notifications whenever it touches any other physics body that belongs to any category inside the bit mask.

You also set the object’s collisionBitMask to kNilOptions (which is simply zero) to indicate to the physics engine that forces generated during collisions should not affect the pigs. Basically, you allow pigs to pass right through other physics objects.

Challenge: What kinds of nodes will generate contact notifications when a pig touches them?

Solution Inside: Solution SelectShow>

Build and run, and let a pig collide with the trough and with another pig. Have a look at the console output and check that the correct logs appear.

Collision_Log

Feeding the Pigs

With your collision detection working, you can make it possible to feed your pigs. Inside Pig.m, add the following instance variables to the @implementation section:

BOOL _hungry;
BOOL _eating;

These flags will keep track of the pig’s current state.

To begin with a hungry pig, add the following line inside initWithImageNamed:, just after the lines that set up the physics body:

_hungry = YES;

Still inside Pig.m, change move: by wrapping its current contents inside an if statement that checks to make sure the pig is not eating, as shown below:

if(!_eating) {
  //existing move: code goes here.
}

This prevents your pig from moving while it’s eating. As your mother always said, “Don’t eat and run at the same time!”

eat_run_mom

When a pig is finished eating, you want it to start walking again. Add the following method to Pig.m to send the pig off in a random direction:

- (void)moveRandom {
  //1
  [_wayPoints removeAllObjects];
 
  //2
  int width = (int)CGRectGetWidth(self.scene.frame);
  int height = (int)CGRectGetHeight(self.scene.frame);
  //3
  CGPoint randomPoint = CGPointMake(arc4random() % width,  arc4random() % height);
  [_wayPoints addObject:[NSValue valueWithCGPoint:randomPoint]];
}

This method has three simple steps:

  1. First, you remove all existing waypoints to make the path truly random.
  2. Then, you get the width and height of the scene to have a range for the random numbers.
  3. With these values, you create a random CGPoint inside your scene and add it as a waypoint. This new waypoint is enough to get the pig moving again.

Now add the following method, which you’ll call when the pig gets to the trough:

- (void)eat {
  //1
  if(_hungry) {
    //2
    [self removeActionForKey:@"moveAction"];
    _eating = YES;
    _hungry = NO;
 
    //3
    SKAction *blockAction = [SKAction runBlock:^{
      _eating = NO;
      [self moveRandom];
    }];
 
    [self runAction:[SKAction sequence:@[[SKAction waitForDuration:1.0], blockAction]]];
  }
}

Here’s how you feed a pig:

  1. When the pig is hungry, it will start eating.
  2. You remove the walking animation and set _eating to YES. Your pig will stand still on the trough and eat. Once it finishes eating it is no longer hungry, so you set _hungry to NO.
  3. Like everything in life, eating takes time, so you run a sequence action that waits for a second and then executes blockAction, which sets _eating to NO and calls the method you just added to start the pig walking again. You could decrease the eating time to make the game easier.

You want to call eat from your scene, so add the declaration to the interface in Pig.h:

- (void)eat;

Now open MyScene.m and find didBeginContact:. Replace the NSLog statement that logs “Food collision detected” with the following code:

if([firstNode.name isEqualToString:@"pig"]) {
  [(Pig *)firstNode eat];
} else {
  [(Pig *)secondNode eat];
}

You know this collision is between the pig and the trough, so you simply figure out which node is the pig and call eat on it.

Build and run, and guide the pig to a trough. He will stop to eat, then move off to a random direction.

HappyPig

Can’t you just imagine that pig squealing with delight? ;]

Finishing the Game

Your game is almost complete, but you still need win and lose conditions. This is an “endless game” in that the player keeps going as long as they can until they lose.

However, you need to make it so that pigs who have eaten can be guided to the barn, at which point they will be removed from the scene. A good place for this check is the move: method of Pig.

Open Pig.m and add the following instance variable to the @implementation:

BOOL _removing;

You’ll use this flag to mark pigs while they are in the process of leaving the game. This is because you will make them fade off the screen when they’re leaving the game, and you want to have a flag that prevents you from running this animation twice.

Still in Pig.m, add the following new method:

- (void)checkForHome {
  //1
  if(_hungry || _removing) {
    return;
  }
 
  //2
  SKSpriteNode *homeNode = ((MyScene *)self.scene).homeNode;
  CGRect homeRect = homeNode.frame;
 
  //3
  if(CGRectIntersectsRect(self.frame, homeRect)) {
    _removing = YES;
    [_wayPoints removeAllObjects];
    [self removeAllActions];
    //4
    [self runAction:
     [SKAction sequence:
      @[[SKAction group:
         @[[SKAction fadeAlphaTo:0.0f duration:0.5],
           [SKAction moveTo:homeNode.position duration:0.5]]],
        [SKAction removeFromParent]]]];
  }
}

What’s happening here?

  1. Hungry pigs won’t go to sleep, so you first check if the pig is hungry or is already set to be removed from the game.
  2. Here you get the frame rectangle of the homeNode.
  3. You then check if the pig’s frame overlaps the barn’s. If that’s the case, you set the pig’s _removing flag to YES and clear its waypoints and any running actions.
  4. Here you run another sequence of actions that first runs a group of actions simultaneously, and when those are done it removes the pig from the scene. The group of actions fades out the pig’s sprite while it moves the pig to the center of the barn.

Now call this method by adding the following line to move:, right after the line that sets the pig’s zRotation:

[self checkForHome];

Build and run, and guide those pigs home!

GoHome

Game Over: When Pigs Collide

When two pigs collide, the game will show a “Game Over!” message and then give the player the chance to start a new round.

Open MyScene.m and add a BOOL instance variable to the @implementation section to indicate when the game is over:

BOOL _gameOver;

You don’t want the pigs moving or spawning when the game is over, so wrap the current contents of update: inside an if statement, as follows:

if(!_gameOver) {
  // existing code here
}

Also, add the following code at the beginning of spawnAnimal to make sure new pigs don’t appear after the player has lost:

if(_gameOver) {
  return;
}

Now add this method, which you’ll call when two pigs collide:

- (void)handleAnimalCollision {
  _gameOver = YES;
 
  SKLabelNode *gameOverLabel = [SKLabelNode labelNodeWithFontNamed:@"Thonburi-Bold"];
  gameOverLabel.text = @"Game Over!";
  gameOverLabel.name = @"label";
  gameOverLabel.fontSize = 35.0f;
  gameOverLabel.position = CGPointMake(self.size.width / 2.0f, self.size.height / 2.0f + 20.0f);
  gameOverLabel.zPosition = 5;
 
  SKLabelNode *tapLabel = [SKLabelNode labelNodeWithFontNamed:@"Thonburi-Bold"];
  tapLabel.text = @"Tap to restart.";
  tapLabel.name = @"label";
  tapLabel.fontSize = 25.0f;
  tapLabel.position = CGPointMake(self.size.width / 2.0f, self.size.height / 2.0f - 20.0f);
  tapLabel.zPosition = 5;
  [self addChild:gameOverLabel];
  [self addChild:tapLabel];
}

Here you first set _gameOver to YES, which ends the game. You add two labels centered on the screen telling the user that the game is over and that they can tap the screen to restart. You set each label’s zPosition to five to ensure they are in front of all other nodes.

Now call this new method in didBeganContact:. Replace the NSLog statement that logs “Animal collision detected” with the following line:

[self handleAnimalCollision];

You told the player that they can tap the screen to restart, but right now that isn’t true. Add the following method to MyScene.m to handle restarting:

- (void)restartGame {
  [self enumerateChildNodesWithName:@"line" usingBlock:^(SKNode *node, BOOL *stop) {
    [node removeFromParent];
  }];
 
  [self enumerateChildNodesWithName:@"pig" usingBlock:^(SKNode *node, BOOL *stop) {
    [node removeFromParent];
  }];
 
  [self enumerateChildNodesWithName:@"label" usingBlock:^(SKNode *node, BOOL *stop) {
    [node removeFromParent];
  }];
 
  _currentSpawnTime = 5.0f;
  _gameOver = NO;
  [self spawnAnimal];
}

restartGame removes all lines, pigs and labels from the scene, sets the spawn time back to five seconds, sets _gameOver to NO and begins spawning animals.

You need to call this method if the user taps the screen while the game is over, so add the following lines to the top of touchesBegan:withEvent::

if(_gameOver) {
  [self restartGame];
}

All right, then! Build and run once more, and enjoy the line drawing game you created. See if you’re a natural-born swineherd.

Game_Over

Adding Polish

You may have noticed some places where the game behaves strangely. For example, look at the behavior of spawning pigs—they appear to walk in place! You can fix this easily by calling moveRandom on the pigs when you spawn them.

You’ll need to access this method from your scene, so add the following declaration to the interface in Pig.h:

- (void)moveRandom;

Then, inside MyScene.m, call this method in spawnAnimal, just after the line that adds the pig to the scene:

[pig moveRandom];

Build and run, and now the pigs move right away:

EagerPigs

Now let’s take a closer look at the way your game currently handles touches. Run the app and add a path to a pig, but before the pig arrives at the endpoint, try to change the path by drawing a new one.

As you can see, the pig initially ignores your second command and continues along the first path. Then, after reaching the end point, it moves back to the place where you tried to change direction and follows the second path. Every new path is just tacked onto the old one!

It would be much better if the game replaced the old path with a new path. To make this happen, first open Pig.h and declare a new method:

- (void)clearWayPoints;

Next, open Pig.m and implement this method as follows:

- (void)clearWayPoints {
  [_wayPoints removeAllObjects];
}

This method simply removes all objects from _wayPoints.

Now go to MyScene.m and add the following line to touchesBegan:withEvent:, just above the line that calls addPointToMove::

[(Pig *)node clearWayPoints];

You call clearWayPoints every time the player touches a pig.

Build and run, and now the pigs listen to your every command!

ListeningPigs

Challenge: Add a HUD to the scene showing the player how many pigs they have removed. See if you can accomplish this by yourself before checking the solution below.

Solution Inside: Tips SelectShow>

Where to Go From Here?

Here is the final project with all the code from the above tutorial.

There is still a lot of room for improvement in your game. But that’s OK—it gives you plenty of opportunities to practice your skills, old and new!

  • You could reduce the area searched when removing waypoints, which would make the pig look like it was on top of the line rather than having the line always just out of the pig’s reach.
  • If you move your finger slowly while creating a line, it currently adds far too many points, producing an unpleasant visual experience. You could apply a smoothing algorithm to the points, such as the basic one described here, to reduce redundant points and improve the quality of both the line rendering and the pig’s motion.
  • It’s currently possible for a new pig to spawn with no warning right on top of an existing pig, ending the game immediately. How would you fix this problem?
  • You could add an indicator to warn the player of incoming pigs.
  • You could add different levels that you load from a plist, as well as different animals.
  • You could use the additional trough images provided with the starter project to make the trough appear empty after a certain amount of feeding, and require the player to refill the trough by tapping on it.
  • To tell you the truth, I really don’t know why the game should end when two animals collide. It would be funnier if the animals began running in random directions for a short time after a collision. Because everything must come to an end (this tutorial, too) you could add a counter that ends the game after five collisions.

Whether or not you implement any of these suggestions, I’m sure you have your own excellent ideas, and now you know more of what you need to bring them to the App Store. Make sure to drop me a note when your game is available.

How to Make a Line Drawing Game with Sprite Kit is a post from: Ray Wenderlich

The post How to Make a Line Drawing Game with Sprite Kit appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



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