How I Went From 0 to Game with Sprite Kit in iOS 7

Written by: on September 18

I’ll start off by saying, I am not a game developer and I never have been. That being said, at WWDC, when I saw what Apple did with the new Sprite Kit I was intrigued.

I decided to make a simple game that uses animation as a perk, not necessarily as the primary functionality. Enter the beloved and age old MineSweeper. The game mechanics are fairly simple; you have a grid of squares that are covered, each square can reveal a virtual bomb (game over!), a number indicating the number of bombs that touch the square, or a blank indicating there are no surrounding bombs.

This game has been done over and over again so I added a twist; you must “Beat the Sweeper”, an animated man who walks out periodically and drops a bomb on a random square to reveal it. If the man drops a bomb on a tile covering a bomb he reveals it and the game is over. If he drops a bomb on a safe unrevealed tile, it is revealed and you can continue.

walkingman

Don’t tell our Design team about this one…

Given the layout and function of the game, I knew I needed to be able to animate the following actions:

Movement: The Sweeper must be able to walk from one side of the screen to the other. And the bomb he throws must animate on a path. Particle: The bomb must smoke on the way down to its destination. Sprite Animation: The bomb must have an explosion and the man must appear to be walking while he is moving across the view.

Enter Sprite Kit

Adding Sprite Kit to a project is fairly painless and is just like any other Apple framework.

  1. Select the project at the top of the file list in Xcode
  2. Select the target and Build Phases panel
  3. Click “Link Binary With Libraries,” click the “+” (plus), find “SpriteKit.framework” and click “Add”
    enter-spritekit

To the Code

All animations with Sprite Kit start with the SKView which inherits from UIView, so it can be treated like any other view you add to your window. All animations performed will be rendered in this view.

Adding an SKView is as simple as adding any view:

(void)viewDidLoad
{
    self.skView = [[SKView alloc] initWithFrame:self.view.bounds];

    [self.view addSubview:self.skView];

    MSWScene *scene = [MSWScene sceneWithSize:size];

    // Set the scale mode to scale to fit the window
    scene.scaleMode = SKSceneScaleModeAspectFit;

    self.skView.backgroundColor = [SKColor clearColor];

    [self.skView presentScene:scene];

    self.scene = scene;
}

You will note above that I also created a scene which is a subclass of SKScene:

@interface MSWScene : SKScene
- (void)createSceneContents;
- (void)didMoveToView:(SKView *)view;
@end

@implementation MSWScene

- (void)didMoveToView:(SKView *)view
{
    [self createSceneContents];
}

- (void)createSceneContents
{
    // create SKSpriteNodes here and general layout for the scene
}

@end

A scene is the landscape from which everything starts. When you add a scene to an SKView, the - (void)didMoveToView:(SKView *)view method is called which is the signal to start building your scene and any SKSpriteNodes you need. I think of a scene as the background, canvas or location your animated objects will be created and managed from. An SKSpriteNode is typically going to be some object that you want to animate around the scene you build. You can transition between scenes; for example, when moving around a game through a maze, some scenes are off-screen in the map until you move them.

Animating Sprites

The sprite starts with the SKSpriteNode object, which can have an animated image sequence, move about the scene on a path, or emit particles. Fortunately, Apple made it fairly simple to make a sprite animate. To make an animation you either need a sprite sheet or multiple images that Sprite Kit can use to loop through the sequence.

For my walking man animation I chose to use multiple images. Here is his class:

static const int kDefaultNumberOfWalkFrames = 4;
static const float kShowCharacterFramesOverOneSecond = 1.0f/(float) kDefaultNumberOfWalkFrames;

@interface MSWManSpriteNode : SKSpriteNode
@property (nonatomic, strong, readonly) SKAction *animateManWalkingRight;
+ (instancetype)createWalkingMan;
- (void)walkToX:(CGFloat)x duration:(NSTimeInterval)duration completion:(MSWManSpriteNodeCompletion)completion;
@end

@interface MSWManSpriteNode ()
@property (nonatomic, strong, readwrite) SKAction *animateManWalkingRight;
@property (nonatomic, strong) NSArray *walkFramesRight;
@end

@implementation MSWManSpriteNode

+ (instancetype)createWalkingMan
{
    MSWManSpriteNode *manWalking = [[self class] spriteNodeWithImageNamed:@"MSWWalkRight-0001.png"];

    return manWalking;
}

