Sprite Kit Tutorial for Beginners

http://www.raywenderlich.com/42699/spritekit-tutorial-for-beginners


This post is also available in: Japanese

Note from Ray: Congratulations, you guys did it! By helping to spread the word about the iOS 7 Feast, you guys unlocked the first iOS 7 tutorial in less than 15 minutes! :]

Sprite Kit is an exciting new framework for making 2D games that comes built-in to iOS 7. It has sprite support, support for cool special effects like videos, filters, and masking, an integrated physics library, and a lot more.

iOS 7 comes with a great Sprite Kit sample project called Adventure that you should definitely check out. However, the game is a bit complicated and sometimes you just want to start with something as simple as possible. That’s what this tutorial is all about!

In this Sprite Kit tutorial for beginners, you will learn how to create a simple and fun 2D game for your iPhone, from start to finish. If you’ve followed our Simple Cocos2D game tutorial, the game might look familiar ;]

Before getting started be sure to install the latest version of Xcode (5.X) which contains support for Sprite Kit and iOS 7.

You can either follow along with this tutorial, or just jump straight to the sample project at the end. And yes. There will be ninjas.

Sprite Kit Pros and Cons

Before you get started, I just want to point out that Sprite Kit isn’t the only option for you to make 2D games on iOS, and it has several pros and cons you should be aware of.

I thought it would be helpful to take a step back and look at the four most popular options for making games on iOS and their pros and cons.

Sprite Kit Pros

  • Pro: It’s built right into iOS, so there is no need to download extra libraries or have external dependencies. It’s also written by Apple, so you can be sure that it will be well supported and updated moving forward.
  • Pro: It has built-in tool support for texture atlases and particles.
  • Pro: It lets you do some things that would be very difficult if not impossible to do in other frameworks, like treating videos as sprites or applying cool image effects and masks.

Sprite Kit Cons

  • Con: If you use SpriteKit, you’re locked into the iOS ecosystem. You never know if you have a hit game that you might want to port to Android!
  • Con: Sprite Kit is still in the early stages and this time doesn’t have as many useful features as some of the other frameworks, like Cocos2D in particular. The biggest thing missing IMHO: no ability to write custom OpenGL code.

Sprite Kit vs Cocos2D-iPhone vs Cocos2D-X vs Unity

After this a lot of you may be thinking, “Well, which 2D framework should I choose?”

The answer that depends on what your goals are. Here’s my 2c:

  • If you are a complete beginner, or solely focused on iOS: Use Sprite Kit – it’s built in, easy to learn, and will get the job done.
  • If you need to write your own OpenGL code: Stick with Cocos2D or another option for now, as Sprite Kit does not currently support this.
  • If you want to be cross-platform: Choose Cocos2D-X or Unity. Cocos2D-X is nice because it’s “down to the wire”, built for 2D games, and you can do just about anything you want with it. Unity is nice because it gives you more flexibility (i.e. you can add some 3D aspects into your game if you want), however you have to go through a few more hoops to make 2D games.

So after reading all this, if you think Sprite Kit might be what you’re looking for, keep reading and we’ll get you started!

Hello, Sprite Kit!

Let’s start by getting a simple Hello World project up and running by using the Sprite Kit Game template that comes built in to Xcode 5.

Start up Xcode, select File\New\Project, choose the iOS\Application\SpriteKit Game template, and clickNext:

Enter SpriteKitSimpleGame for the Product Name, iPhone for Devices, and click Next:

Choose somewhere on your drive to save the project, and click Create. Then click the play button to run the project as-is. You should see the following:

Just like Cocos2D, Sprite Kit is organized into the concept of scenes, which are kind of like “levels’ or “screens” for a game. For example, you might have a scene for the main gameplay area, and another scene for the world map between levels.

If you take a look at your project, you’ll see the template has already created a scene for you by default –MyScene. Open MyScene.m and you’ll see that it contains some code to put a label on the screen, and add a rotating spaceship when you tap somewhere.

In this tutorial, you’ll mainly be working within MyScene. But before you begin, you have to make a few tweaks because this game needs to run in landscape instead of portrait.

Switching to Landscape Orientation

First, open your target setting by clicking your SpriteKitSimpleGame project in the Project Navigator, selecting the SpriteKitSimpleGame target. Then, in the Deployment Info section, uncheck Portrait so onlyLandscape Left and Landscape Right are checked, as shown below:

Build and run your project, and all seems to work:

However, all is not as it seems. Let’s try adding the ninja to the game to see why.

First, download the resources for this project and drag them into your Xcode project. Make sure that “Copy items into destination group’s folder (if needed)” is checked, and that your SpriteKitSimpleGame target is selected.

Next, open MyScene.m and replace the contents with the following:

#import "MyScene.h"
 
// 1
@interface MyScene ()
@property (nonatomic) SKSpriteNode * player;
@end
 
@implementation MyScene
 
-(id)initWithSize:(CGSize)size {    
    if (self = [super initWithSize:size]) {
 
        // 2
        NSLog(@"Size: %@", NSStringFromCGSize(size));
 
        // 3
        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
 
        // 4
        self.player = [SKSpriteNode spriteNodeWithImageNamed:@"player"];
        self.player.position = CGPointMake(100, 100);
        [self addChild:self.player];
 
    }
    return self;
}
 
@end

Let’s go over this step-by-step.

  1. Here you create a private interface so that you can declare a private variable for the player (i.e. the ninja), which you’re about to add to the scene.
  2. Here you log out the size of the scene, for reasons you’ll see shortly.
  3. Setting the background color of a scene in Sprite Kit is as simple as setting the backgroundColorproperty. Here you set it to white.
  4. Adding a sprite to a Sprite Kit scene is very easy as well. You just use the spriteNodeWithImageNamed method, and pass in the name of the image. You then set the position of the sprite, and calladdChild to add it to the scene. Here you set the ninja to be at (100, 100), which should be a bit up and to the right from the lower left corner of the screen.

Build and run, and…

Yowza! The screen is white, but the ninja is nowhere to be seen. You may think that this is by design (because after all, this is a ninja we’re talking about), but there’s actually a problem going on here.

If you look at the console output, you’ll see the following:

SpriteKitSimpleGame[3139:907] Size: {320, 568}

So our scene thinks its width is 320 and its height is 568 – but actually it’s the opposite!

To see what’s happening here, take a look at ViewController.m‘s viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Configure the view.
    SKView * skView = (SKView *)self.view;
    skView.showsFPS = YES;
    skView.showsNodeCount = YES;
 
    // Create and configure the scene.
    SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
    scene.scaleMode = SKSceneScaleModeAspectFill;
 
    // Present the scene.
    [skView presentScene:scene];
}

This creates the scene with a size as the bounds of the view. However, when viewDidLoad is called, this is before the view has been added to the view hierarchy and hence it hasn’t responded to layout changes yet. So the view bounds might not be correct yet, and this probably isn’t the best time to start up the scene.

Note: For more details, check out this great explanation by Rob Mayoff.

The solution is to move the start up code to a later point in the process. Replace viewDidLoad with the following:

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
 
    // Configure the view.
    SKView * skView = (SKView *)self.view;
    if (!skView.scene) {
      skView.showsFPS = YES;
      skView.showsNodeCount = YES;
 
      // Create and configure the scene.
      SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
      scene.scaleMode = SKSceneScaleModeAspectFill;
 
      // Present the scene.
      [skView presentScene:scene];
    }
}

Build and run, and viola – ladies and gentlemen, the ninja has entered the building!

Now that you know the coordinates are working correctly, you’ll put the ninja where he belongs – on the left side toward the middle. To do this, switch back to MyScene.m and replace the line that sets the ninja’s position with the following:

self.player.position = CGPointMake(self.player.size.width/2, self.frame.size.height/2);

Moving Monsters

Next you want to add some monsters into your scene for your ninja to combat. To make things more interesting, you want the monsters to be moving – otherwise there wouldn’t be much of a challenge! So let’s create the monsters slightly off screen to the right, and set up an action for them telling them to move to the left.

Add the following method to MyScene.m:

- (void)addMonster {
 
    // Create sprite
    SKSpriteNode * monster = [SKSpriteNode spriteNodeWithImageNamed:@"monster"];
 
    // Determine where to spawn the monster along the Y axis
    int minY = monster.size.height / 2;
    int maxY = self.frame.size.height - monster.size.height / 2;
    int rangeY = maxY - minY;
    int actualY = (arc4random() % rangeY) + minY;
 
    // Create the monster slightly off-screen along the right edge,
    // and along a random position along the Y axis as calculated above
    monster.position = CGPointMake(self.frame.size.width + monster.size.width/2, actualY);
    [self addChild:monster];
 
    // Determine speed of the monster
    int minDuration = 2.0;
    int maxDuration = 4.0;
    int rangeDuration = maxDuration - minDuration;
    int actualDuration = (arc4random() % rangeDuration) + minDuration;
 
    // Create the actions
    SKAction * actionMove = [SKAction moveTo:CGPointMake(-monster.size.width/2, actualY) duration:actualDuration];
    SKAction * actionMoveDone = [SKAction removeFromParent];
    [monster runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];
 
}

I’ve spelled out things in a verbose manner here to make things as easy to understand as possible. The first part should make sense based on what we’ve discussed so far: you do some simple calculations to determine where you want to create the object, set the position of the object, and add it to the scene the same way you did for the player sprite.

The new element here is adding actions. Just like in Cocos2D, Sprite Kit provides a lot of extremely handy built-in actions, such as move actions, rotate actions, fade actions, animation actions, and more. Here you use three actions on the monster:

  • moveTo:duration: You use this action to direct the object to move off-screen to the left. Note that you can specify the duration for how long the movement should take, and here you vary the speed randomly from 2-4 seconds.
  • removeFromParent: Sprite Kit comes with a handy action that removes a node from its parent, effectively “deleting it” from the scene. Here you use this action to remove the monster from the scene when it is no longer visible. This is important because otherwise you’d have an endless supply of monsters and would eventually consume all device resources.
  • sequence: The sequence action allows you to chain together a sequence of actions that are performed in order, one at a time. This way, you can have the moveTo: action perform first, and once it is complete perform the removeFromParent: action.

One last thing before you go. You need to actually call the method to create monsters! And to make things fun, let’s have monsters continuously spawning over time.

Sprite Kit does not have the ability to configure update callbacks every X seconds like Cocos2D does. It also does not pass in the delta time since the last update into the method (shocking, I know!)

However, you can mimic this behavior with a small bit of code. First add these properties to the private interface in MyScene.m:

@property (nonatomic) NSTimeInterval lastSpawnTimeInterval;
@property (nonatomic) NSTimeInterval lastUpdateTimeInterval;

You will use lastSpawnTimeInterval to keep track of the time since you last spawned a monster, andlastUpdateTimeInterval to keep track of the time since the last update.

Next, you’ll write a method that will be called every frame with the time since the last update. This won’t be called by default – you’ll have to write a separate method to call this, which you’ll do next.

- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast {
 
    self.lastSpawnTimeInterval += timeSinceLast;
    if (self.lastSpawnTimeInterval > 1) {
        self.lastSpawnTimeInterval = 0;
        [self addMonster];
    }
}

Here you simply add the time since the last update to the lastSpawnTimeInterval. Once it is greater than one second, you spawn a monster and reset the time.

Finally, add this method to call the above:

- (void)update:(NSTimeInterval)currentTime {
    // Handle time delta.
    // If we drop below 60fps, we still want everything to move the same distance.
    CFTimeInterval timeSinceLast = currentTime - self.lastUpdateTimeInterval;
    self.lastUpdateTimeInterval = currentTime;
    if (timeSinceLast > 1) { // more than a second since last update
        timeSinceLast = 1.0 / 60.0;
        self.lastUpdateTimeInterval = currentTime;
    }
 
    [self updateWithTimeSinceLastUpdate:timeSinceLast];
 
}

The update: method will be called automatically by Sprite Kit each frame.

This code actually comes from Apple’s Adventure sample. It passes in the current time, and you do some calculations to determine the time since the last frame. Note it does some sanity checking so that if an unexpectedly large amount of time has elapsed since the last frame, it resets the interval to 1/60th of a second to avoid strange behavior.

That’s it! Build and run the project, now you should see monsters happily moving across the screen:

Shooting Projectiles

At this point, the ninja is just begging for some action – so let’s add shooting! There are many ways you could implement shooting, but for this game you are going to make it so when the user taps the screen, it shoots a projectile from the player in the direction of the tap.

I want to use a moveTo: action to implement this to keep things at a beginner level, but in order to use this you have to do a little math. This is because the moveTo: requires you to give a destination for the projectile, but you can’t just use the touch point because the touch point represents just the direction to shoot relative to the player. You actually want to keep the bullet moving through the touch point until the bullet goes off-screen.

Here’s a picture that illustrates the matter:

So as you can see, you have a small triangle created by the x and y offset from the origin point to the touch point. You just need to make a big triangle with the same ratio – and you know you want one of the endpoints to be off the screen.

To run these calculations, it really helps if you have some basic vector math routines you can call (like methods to add and subtract vectors). However, Sprite Kit doesn’t have any by default so you’ll have to write your own.

Luckily they are very easy to write. Add these functions to the top of your file, right before the implementation:

static inline CGPoint rwAdd(CGPoint a, CGPoint b) {
    return CGPointMake(a.x + b.x, a.y + b.y);
}
 
static inline CGPoint rwSub(CGPoint a, CGPoint b) {
    return CGPointMake(a.x - b.x, a.y - b.y);
}
 
static inline CGPoint rwMult(CGPoint a, float b) {
    return CGPointMake(a.x * b, a.y * b);
}
 
static inline float rwLength(CGPoint a) {
    return sqrtf(a.x * a.x + a.y * a.y);
}
 
// Makes a vector have a length of 1
static inline CGPoint rwNormalize(CGPoint a) {
    float length = rwLength(a);
    return CGPointMake(a.x / length, a.y / length);
}

These are standard implementations of some vector math functions. If you’re confused about what’s going on here or are new to vector math, check out this quick vector math explanation.

Next, add a new method to the file:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
    // 1 - Choose one of the touches to work with
    UITouch * touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];
 
    // 2 - Set up initial location of projectile
    SKSpriteNode * projectile = [SKSpriteNode spriteNodeWithImageNamed:@"projectile"];
    projectile.position = self.player.position;
 
    // 3- Determine offset of location to projectile
    CGPoint offset = rwSub(location, projectile.position);
 
    // 4 - Bail out if you are shooting down or backwards
    if (offset.x <= 0) return;
 
    // 5 - OK to add now - we've double checked position
    [self addChild:projectile];
 
    // 6 - Get the direction of where to shoot
    CGPoint direction = rwNormalize(offset);
 
    // 7 - Make it shoot far enough to be guaranteed off screen
    CGPoint shootAmount = rwMult(direction, 1000);
 
    // 8 - Add the shoot amount to the current position       
    CGPoint realDest = rwAdd(shootAmount, projectile.position);
 
    // 9 - Create the actions
    float velocity = 480.0/1.0;
    float realMoveDuration = self.size.width / velocity;
    SKAction * actionMove = [SKAction moveTo:realDest duration:realMoveDuration];
    SKAction * actionMoveDone = [SKAction removeFromParent];
    [projectile runAction:[SKAction sequence:@[actionMove, actionMoveDone]]];
 
}

There’s a lot going on here, so let’s review it step by step.

  1. One of the cool things about SpriteKit is that it includes a category on UITouch with locationInNode:and previousLocationInNode: methods. These let you find the coordinate of a touch within a SKNode’s coordinate system. In this case, you use it to find out where the touch is within the scene’s coordinate system.
  2. You then create a projectile and place it where the player is to start. Note you don’t add it to the scene yet, because you have to do some sanity checking first – this game does not allow the ninja to shoot backwards.
  3. You then subtract the projectile’s current position from the touch location to get a vector from the current position to the touch location.
  4. If the X value is less than 0, this means the player is trying to shoot backwards. This is is not allowed in this game (real ninjas don’t look back!), so just return.
  5. Otherwise, it’s OK to add the projectile to the scene.
  6. Convert the offset into a unit vector (of length 1) by calling rwNormalize. This will make it easy to make a vector with a fixed length in the same direction, because 1 * length = length.
  7. Multiply the unit vector in the direction you want to shoot in by 1000. Why 1000? It will definitely be long enough to go past the edge of the screen :]
  8. Add the shoot amount to the current position to get where it should end up on the screen.
  9. Finally, create moveTo: and removeFromParent actions like you did earlier for the monster.

Build and run, and now your ninja should be able to fire away at the oncoming hordes!

Collision Detection and Physics: Overview

So now you have shurikens flying everywhere – but what your ninja really wants to do is to lay some smack down. So let’s add in some code to detect when your projectiles intersect your targets.

One of the nice things about Sprite Kit is it comes with a physics engine built right in! Not only are physics engines great for simulating realistic movement, but they are also great for collision detection purposes.

Let’s set up the game to use Sprite Kit’s physics engine to determine when monsters and projectiles collide. At a high level, here’s what you’re going to do:

  • Set up the physics world. A physics world is the simulation space for running physics calculations. One is set up on the scene by default, and you might want to configure a few properties on it, like gravity.
  • Create physics bodies for each sprite. In Sprite Kit, you can associate a shape to each sprite for collision detection purposes, and set certain properties on it. This is called a physics body. Note that the physics body does not have to be the exact same shape as the sprite. Usually it’s a simpler, approximate shape rather than pixel-perfect, since that is good enough for most games and performant.
  • Set a category for each type of sprite. One of the properties you can set on a physics body is a category, which is a bitmask indicating the group (or groups) it belongs to. In this game, you’re going to have two categories – one for projectiles, and one for monsters. Then later when two physics bodies collide, you can easily tell what kind of sprite you’re dealing with by looking at its category.
  • Set a contact delegate. Remember that physics world from earlier? Well, you can set a contact delegate on it to be notified when two physics bodies collide. There you’ll write some code to examine the categories of the objects, and if they’re the monster and projectile, you’ll make them go boom!

Now that you understand the battle plan, it’s time to put it into action!

Collision Detection and Physics: Implementation

Start by adding these two constants to the top of MyScene.m:

static const uint32_t projectileCategory     =  0x1 << 0;
static const uint32_t monsterCategory        =  0x1 << 1;

This is setting up the two categories you’ll need in a bit – one for the projectiles and one for the monsters.

Note: You may be wondering what the fancy syntax is here. Note that the category on Sprite Kit is just a single 32-bit integer, and acts as a bitmask. This is a fancy way of saying each of the 32-bits in the integer represents a single category (and hence you can have 32 categories max). Here you’re setting the first bit to indicate a projectile, and the next bit over to represent a monster.

Next, inside initWithSize add these lines after adding the player to the scene:

self.physicsWorld.gravity = CGVectorMake(0,0);
self.physicsWorld.contactDelegate = self;

This sets up the physics world to have no gravity, and sets the scene as the delegate to be notified when two physics bodies collide.

Inside the addMonster method, add these lines right after creating the monster sprite:

monster.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:monster.size]; // 1
monster.physicsBody.dynamic = YES; // 2
monster.physicsBody.categoryBitMask = monsterCategory; // 3
monster.physicsBody.contactTestBitMask = projectileCategory; // 4
monster.physicsBody.collisionBitMask = 0; // 5

Let’s go over what this does line by line.

  1. Creates a physics body for the sprite. In this case, the body is defined as a rectangle of the same size of the sprite, because that’s a decent approximation for the monster.
  2. Sets the sprite to be dynamic. This means that the physics engine will not control the movement of the monster – you will through the code you’ve already written (using move actions).
  3. Sets the category bit mask to be the monsterCategory you defined earlier.
  4. The contactTestBitMask indicates what categories of objects this object should notify the contact listener when they intersect. You choose projectiles here.
  5. The collisionBitMask indicates what categories of objects this object that the physics engine handle contact responses to (i.e. bounce off of). You don’t want the monster and projectile to bounce off each other – it’s OK for them to go right through each other in this game – so you set this to 0.

Next add some similar code to touchesEnded:withEvent:, right after the line setting the projectile’s position:

projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:projectile.size.width/2];
projectile.physicsBody.dynamic = YES;
projectile.physicsBody.categoryBitMask = projectileCategory;
projectile.physicsBody.contactTestBitMask = monsterCategory;
projectile.physicsBody.collisionBitMask = 0;
projectile.physicsBody.usesPreciseCollisionDetection = YES;

As a test, see if you can understand each line here and what it does. If not, just refer back to the points explained above!

As a second test, see if you can spot two differences. Answer below!

Solution Inside: What Are the Differences? Show
 
   
 

Next, add a method that will be called when the projectile collides with the monster. Note that nothing calls this automatically, you will be calling this later.

- (void)projectile:(SKSpriteNode *)projectile didCollideWithMonster:(SKSpriteNode *)monster {
    NSLog(@"Hit");
    [projectile removeFromParent];
    [monster removeFromParent];
}

All you do here is remove the projectile and monster from the scene when they collide. Pretty simple, eh?

Now it’s time to implement the contact delegate method. Add the following new method to the file:

- (void)didBeginContact:(SKPhysicsContact *)contact
{
    // 1
    SKPhysicsBody *firstBody, *secondBody;
 
    if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
    {
        firstBody = contact.bodyA;
        secondBody = contact.bodyB;
    }
    else
    {
        firstBody = contact.bodyB;
        secondBody = contact.bodyA;
    }
 
    // 2
    if ((firstBody.categoryBitMask & projectileCategory) != 0 &&
        (secondBody.categoryBitMask & monsterCategory) != 0)
    {
        [self projectile:(SKSpriteNode *) firstBody.node didCollideWithMonster:(SKSpriteNode *) secondBody.node];
    }
}

Since you set the scene as the contactDelegate of the physics world earlier, this method will be called whenever two physics bodies collide (and their contactTestBitMasks are set appropriately).

There are two parts to this method:

  1. This method passes you the two bodies that collide, but does not guarantee that they are passed in any particular order. So this bit of code just arranges them so they are sorted by their category bit masks so you can make some assumptions later. This bit of code came from Apple’s Adventure sample.
  2. Finally, it checks to see if the two bodies that collide are the projectile and monster, and if so calls the method you wrote earlier.

One last step – mark the private interface as implementing SKPhysicsContactDelegate to make the compiler happy:

@interface MyScene () 

Give it a build and run, and now when your projectiles intersect targets they should disappear!

Finishing Touches

You’re pretty close to having a workable (but extremely simple) game now. You just need to add some sound effects and music (since what kind of game doesn’t have sound!) and some simple game logic.

Sprite Kit does not come with an audio engine like Cocos2D does, but the good news it does come with a simple way to play sound effects via actions, and that you can play background music pretty easily with AVFoundation.

You already have some cool background music I made and an awesome pew-pew sound effect in your project, from the resources for this tutorial you added to your project earlier. You just need to play them!

To do this, add the following import to the top of ViewController.m:

@import AVFoundation;

This demonstrates the new modules feature in iOS 7 – basically by using the new @import keyword, you can bring in the header files (and library) for a framework in a single, efficient step. To learn more about this, check out Chapter 10 in iOS 7 by Tutorials, What’s New with Objective-C and Foundation.

Next add a new property and private interface:

@interface ViewController ()
@property (nonatomic) AVAudioPlayer * backgroundMusicPlayer;
@end

Add this code to viewWillLayoutSubviews, right after the call to [super viewWillLayoutSubviews]:

NSError *error;
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];

This is some simple code to start the background music playing with endless loops.

As for the sound effect, switch back to MyScene.m and add this line to the top of touchesEnded:withEvent::

[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];

Pretty handy, eh? You can play a sound effect with one line!

Now, let’s create a new scene and layer that will serve as your “You Win” or “You Lose” indicator. Create a new file with the iOS\Cocoa Touch\Objective-C class template, name the class GameOverScene, make it a subclass of SKScene, and click Next and then Create.

Then replace GameOverScene.h with the following code:

#import 
 
@interface GameOverScene : SKScene
 
-(id)initWithSize:(CGSize)size won:(BOOL)won;
 
@end

Here you imported the Sprite Kit header and marked that you are implementing a special initializer that takes a parameter of whether the user won the level or not in addition to the size.

Then replace GameOverLayer.m with the following code:

#import "GameOverScene.h"
#import "MyScene.h"
 
@implementation GameOverScene
 
-(id)initWithSize:(CGSize)size won:(BOOL)won {
    if (self = [super initWithSize:size]) {
 
        // 1
        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
 
        // 2
        NSString * message;
        if (won) {
            message = @"You Won!";
        } else {
            message = @"You Lose :[";
        }
 
        // 3
        SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        label.text = message;
        label.fontSize = 40;
        label.fontColor = [SKColor blackColor];
        label.position = CGPointMake(self.size.width/2, self.size.height/2);
        [self addChild:label];
 
        // 4
        [self runAction:
            [SKAction sequence:@[
                [SKAction waitForDuration:3.0],
                [SKAction runBlock:^{
                    // 5
                    SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
                    SKScene * myScene = [[MyScene alloc] initWithSize:self.size];
                    [self.view presentScene:myScene transition: reveal];
                }]
            ]]
        ];
 
    }
    return self;
}
 
@end

There are four parts to this method:

  1. Sets the background color to white, same as you did for the main scene.
  2. Based on the won parameter, sets the message to either “You Won” or “You Lose”.
  3. This is how you display a label of text to the screen with Sprite Kit. As you can see, it’s pretty easy – you just choose your font and set a few parameters.
  4. Finally, this sets up and runs a sequence of two actions. I’ve included them all inline here to show you how handy that is (instead of having to make separate variables for each action). First it waits for 3 seconds, then it uses the runBlock action to run some arbitrary code.
  5. This is how you transition to a new scene in Sprite Kit. First you can pick from a variety of different animated transitions for how you want the scenes to display – you choose a flip transition here that takes 0.5 seconds. Then you create the scene you want to display, and use the presentScene:transition: method on the self.view property.

So far so good, now you just need to set up your main scene to load the game over scene when appropriate.

To do this, first add an import for the new scene at the top of MyScene.m:

#import "GameOverScene.h"

Then inside addMonster, replace the last line that runs the actions on the monster with the following:

SKAction * loseAction = [SKAction runBlock:^{
    SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
    SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:NO];
    [self.view presentScene:gameOverScene transition: reveal];
}];
[monster runAction:[SKAction sequence:@[actionMove, loseAction, actionMoveDone]]];

This creates a new “lose action” that displays the game over scene when a monster goes off-screen. See if you understand each line here, if not refer to the explanation for the previous code block.

Also, another pop-quiz for you: why do you run the loseAction before actionMoveDone? Try reversing them to see what happens if you don’t know.

Solution Inside: Why is Lose Action First? Show
 
   
 

Now you should handle the win case too – don’t be cruel to your players! :] Add a new property to the private interface:

@property (nonatomic) int monstersDestroyed;

And add this to the bottom of projectile:didCollideWithMonster::

self.monstersDestroyed++;
if (self.monstersDestroyed > 30) {
    SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
    SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:YES];
    [self.view presentScene:gameOverScene transition: reveal];
}

Go ahead and give it a build and run, and you should now have win and lose conditions and see a game over scene when appropriate!

Where To Go From Here?

And that’s a wrap! Here is the full source code for this Sprite Kit tutorial for beginners.

I hope you enjoyed learning about Sprite Kit and are inspired to make your own game!

If you want to learn more about Sprite Kit, you should check out our book iOS Games by Tutorials. We’ll teach you everything you need to know – from physics, to tile maps, to particle systems, and even making your own level editor.

If you have any questions or comments about this tutorial, please join the discussion below!

你可能感兴趣的:(iOS开发)