+ (NSArray *)animationFramesForImageNamePrefix:(NSString *)baseImageName frameCount:(NSInteger)count
{
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:count];
    for (NSInteger index = 1; index <= count; ++index) {
        NSString *imageName = [NSString stringWithFormat:@"%@%04d.png", baseImageName, index];

        SKTexture *texture = [SKTexture textureWithImageNamed:imageName];

        [array addObject:texture];
    }

    return [NSArray arrayWithArray:array];
}

#pragma mark - Public

- (void)walkToX:(CGFloat)x duration:(NSTimeInterval)duration completion:(MSWManSpriteNodeCompletion)completion
{
    SKAction *walk = self.position.x > x ? self.animateManWalkingRight : self.animateManWalkingLeft;
    SKAction *animateWalk = [SKAction repeatActionForever:walk];

    [self runAction:animateWalk withKey:@"walk"];
    SKAction *animateRightGroup = [SKAction group:@[
        [SKAction moveToX:x duration:duration],
    ]];

    [self runAction:animateRightGroup completion:^{
        [self removeActionForKey:@"walk"];

        if (completion)
            completion();
    }];
}

#pragma mark - [Accessor Overrides]

- (SKAction *)animateManWalkingRight
{
    if (_animateManWalkingRight == nil) {
        self.walkFramesRight = [[self class] animationFramesForImageNamePrefix:@"MSWWalkRight-" frameCount:kDefaultNumberOfWalkFrames];
        _animateManWalkingRight = [SKAction animateWithTextures:self.walkFramesRight timePerFrame:kShowCharacterFramesOverOneSecond resize:YES restore:NO];
    }

    return _animateManWalkingRight;
}

Above you can see that I’ve implemented a factory method to create my walking man with his first frame, included a helper method to load all of the images for the animation sequence and a method with completion to make him walk to a coordinate over a particular duration. Later in the code I will execute the following to create him in the scene:

In my MSWScene class:

- (void)createWalkingManFromRight
{
    self.manWalking = [MSWManSpriteNode createWalkingMan];

    // start him out left just off screen
    CGFloat y = self.size.height - (self.manWalking.frame.size.height / 2) - 10.0f; // 5 px padding
    CGFloat x = self.size.width + 16.0f;

    self.manWalking.position = CGPointMake(x, y);

    [self addChild:self.manWalking];
}

SKActions

SKActions are the magic; beyond simply animating an image sequence, they are the control for all things changeable in the Sprite Kit universe. SKActions can be chained to execute blocks on completion, manipulate SKSpriteNodes movements, begin animation image sequences and much much more. They are the building blocks of a sprite’s interaction with their environment. You can run actions as simultaneous groups or sequences to create just about any effect, movement or animation you can dream up. As can be seen in the walkToX method above, I am executing the image animation sequence at the same time I am animating the walking man across the screen and then removing the walking sequence when he stops.

SKActions can also be used to animate an SKSpriteNode on a path, which in my game I used in order to create the effect of the man tossing the mine from his hand on a curve down to the random row it will explode on.

Quite a complex example, but here is how I did the animation:

- (void)tossMineAtPoint:(CGPoint)point completion:(dispatch_block_t)completion
{
    static BOOL tossing = NO;

    if (tossing)
        return;

    tossing = YES;

    BOOL fromRight = (point.x < self.view.frame.size.width / 2);
    [self createWalkingManFromRight:fromRight];

    // figure out where walking man needs to go
    CGPoint mineToHitPoint = [self convertPointFromView:point];
    CGPoint walkToPoint = CGPointMake(point.x + (fromRight ? 100.0f : -100.0f), 16.0f);
    CGPoint walkToScenePoint = [self convertPointFromView:walkToPoint];

    CGFloat halfWidth = 0.0f;
    CGPoint tangentPoint1 = CGPointMake(walkToPoint.x + (fromRight ? -(100.0f + halfWidth) : (100.0f + halfWidth)), walkToPoint.y);
    CGPoint tangentPoint2 = CGPointMake(tangentPoint1.x, walkToPoint.y + 100.0f);

    [self.manWalking walkToX:walkToScenePoint.x duration:5.0f completion:^{
        [self.manWalking addChild:[self.manWalking emitSmokeFromRight:fromRight]];

        CGMutablePathRef pathRef = CGPathCreateMutable();
        CGPathMoveToPoint(pathRef, NULL, walkToScenePoint.x, walkToScenePoint.y);

        CGPoint tangentScenePoint1 = [self convertPointFromView:tangentPoint1];
        CGPoint tangentScenePoint2 = [self convertPointFromView:tangentPoint2];

        CGPathAddArcToPoint(pathRef, NULL, tangentScenePoint1.x, tangentScenePoint1.y, tangentScenePoint2.x, tangentScenePoint2.y, 50.0f);
        CGPathAddLineToPoint(pathRef, NULL, mineToHitPoint.x, mineToHitPoint.y);

        MSWBombSprite *bombSprite = [MSWBombSprite createSprite];
        bombSprite.position = walkToScenePoint;
        [self addChild:bombSprite];

        [bombSprite setScale:0.1f];

        SKAction *group = [SKAction group:@[
            [SKAction scaleBy:5.0f duration:1.0f],
            [SKAction followPath:pathRef asOffset:NO orientToPath:NO duration:1.0f]
        ]];

        [bombSprite runAction:group completion:^{
            [bombSprite runAction:[SKAction fadeOutWithDuration:2.0f]];
            [self explodeMineAtPoint:point];
            CGPathRelease(pathRef);

            [self.manWalking walkToX:fromRight ? -16.0f : (self.size.width + 16.0f) duration:2.0f completion:^{
                [self.manWalking removeFromParent];
                tossing = NO;
            }];

            [bombSprite removeFromParent];

            if (completion)
                completion();

        }];
    }];
}

Particle Emitters

As I wanted it to appear that my walking man was dropping a real bomb on the unsuspecting gamers’ game board, I needed smoke to be emitted out from its fuse. Apple has provided several emitters with Sprite Kit that can be added to the project in the form of an SKS file; everything from smoke to fire. Particle emitters can be added to the project as a new file under the iOS Resource group.

addemitter

You can edit emitters’ individual properties, like number of particles emitted, direction, and speed, all in Xcode 5′s built in editor or from code when you load them.

emitteredit

- (SKEmitterNode *)emitSmokeFromRight:(BOOL)fromRight
{
    NSString *smokePath = [[NSBundle mainBundle] pathForResource:@"MSWSmokeEmitter" ofType:@"sks"];
    SKEmitterNode *smoke = [NSKeyedUnarchiver unarchiveObjectWithFile:smokePath];

    smoke.emissionAngle = fromRight ? deg2rad(170.0f) : smoke.emissionAngle;
    smoke.numParticlesToEmit = 2.0f;

    return smoke;
}

Sprite Kit Coordinate System

It should be noted that the coordinate system is not the same in a Sprite Kit scene as they are in a UIView. By default, the coordinate system is 0,0 center based. This means that anything to the left of center is a negative and anything below center is negative, starting from 0.

cartesian_coords_2x

Conveniently, Apple has given us conversion methods in SKScene that will convert between view coordinates and scene coordinates:

- (CGPoint)convertPointFromView:(CGPoint)point;
- (CGPoint)convertPointToView:(CGPoint)point;

Physics

What game would be complete without physics (besides mine)? Not only does Sprite Kit contain excellent tools for creating scenes and animating sprites, but it also includes a world of physics. I didn’t have a need in my small game for physics so this addition is a bit beyond the scope of the article, however, it is worth mentioning in the spirit of iOS 7’s grand release that Apple has provided everything from gravity to collision detection and reaction in Sprite Kit.

What will you do with Sprite Kit?

As can be seen, Apple has added a valuable framework that previously existed as several different add-on third-party libraries to expand the power of sprite animation to iOS 7. I stated above that I am not a game developer, but the power and ease-of-use of Sprite Kit has definitely turned my head. I am excited to see what this new framework will do to inspire future game developers. I’m excited to see what you will do with it!

This is the tenth part in an 11-part Developer’s Guide to iOS 7. You can find the full guide here. For more information on how Double Encore can help you prepare your company for the changes in iOS 7, please email us.

Greg Carter

Gregory Carter is a Software Engineer at Double Encore, Inc., a leading mobile development company. Greg has developed iOS apps since 2007 with a focus on backend engineering and has a 15 year history of scalable front and backend systems, library and data development. As a side hobby he dabbles in interface, graphics design, and semi-professional dart games with his coworkers.

Article


Add your voice to the discussion: