Getting Started with Sprite Kit - Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)

Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)

Chapter 17. Getting Started with Sprite Kit

In iOS 7, Apple introduced Sprite Kit, a framework for the high-performance rendering of 2D graphics. That sounds a bit like Core Graphics and Core Animation, so what’s new here? Well, unlike Core Graphics (which is focused on drawing graphics using a painter’s model) or Core Animation (which is focused on animating attributes of GUI elements), Sprite Kit is focused on a different area entirely: video games! Sprite Kit is built on top of OpenGL, a technology present in many computing platforms that allows modern graphics hardware to write graphics bitmaps into a video buffer at incredible speeds. With Sprite Kit, you get the great performance characteristics of OpenGL, but without needing to dig into the depths of OpenGL coding.

This is Apple’s first foray into the graphical side of game programming in the iOS era. It was released for iOS 7 and OS X 10.9 (Mavericks) at the same time, and it provides the same API on both platforms, so that apps written for one can be easily ported to the other. Although Apple has never before supplied a framework quite like Sprite Kit, it has clear similarities to various open source libraries such as Cocos2D. If you’ve used Cocos2D or something similar in the past, you’ll feel right at home.

Sprite Kit does not implement a flexible, general-purpose drawing system like Core Graphics. There are no methods for drawing paths, gradients, or filling spaces with color. Instead, what you get is a scene graph (analogous to UIKit’s view hierarchy); the ability to transform each graph node’s position, scale, and rotation; and the ability for each node to draw itself. Most drawing occurs in an instance of the SKSprite class (or one of its subclasses), which represents a single graphical image ready for putting on the screen.

In this chapter, we’re going to use Sprite Kit to build a simple shooting game called TextShooter. Instead of using premade graphics, we’re going to build our game objects with pieces of text, using a subclass of SKSprite that is specialized for just this purpose. Using this approach, you won’t need to pull graphics out of a project library or anything like that. The app we make will be simple in appearance, but easy to modify and play with.

Simple Beginnings

Let’s get the ball rolling. In Xcode, press imageN or select File image New image File… and choose the Game template from the iOS section. Press Next, name your project TextShooter, set Devices to Universal and Game Technology to SpriteKit, and create the project. While you’re here, it’s worth looking briefly at the other available technology choices. OpenGL ES and Metal (the latter of which is new in iOS 8) are low-level graphics APIs that give you almost total control over the graphics hardware, but are much more difficult to use than Sprite Kit. Whereas Sprite Kit is a 2D API, SceneKit (also new in iOS 8) is a toolkit that you can use to build 3D graphics applications. After you’ve read this chapter, it’s worth checking out the SceneKit documentation athttps://developer.apple.com/library/prerelease/ios/documentation/SceneKit/Reference/SceneKit_Framework/index.html if you have any interest in 3D game programming.

If you run the TextShooter project now, you’ll see the default Sprite Kit application, which is shown in Figure 17-1. Initially, you’ll just see the “Hello, World” text. To make things slightly (but only slightly) more interesting, touch the screen to add some rotating spaceships. Over the course of this chapter, we’ll replace everything in this template and progressively build up a simple application of our own.

image

Figure 17-1. The default Sprite Kit app in action. Some text is displayed in the center of the screen, and each tap on the screen puts a rotating graphic of a fighter jet at that location

Now let’s take a look at the project that Xcode created. You’ll see it has a pretty standard-looking AppDelegate class and a small view controller class called GameViewController that does some initial configuration of an SKView object. This object, which is loaded from the application’s storyboard, is the view that will display all our Sprite Kit content. Here’s the code from the GameViewController viewDidLoad method that initializes the SKView:

- (void)viewDidLoad
{
[super viewDidLoad];

// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
skView.ignoresSiblingOrder = YES;

// Create and configure the scene.
GameScene *scene = [GameScene nodeWithFileNamed:@"GameScene"];
scene.scaleMode = SKSceneScaleModeAspectFill;

// Present the scene.
[skView presentScene:scene];
}

The first few lines get the SKView instance from the storyboard and configure it to show some performance characteristics while the game is running. Sprite Kit applications are constructed as a set of scenes, represented by the SKScene class. When developing with Sprite Kit, you’ll probably make a new SKScene subclass for each visually distinct portion of your app. A scene can represent a fast-paced game display with dozens of objects animating around the screen, or something as simple as a start menu. We’ll see multiple uses of SKScene in this chapter. The template generates an initially empty scene in the shape of a class called GameScene.

The relationship between SKView and SKScene has some parallels to the UIViewController classes we’ve been using throughout this book. The SKView class acts a bit like UINavigationController, in the sense that it is sort of a blank slate that simply manages access to the display for other controllers. At this point, things start to diverge, however. Unlike UINavigationController, the top-level objects managed by SKView aren’t UIViewController subclasses. Instead, they’re subclasses of SKScene, which knows how to manage a graph of objects that can be displayed, acted upon by the physics engine, and so on.

The next part of the viewDidLoad method creates the initial scene:

// Create and configure the scene.
GameScene *scene = [GameScene nodeWithFileNamed:@"GameScene"];

There are two ways to create a scene—you can manually allocate and initialize an instance programmatically, or you can load one from a Sprite Kit scene file. The Xcode template takes the latter approach—it generates a Sprite Kit scene file called GameScene.sks containing an archived copy of an SKScene object. SKScene, like most of the other Sprite Kit classes, conforms to the NSCoder protocol, which we discussed in Chapter 13. The GameScene.sks file is just a standard archive, which you can read and write using the NSKeyedUnarchiver andNSKeyedArchiver classes. Usually, though, you’ll use the SKScene nodeWithFileNamed: method, which loads the SKScene from the archive for you and initializes it as an instance of the concrete subclass on which it is invoked—in this case, the archived SKScene data is used to initialize the GameScene object.

You may be wondering why the template code goes to the trouble of loading an empty scene object from the scene file when it could have just created one. The reason is the Xcode Sprite Kit Level Designer, which lets you design a scene much like you construct a user interface in Interface Builder. Having designed your scene, you save it to the scene file and run your application again. This time, of course, the scene is not empty and you should see the design that you created in the Level Designer. Having loaded the initial scene, you are at liberty to programmatically add additional elements to it. We’ll be doing a lot of that in this chapter. Alternatively, if you don’t find the Level Designer useful, you can build all your scenes completely in code.

If you select the GameScene.sks file in the Project Navigator, Xcode opens it in the Level Designer, as shown in Figure 17-2.

image

Figure 17-2. The Xcode Sprite Kit Level Designer, showing the initially empty GameScene

The scene is displayed in the editor area—right now, it’s just an empty yellow rectangle on a gray background. To the right of it is the SKNode Inspector, which you can use to set properties of the node that’s selected in the editor. Sprite Kit scene elements are all nodes—instances of theSKNode class. SKScene itself is a subclass of SKNode. Here, the SKScene node is selected, so the SKNode Inspector is displaying its properties. Below the inspector, in the bottom right, is the usual Xcode Object Library, which is automatically filtered to show only the types of objects you can add to a Sprite Kit scene. You design your scene by dragging objects from here and dropping them onto the editor.

Tip You may be wondering why the method used to load the scene is from the scene file called nodeWithFileNamed: and not sceneWithFileNamed:. Although the template code only uses this method to load the initial scene, it can actually load any Sprite Kit node, which means you can archive a node to a file and load it back later. In fact, you can use the Level Designer to create a scene consisting of a single node that you can save and load into the scene graph later. This allows you to design complex nodes without having to write any code.

Now let’s go back and finish up our discussion of the viewDidLoad method.

scene.scaleMode = SKSceneScaleModeAspectFill;

// Present the scene.
[skView presentScene:scene];

These two lines of code set the scene’s scale mode and make the scene visible. Let’s talk about those two things in reverse order. In order for a scene and its content to be visible and active, it must be presented by an SKView. To present a scene, you call the SKView’s presentScene:method. An SKView can display only one scene at a time, so calling this method when there’s already a presented scene causes the new scene to immediately replace the old one. If you are switching from one scene to another, you should probably prefer to use thepresentScene:transition: method, which animates the scene change. You’ll see examples of this later in the chapter. In this case, since we are making the initial scene visible, there is nothing to transition from, so it’s acceptable to use the presentScene: method.

Now let’s talk about the scene’s scaleMode property. If you look back at Figure 17-2, you’ll see that the default scene in the Level Designer is 1024 points wide and 768 points high—the same as the size of an iPad screen. That’s all well and good if you plan to run your game only in landscape mode on an iPad, but what about portrait mode, or other screen sizes, like on the iPhone? How should you adapt the scene for the size of the screen that your application is running on? There is no simple answer to that question. There are four different ways to adjust the size of the scene when it’s presented in an SKView, corresponding to the four values of the SKSceneScaleMode enumeration. To see what each scale mode does, let’s create another Sprite Kit project and experiment with it. Using the same steps as before, create a Sprite Kit project, call itResizeModes, and select the GameScene.sks file in the Project Navigator. At this point, your Xcode window should look like Figure 17-2.

In the Object Library, locate a Label node and drag it into the center of the scene. In the SKNode Inspector, use the Text, field to change the label’s text to Center. Drag another label to the bottom left of the scene, placing it carefully so that it’s exactly in the corner of the scene. Change itstext property to Bottom Left. Drag a third label to the top right of the scene and change its text to Top Right. Drag a couple more labels to the top and bottom of the scene and name them Top and Bottom, respectively. You can change the colors and fonts associated with the labels to make the text more visible, if necessary. When you’re done, you should have something like the scene shown in Figure 17-3.

image

Figure 17-3. Using the Sprite Kit Level Designer to add nodes to a scene

Tip If you can’t see all of the scene in the Editor area, you can use the -/=/+ buttons at the bottom right of the Editor to zoom out until you have enough of the scene in view to work comfortably with it.

Select GameScene.m in the Project Navigator and delete the didMoveToView: method. This method contains the code that adds the “Hello, World” label to the scene, which we don’t need. Next, select GameViewController.m and locate the line of code in the viewDidLoad method that sets the scaleMode of the SKScene object. As you can see, it’s initially set to SKSceneScaleModeAspectFill. Run the application on an iPhone simulator (or device) with this scale mode set, and then edit the code and run it three more times, using the valuesSKSceneScaleModeAspectFit, SKSceneScaleModeFill, and SKSceneScaleModeResizeFill. The results are shown in Figure 17-4.

image

Figure 17-4. Comparing the four scene rescale modes

Here’s what these modes do:

· SKSceneScaleModeAspectFill resizes the scene so that it fills the screen while preserving its aspect (width-to-height ratio). As you can see in Figure 17-4, this mode ensures that every pixel of the SKView is covered, but loses part of the scene—in this case, the scene has been cropped on the left and right. The content of the scene is also scaled, so the text is smaller than in the original scene, but its position relative to the scene is preserved.

· SKSceneScaleModeAspectFit also preserves the scene’s aspect ratio, but ensures that the whole scene is visible. The result is a letter-box view, with parts of the SKView visible above and below the scene content.

· SKSceneScaleModeFill scales the scene along both axes so that it exactly fits the view. This ensures that everything in the scene is visible, but since the aspect ratio of the original scene is not preserved, there may be unacceptable distortion of the content. Here, you can see that the text has been horizontally compressed.

· Finally, SKSceneScaleModeResizeFill places the bottom-left corner of the scene in the bottom-left corner of the view and leaves it at its original size.

Which of these rescale modes is best for you depends on the needs of your application. If none of them work, there are two other possibilities. First, you can elect to support a fixed set of screen sizes, create an individual design tailored to each of them, store it in its own .sks file, and load the scene from the correct file when it’s needed. Secondly, you can simply create the scene in code, make it the same size as the SKView in which it’s being presented, and populate it with nodes programmatically. This only works if your game doesn’t depend on the exact relative positions of its elements. To illustrate how this approach works, we’ll use it for the TextShooter application.

Initial Scene Customization

Open the TextShooter project and select the GameScene class. We don’t need most of the code that the Xcode template generated for us, so let’s remove it. First, delete the entire didMoveToView: method. This method is called whenever the scene is presented in an SKView and it is typically used to make last-minute changes to the scene before it becomes visible. Next, take away most of the touchesBegan:withEvent: method, leaving just the for loop and the first line of code it contains. At this point, your GameScene class should look like the following (the compiler will warn that location is an unused variable—don’t worry about that, because we’ll fix it later):

@implementation GameScene

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */

for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
}
}

-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
}

@end

Since we’re not going to load our scene from GameScene.sks, we need a method that will create a scene for us, with some initial content. We’ll also need to add properties for the current game-level number, the number of lives the player has, and a flag to let us know whether the level is finished. Add the following bold lines to GameScene.h:

@interface GameScene : SKScene

@property (assign, nonatomic) NSUInteger levelNumber;
@property (assign, nonatomic) NSUInteger playerLives;
@property (assign, nonatomic) BOOL finished;

+ (instancetype)sceneWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber;
- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber;

@end

Now switch over to GameScene.m, where we’ll implement the two new methods that we just declared. Add the following code in the implementation section of the file:

+ (instancetype)sceneWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
return [[self alloc] initWithSize:size levelNumber:levelNumber];
}

- (instancetype)initWithSize:(CGSize)size {
return [self initWithSize:size levelNumber:1];
}

- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
if (self = [super initWithSize:size]) {
_levelNumber = levelNumber;
_playerLives = 5;

self.backgroundColor = [SKColor whiteColor];

SKLabelNode *lives = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
lives.fontSize = 16;
lives.fontColor = [SKColor blackColor];
lives.name = @"LivesLabel";
lives.text = [NSString stringWithFormat:@"Lives: %lu",
(unsigned long)_playerLives];
lives.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
lives.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeRight;
lives.position = CGPointMake(self.frame.size.width,
self.frame.size.height);
[self addChild:lives];

SKLabelNode *level = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
level.fontSize = 16;
level.fontColor = [SKColor blackColor];
level.name = @"LevelLabel";
level.text = [NSString stringWithFormat:@"Level: %lu",
(unsigned long)_levelNumber];
level.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
level.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
level.position = CGPointMake(0, self.frame.size.height);
[self addChild:level];
}
return self;
}

The first method, sceneWithSize:levelNumber:, gives us a factory method that will work as a shorthand for creating a level and setting its level number at once. In the second method, initWithSize:, we override the class’s default initializer, passing control to the third method (and passing along a default value for the level number). That third method in turn calls the designated initializer from its superclass’s implementation. This may seem like a roundabout way of doing things, but it’s a common pattern when you want to add new initializers to a class while still using the class’s designated initializer.

The third method we added, initWithSize:levelNumber:, is where we set up the basic configuration of our level scene. First, we set the values of a couple of instance variables from the parameters that were passed in. Second, we set the scene’s background color. Note that we’re using a class called SKColor instead of UIColor here. In fact, SKColor isn’t really a class at all; it’s a sort of alias that can be used in place of either UIColor for an iOS app or NSColor for an OS X app. This allows us to port games between iOS and OS X a little more easily.

After that, we create two instances of a class called SKLabelNode. This is a handy class that works somewhat like a UILabel, allowing us to add some text to the scene and letting us choose a font, set a text value, and specify some alignments. We create one label for displaying the number of lives at the upper right of the screen and another that will show the level number at the upper left of the screen. Look closely at the code that we use to position these labels. Here is the code that sets the position of the lives label:

lives.position = CGPointMake(self.frame.size.width,
self.frame.size.height);

If you think about the points we’re passing in as the position for this label, you may be surprised to see that we’re passing in the scene’s height. In UIKit, positioning anything at the height of a UIView would put it at the bottom of that view; but in Scene Kit, the y axis is flipped—the coordinate origin is at the bottom left of the scene and the y axis points upward. As a result, the maximum value of the scene’s height is a position at the top of the screen instead. What about the label’s x coordinate? We’re setting that to be the width of the view. If you did that with aUIView, the view would be positioned just off the right side of the screen. That doesn’t happen here, because we also did this:

lives.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeRight;

Setting the horizontalAlignmentMode property of the SKLabelNode to SKLabelHorizontalAlignmentModeRight moves the point of the label node that’s used to position it (it’s actually a property called position) to the right of the text. Since we want the text to be right justified on the screen, we therefore need to set the x coordinate of the position property to be the width of the scene. By contrast, the text in the level label is left-aligned and we position it at the left edge of the scene by setting its x coordinate to zero:

level.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
level.position = CGPointMake(0, self.frame.size.height);

You’ll also see that we gave each label a name. This works similar to a tag or identifier in other parts of UIKit, and it will let us retrieve those labels later by asking for them by name.

Now select GameViewController.m and make the following changes to the viewDidLoad method:

- (void)viewDidLoad
{
[super viewDidLoad];

// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
skView.ignoresSiblingOrder = YES;

// Create and configure the scene.
GameScene *scene = [GameScene nodeWithFileNamed:@"GameScene"];
scene.scaleMode = SKSceneScaleModeAspectFill;
GameScene *scene = [GameScene sceneWithSize:self.view.frame.size
levelNumber:1];

// Present the scene.
[skView presentScene:scene];
}

Instead of loading the scene from the scene file, we’re using the sceneWithSize:levelNumber: method that we just added to GameScene to create and initialize the scene and make it the same size as the SKView. Since the view and scene are the same size, there is no longer any need to set the scene’s scaleMode property, so we can remove the line of code that does that.

At the bottom of GameViewController.m, you’ll see the following method that the template included for us:

- (BOOL)prefersStatusBarHidden {
return YES;
}

Returning YES from this method makes the iOS status bar disappear while our game is running, which is usually what you want for action games like this. Now run the game and you’ll see that we have a very basic structure in place, as shown in Figure 17-5.

image

Figure 17-5. Our game doesn’t have much fun factor right now, but at least it has a high frame rate!

Tip The node count and frame rate at the bottom right of the scene are useful for debugging, but you don’t want them to be there when you release your game! You can switch them off by setting the showsFPS and showsNodeCount properties of the SKView to NO in theviewDidLoad method of GameViewController. There are some other SKView properties that let you get more debugging information—refer to the API documentation for the details.

Player Movement

Now it’s time to add a little interactivity. We’re going to make a new class that represents a player. It will know how to draw itself using internal components, as well as how to move to a new location in a nicely animated way. Next, we’ll insert an instance of the new class into the scene and write some code to let the player move the object around by touching the screen.

Every object that’s going to be part of our scene must be a subclass of SKNode. Thus, you’ll use Xcode’s File menu to create a new Cocoa Touch class named PlayerNode that’s a subclass of SKNode. In the nearly empty PlayerNode.m file that’s created, add the following methods:

- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Player %p", self];
[self initNodeGraph];
}
return self;
}

- (void)initNodeGraph {
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
label.fontColor = [SKColor darkGrayColor];
label.fontSize = 40;
label.text = @"v";
label.zRotation = M_PI;
label.name = @"label";

[self addChild:label];

}

Our PlayerNode doesn’t display anything itself, because a plain SKNode has no way to do any drawing of its own. Instead, the init method sets up a subnode that will do the actual drawing. This subnode is another instance of SKLabelNode, just like the one we created for displaying the level number and the number of lives remaining. SKLabelNode is a subclass of SKNode that does know how to draw itself. Another such subclass is SKSpriteNode. We’re not setting a position for the label, which means that its position is coordinate (0, 0). Just like views, eachSKNode lives in a coordinate system that is inherited from its parent object. Giving this node a zero position means that it will appear on-screen at the PlayerNode instance’s position. Any non-zero values would effectively be an offset from that point.

We also set a rotation value for the label, so that the lowercase letter “v” it contains will be shown upside-down. The name of the rotation property, zRotation, may seem a bit surprising; however, it simply refers to the z axis of the coordinate space in use with Sprite Kit. You only see the x and y axes on screen, but the z axis is useful for ordering items for display purposes, as well as for rotating things around. The values assigned to zRotation need to be in radians instead of degrees, so we assign the value M_PI, which is equivalent to the mathematical value π. Since πradians are equal to 180°, this is just what we want.

Adding the Player to the Scene

Now switch back to GameScene.m. Here, we’re going to add an instance of PlayerNode to the scene. Start off by importing the new class’s header and adding a property inside a new class extension:

#import "GameScene.h"
#import "PlayerNode.h"

@interface GameScene ()

@property (strong, nonatomic) PlayerNode *playerNode;

@end

Continue by adding the following bold code near the end of the initWithSize:levelNumber: method. Be sure to put it before the return self and before the right curly brace above it:

[self addChild:level];
_playerNode = [PlayerNode node];
_playerNode.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetHeight(self.frame) * 0.1);

[self addChild:_playerNode];
}
return self;
}

If you build and run the app now, you should see that the player appears near the lower middle of the screen, as shown in Figure 17-6.

image

Figure 17-6. An upside-down “v” to the rescue!

Handling Touches: Player Movement

Next, we’re going to put some logic back into the touchesBegan:withEvent: method, which we earlier left nearly empty. Insert the bold lines shown here in GameScene.m (you’ll get a compiler error when you add this code—we’ll fix it shortly):

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */

for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
if (location.y < CGRectGetHeight(self.frame) * 0.2 ) {
CGPoint target = CGPointMake(location.x,
self.playerNode.position.y);
[self.playerNode moveToward:target];
}
}
}

The preceding snippet uses any touch location in the lower fifth of the screen as the basis of a new location toward which you want the player node to move. It also tells the player node to move toward it. The compiler complains because we haven’t defined the player node’s moveToward:method yet. So, start by declaring the method in PlayerNode.h, like this:

#import <SpriteKit/SpriteKit.h>

@interface PlayerNode : SKNode

// returns duration of future movement
- (void)moveToward:(CGPoint)location;

@end

Next, switch over to PlayerNode.m and add the following implementation:

- (void)moveToward:(CGPoint)location {
[self removeActionForKey:@"movement"];

CGFloat distance = PointDistance(self.position, location);
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
CGFloat duration = 2.0 * distance / screenWidth;

[self runAction:[SKAction moveTo:location duration:duration]
withKey:@"movement"];
}

We’ll skip the first line for now, returning to it shortly. This method compares the new location to the current position and figures out the distance and the number of pixels to move. Next, it figures out how much time the movement should take, using a numeric constant to set the speed of the overall movement. Finally, it creates an SKAction to make the move happen. SKAction is a part of Sprite Kit that knows how to make changes to nodes over time, letting you easily animate a node’s position, size, rotation, transparency, and more. In this case, we are telling the player node to run a simple movement action over a particular duration, and then assigning that action to the key @"movement." As you see, this key is the same as the key used in the first line of this method to remove an action. We started off this method by removing any existing action with the same key, so that the user can tap several locations in quick succession without spawning a lot of competing actions trying to move in different ways!

Geometry Calculations

Now you’ll notice that we’ve introduced another problem, because Xcode can’t find any function called PointDistance(). This is one of several simple geometric functions that our app will use to perform calculations using points, vectors, and floats. Let’s put this in place now. Use Xcode to make a new file, this time a Header File from the iOS section. Name it Geometry.h and give it the following content:

#ifndef TextShooter_Geometry_h
#define TextShooter_Geometry_h

// Takes a CGVector and a CGFLoat.
// Returns a new CGFloat where each component of v has been multiplied by m.
static inline CGVector VectorMultiply(CGVector v, CGFloat m) {
return CGVectorMake(v.dx * m, v.dy * m);
}

// Takes two CGPoints.
// Returns a CGVector representing a direction from p1 to p2.
static inline CGVector VectorBetweenPoints(CGPoint p1, CGPoint p2) {
return CGVectorMake(p2.x - p1.x, p2.y - p1.y);
}

// Takes a CGVector.
// Returns a CGFloat containing the length of the vector, calculated using
// Pythagoras' theorem.
static inline CGFloat VectorLength(CGVector v) {
return sqrtf(powf(v.dx, 2) + powf(v.dy, 2));
}

// Takes two CGPoints. Returns a CGFloat containing the distance between them,
// calculated with Pythagoras' theorem.
static inline CGFloat PointDistance(CGPoint p1, CGPoint p2) {
return sqrtf(powf(p2.x - p1.x, 2) + powf(p2.y - p1.y, 2));
}

#endif

These are simple implementations of some common operations that are useful in many games: multiplying vectors, creating vectors pointing from one point to another, and calculating distances. To let the code use these, just add the following import near the top of PlayerNode.m:

#import "Geometry.h"

Now build and run the app. After the player’s ship appears, tap anywhere in the bottom portion of the screen to see that the ship slides left or right to reach the point you tapped. You can tap again before the ship reaches its destination, and it will immediately begin a new animation to move toward the new spot. That’s fine, but wouldn’t it be nice if the player’s ship were a bit livelier in its motion?

Wobbly Bits

Let’s give the ship a bit of a wobble as it moves by adding another animation. Add the bold lines to PlayerNode’s moveToward: method.

- (void)moveToward:(CGPoint)location {
[self removeActionForKey:@"movement"];
[self removeActionForKey:@"wobbling"];

CGFloat distance = PointDistance(self.position, location);
CGFloat pixels = [UIScreen mainScreen].bounds.size.width;
CGFloat duration = 2.0 * distance / pixels;

[self runAction:[SKAction moveTo:location duration:duration]
withKey:@"movement"];

CGFloat wobbleTime = 0.3;
CGFloat halfWobbleTime = wobbleTime * 0.5;
SKAction *wobbling = [SKAction
sequence:@[[SKAction scaleXTo:0.2 duration:halfWobbleTime],
[SKAction scaleXTo:1.0
duration:halfWobbleTime]
]];
NSUInteger wobbleCount = duration / wobbleTime;

[self runAction:[SKAction repeatAction:wobbling count:wobbleCount]
withKey:@"wobbling"];
}

What we just did is similar to the movement action we created earlier, but it differs in some important ways. For the basic movement, we simply calculated the movement duration, and then created and ran a movement action in a single step. This time, it’s a little more complicated. First, we define the time for a single “wobble” (the ship may wobble multiple times while moving, but will wobble at a consistent rate throughout). The wobble itself consists of first scaling the ship along the x axis (i.e., its width) to 2/10ths of its normal size, and then scaling it back to it to its full size. Each of these is a single action that is packed together into another kind of action called a sequence, which performs all the actions it contains one after another. Next, we figure out how many times this wobble can happen during the duration of the ship’s travel and wrap the wobblingsequence inside a repeat action, telling it how many complete wobble cycles it should execute. And, as before, we start the method by canceling any previous wobbling action, since we wouldn’t want competing wobblers.

Now run the app, and you’ll see that the ship wobbles pleasantly when moving back and forth. It kind of looks like it’s walking!

Creating Your Enemies

So far so good, but this game is going to need some enemies for our players to shoot at. Use Xcode to make a new Cocoa Touch class called EnemyNode, using SKNode as the parent class. We’re not going to give the enemy class any real behavior just yet, but we will give it an appearance. We’ll use the same technique that we used for the player, using text to build the enemy’s body. Surely, there’s no text character more intimidating than the letter X, so our enemy will be a letter X… made of lowercase Xs! Try not to be scared just thinking about that as you add these methods to EnemyNode.m:

- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Enemy %p", self];
[self initNodeGraph];
}
return self;
}

- (void)initNodeGraph {
SKLabelNode *topRow = [SKLabelNode
labelNodeWithFontNamed:@"Courier-Bold"];
topRow.fontColor = [SKColor brownColor];
topRow.fontSize = 20;
topRow.text = @"x x";
topRow.position = CGPointMake(0, 15);
[self addChild:topRow];

SKLabelNode *middleRow = [SKLabelNode
labelNodeWithFontNamed:@"Courier-Bold"];
middleRow.fontColor = [SKColor brownColor];
middleRow.fontSize = 20;
middleRow.text = @"x";
[self addChild:middleRow];

SKLabelNode *bottomRow = [SKLabelNode
labelNodeWithFontNamed:@"Courier-Bold"];
bottomRow.fontColor = [SKColor brownColor];
bottomRow.fontSize = 20;
bottomRow.text = @"x x";
bottomRow.position = CGPointMake(0, -15);
[self addChild:bottomRow];
}

There’s nothing much new there; we’re just adding multiple “rows” of text by shifting the y value for each of their positions.

Putting Enemies in the Scene

Now let’s make some enemies appear in the scene by making some changes to GameScene.m. First, add the bold lines shown here, near the top:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"

@interface GameScene ()

@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) SKNode *enemies;

@end

We imported the header for our new enemy class and we added a new property for holding all the enemies that will be added to the level. You might think that we’d use an NSMutableArray for this, but it turns out that using a plain SKNode is perfect for the job. SKNode can hold any number of child nodes. And since we need to add all the enemies to the scene anyway, we may as well hold them all in an SKNode for easy access.

The next step is to create the spawnEnemies method, as shown here:

- (void)spawnEnemies {
NSUInteger count = log(self.levelNumber) + self.levelNumber;
for (NSUInteger i = 0; i < count; i++) {
EnemyNode *enemy = [EnemyNode node];
CGSize size = self.frame.size;
CGFloat x = arc4random_uniform(size.width * 0.8)
+ (size.width * 0.1);
CGFloat y = arc4random_uniform(size.height * 0.5)
+ (size.height * 0.5);
enemy.position = CGPointMake(x, y);
[self.enemies addChild:enemy];
}
}

Finally, add these lines near the end of the initWithSize:levelNumber: method to create an empty enemies node, and then call the spawnEnemies method:

[self addChild:_playerNode];
_enemies = [SKNode node];
[self addChild:_enemies];
[self spawnEnemies];

Since we added the enemies node to the scene, any child enemy nodes we add to the enemies node will also appear in the scene.

Now run the app, and you’ll see a dreadful enemy placed randomly in the upper portion of the screen (see Figure 17-7). Don’t you wish you could shoot it?

image

Figure 17-7. I’m sure you’ll agree that the X made of Xs just needs to be shot

Start Shooting

It’s time to implement the next logical step in the development of this game: letting the player attack the enemies. We want the player to be able to tap anywhere in the upper 80% of the screen to shoot a bullet at the enemies. We’re going to use the physics engine included in Sprite Kit both to move our player’s bullets and to let us know when a bullet collides with an enemy.

But first, what is this thing we call a physics engine? Basically, a physics engine is a software component that keeps track of multiple physical objects (commonly referred to as bodies) in a world, along with the forces that are acting upon them. It also makes sure that everything moves in a realistic way. It can take into account the force of gravity, handle collisions between objects (so that objects don’t occupy the same space simultaneously), and even simulate physical characteristics like friction and bounciness.

It’s important to understand that a physics engine is typically separate from a graphics engine. Apple provides convenient APIs to let us work with both, but they are essentially separate. It’s common to have objects in your display, such as our labels that show the current level number and remaining lives, that are completely separate from the physics engine. And it’s possible to create objects that have a physics body, but don’t actually display anything at all.

Defining Your Physics Categories

One of the things that the Sprite Kit physics engine lets us do is to assign objects to several distinct physics categories. A physics category has nothing to do with Objective-C categories. Instead, a physics category is a way to group related objects so that the physics engine can handle collisions between them in different ways. In this game, for example, we’ll create three categories: one for enemies, one for the player, and one for player missiles. We definitely want the physics engine to concern itself with collisions between enemies and player missiles, but we probably want it to ignore collisions between player missiles and the player itself. This is easy to set up using physics categories.

So, let’s create the categories we’re going to need. Press imageN to bring up the new file assistant, choose Header File from the iOS section, and press Next. Give the new header file the name PhysicsCategories.h and save it, and then add the following code to it:

#ifndef TextShooter_PhysicsCategories_h
#define TextShooter_PhysicsCategories_h

typedef NS_OPTIONS(uint32_t, PhysicsCategory) {
PlayerCategory = 1 << 1,
EnemyCategory = 1 << 2,
PlayerMissileCategory = 1 << 3
};

#endif

Here we declared three category constants. Note that the categories work as a bitmask, so each of them must be a power of two. We can easily do this by bit-shifting. These are set up as a bitmask in order to simplify the physics engine’s API a little bit. With bitmasks, we can logically ORseveral values together. This enables us to use a single API call to tell the physics engine how to deal with collisions between many different layers. We’ll see this in action soon.

Creating the BulletNode Class

Now that we’ve laid some groundwork, let’s create some bullets so we can start shooting.

Create a new Cocoa Touch class called BulletNode, once again using SKNode as its superclass. Start in the header file, where you’ll declare the two public methods this class will have:

#import <SpriteKit/SpriteKit.h>

@interface BulletNode : SKNode

+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination;
- (void)applyRecurringForce;

@end

The first method is a factory method for creating new instances of the class. The second is one that you’ll need to call from your scene each frame, to tell the bullet to move. Now switch over to BulletNode.m to start implementing this class.

The first thing we’re going to do is import a header for our special geometry functions and physics categories. The second step is to add a class extension with a single property, which will contain this bullet’s thrust vector:

#import "BulletNode.h"
#import "PhysicsCategories.h"
#import "Geometry.h"

@interface BulletNode ()

@property (assign, nonatomic) CGVector thrust;

@end

Next, we implement an init method. Like other init methods in this application, this is where we create the object graph for our bullet. This will consist of a single dot. While we’re at it, let’s also configure physics for this class by creating and configuring an SKPhysicsBody instanceand attaching it to self. In the process, we tell the new body what category it belongs to and which categories should be checked for collisions with this object.

@implementation BulletNode

- (instancetype)init {
if (self = [super init]) {
SKLabelNode *dot = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
dot.fontColor = [SKColor blackColor];
dot.fontSize = 40;
dot.text = @".";
[self addChild:dot];

SKPhysicsBody *body = [SKPhysicsBody bodyWithCircleOfRadius:1];
body.dynamic = YES;
body.categoryBitMask = PlayerMissileCategory;
body.contactTestBitMask = EnemyCategory;
body.collisionBitMask = EnemyCategory;
body.mass = 0.01;

self.physicsBody = body;
self.name = [NSString stringWithFormat:@"Bullet %p", self];
}
return self;
}

Applying Physics

Next, we’ll create the factory method that creates a new bullet and gives it a thrust vector that the physics engine will use to propel the bullet toward its target:

+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination {
BulletNode *bullet = [[self alloc] init];

bullet.position = start;

CGVector movement = VectorBetweenPoints(start, destination);
CGFloat magnitude = VectorLength(movement);
if (magnitude == 0.0f) return nil;

CGVector scaledMovement = VectorMultiply(movement, 1 / magnitude);

CGFloat thrustMagnitude = 100.0;
bullet.thrust = VectorMultiply(scaledMovement, thrustMagnitude);

return bullet;
}

The basic calculations are pretty simple. We first determine a movement vector that points from the start location to the destination, and then we determine its magnitude (length). Dividing the movement vector by its magnitude produces a normalized unit vector, a vector that points in the same direction as the original, but is exactly one unit long (a unit, in this case, is the same as a “point” on the screen—e.g., two pixels on a Retina device, one pixel on older devices). Creating a unit vector is very useful because we can multiply that by a fixed magnitude (in this case, 100) to determine a uniformly powerful thrust vector, no matter how far away the user tapped the screen.

The final piece of code we need to add to this class is this method, which applies thrust to the physics body. We’ll call this once per frame, from inside the scene:

- (void)applyRecurringForce {
[self.physicsBody applyForce:self.thrust];
}

Adding Bullets to the Scene

Now switch over to GameScene.m to add bullets to the scene itself. For starters, import the header for the new class near the top. Next, add another property to contain all bullets in a single SKNode, just as you did earlier for enemies:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
#import "BulletNode.h"

@interface GameScene ()

@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) SKNode *enemies;
@property (strong, nonatomic) SKNode *playerBullets;

@end

Find the section of the initWithSize:levelNumber: method where you previously added the enemies. That’s the place to set up the playerBullets node, too.

[self spawnEnemies];
_playerBullets = [SKNode node];
[self addChild:_playerBullets];

Now we’re ready to code the actual missile launches. Add this else clause to the touchesBegan:withEvent: method, so that all taps in the upper part of the screen shoot a bullet instead of moving the ship:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
if (location.y < CGRectGetHeight(self.frame) * 0.2 ) {
CGPoint target = CGPointMake(location.x,
self.playerNode.position.y);
[self.playerNode moveToward:target];

} else {
BulletNode *bullet = [BulletNode
bulletFrom:self.playerNode.position
toward:location];
[self.playerBullets addChild:bullet];
}
}
}

That adds the bullet, but none of the bullets we add will actually move unless we tell them to by applying thrust every frame. Our scene already contains an empty method called update:. This method is called each frame, and that’s the perfect place to do any game logic that needs to occur in each frame. Rather than updating all our bullets right in that method, however, we put that code in a separate method that we call from the update: method:

- (void)update:(CFTimeInterval)currentTime {
[self updateBullets];
}

- (void)updateBullets {
NSMutableArray *bulletsToRemove = [NSMutableArray array];
for (BulletNode *bullet in self.playerBullets.children) {
// Remove any bullets that have moved off-screen
if (!CGRectContainsPoint(self.frame, bullet.position)) {
// mark bullet for removal
[bulletsToRemove addObject:bullet];
continue;
}
// Apply thrust to remaining bullets
[bullet applyRecurringForce];
}
[self.playerBullets removeChildrenInArray:bulletsToRemove];
}

Before telling each bullet to apply its recurring force, we also check whether each bullet is still on-screen. Any bullet that’s gone off-screen is put into a temporary array; and then, at the end, those are swept out of the playerBullets node. Note that this two-stage process is necessary because the for loop at work in this method is iterating over all children in the playerBullets node. Making changes to a collection while you’re iterating over it is never a good idea, and it can easily lead to a crash.

Now build and run the app, and you’ll see that, in addition to moving the player’s ship, you can make it shoot missiles upward by tapping on the screen (see Figure 17-8). Neat!

image

Figure 17-8. Shooting up a storm!

Attacking Enemies with Physics

A couple of important gameplay elements are still missing from our game. The enemies never attack us, and we can’t yet get rid of the enemies by shooting them. Let’s take care of the latter right now. We’re going to set things up so that shooting an enemy has the effect of dislodging it from the spot where it’s currently fixed on the screen. This feature will use the physics engine for all the heavy lifting, and it will involve making changes to PlayerNode, EnemyNode, and GameScene.

For starters, let’s add physics bodies to our nodes that don’t already have them. Start with EnemyNode.m, adding this #import statement near the top:

#import "PhysicsCategories.h"

Next, add the following line to the init method:

- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Enemy %p", self];
[self initNodeGraph];
[self initPhysicsBody];
}
return self;
}

Now add the code to really set up the physics body. This is pretty similar to what you did earlier for the PlayerBullet class:

- (void)initPhysicsBody {
SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
CGSizeMake(40, 40)];
body.affectedByGravity = NO;
body.categoryBitMask = EnemyCategory;
body.contactTestBitMask = PlayerCategory|EnemyCategory;
body.mass = 0.2;
body.angularDamping = 0.0f;
body.linearDamping = 0.0f;
self.physicsBody = body;
}

Then select PlayerNode.m, where you’re going to do a pretty similar set of things. First, add the following #import near the top:

#import "PhysicsCategories.h"

Follow up by adding the bold line shown here to the init method:

- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Player %p", self];
[self initNodeGraph];
[self initPhysicsBody];
}
return self;
}

Finally, add the new initPhysicsBody method:

- (void)initPhysicsBody {
SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
CGSizeMake(20, 20)];
body.affectedByGravity = NO;
body.categoryBitMask = PlayerCategory;
body.contactTestBitMask = EnemyCategory;
body.collisionBitMask = 0;

self.physicsBody = body;
}

At this point, you can run the app and see that your bullets now have the ability to knock enemies into space. However, you’ll also see there’s a problem here. When you start the game and then send the lone enemy hurtling into space, you’re stuck! This is probably a good time to add level management to the game.

Finishing Levels

We need to enhance GameScene so that it knows when it’s time to move to the next level. It can figure this out simply enough by looking at the number of available enemies. If it finds that there aren’t any on-screen, then the level is over, and the game should transition to the next.

Keeping Tabs on the Enemies

Begin by adding this updateEnemies method. It works a lot like the updateBullets method added earlier:

- (void)updateEnemies {
NSMutableArray *enemiesToRemove = [NSMutableArray array];
for (SKNode *node in self.enemies.children) {
// Remove any enemies that have moved off-screen
if (!CGRectContainsPoint(self.frame, node.position)) {
// mark enemy for removal
[enemiesToRemove addObject:node];
continue;
}
}
if ([enemiesToRemove count] > 0) {
[self.enemies removeChildrenInArray:enemiesToRemove];
}
}

That takes care of removing each enemy from the level’s enemies array each time one goes off-screen. Now let’s modify the update: method, telling it to call updateEnemies, as well as a new method we haven’t yet implemented:

- (void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
if (self.finished) return;

[self updateBullets];
[self updateEnemies];
[self checkForNextLevel];
}

We started out that method by checking the finished property. Since we’re about to add code that can officially end a level, we want to be sure that we don’t keep doing additional processing after the level is complete! Then, just as we’re checking each frame to see if any bullets or enemies have gone off-screen, we’re going to call checkForNextLevel each frame to see if the current level is complete. Let’s add this method:

- (void)checkForNextLevel {
if ([self.enemies.children count] == 0) {
[self goToNextLevel];
}
}

Transitioning to the Next Levels

The checkForNextLevel method in turn calls another method we haven’t yet implemented. The goToNextLevel method marks this level as finished, displays some text on the screen to let the player know, and then starts the next level:

- (void)goToNextLevel {
self.finished = YES;

SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
label.text = @"Level Complete!";
label.fontColor = [SKColor blueColor];
label.fontSize = 32;
label.position = CGPointMake(self.frame.size.width * 0.5,
self.frame.size.height * 0.5);
[self addChild:label];

GameScene *nextLevel = [[GameScene alloc]
initWithSize:self.frame.size
levelNumber:self.levelNumber + 1];
nextLevel.playerLives = self.playerLives;
[self.view presentScene:nextLevel
transition:[SKTransition flipHorizontalWithDuration:1.0]];
}

The second half of the goToNextLevel method creates a new instance of GameScene and gives it all the start values it needs. It then tells the view to present the new scene, using a transition to smooth things over. The SKTransition class lets us pick from a variety of transition styles. Run the app and complete a level to see what this one looks like (see Figure 17-9).

image

Figure 17-9. Here you see a snapshot taken during the end-of-level screen-flipping transition

The transition in use here makes it looks like we’re flipping a card over its horizontal axis, but there are plenty more to choose from! See the documentation or header file for SKTransition to see more possibilities. We’ll use a couple more variations later in this chapter.

Customizing Collisions

Now we’ve got a game that you can really play. You can clear level after level by knocking enemies upward off the screen. That’s OK, but there’s really not much challenge! We mentioned earlier that having enemies attack the player is one piece of missing gameplay, and now it’s time to make that happen. We’re going to make things a little harder by making the enemies fall down when they’re bumped, either from being hit by a bullet or from being touched by another enemy. We also want to make it so that being hit by a falling enemy takes a life away from the player. You also may have noticed that after a bullet hits an enemy, the bullet squiggles its way around the enemy and continues on its upward trajectory, which is pretty weird. We’re going to tackle all these things by implementing a collision-handling routine in GameScene.m.

The method for handling detected collisions is a delegate method for the SKPhysicsWorld class. Our scene has a physics world by default, but we need to set it up a little bit before it will tell us anything. For starters, it’s good to let the compiler know that we’re going to implement a delegate protocol, so let’s add this declaration to the class extension declaration near the top of the GameScene.m file:

@interface GameScene () <SKPhysicsContactDelegate>

We still need to configure the world a bit (giving it a slightly less cruel amount of gravity) and tell it who its delegate is. To do so, we add these bold lines near the end of the initWithSize:levelNumber: method:

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

Now that we’ve set the physics world’s contactDelegate to be the GameScene, we can implement the relevant delegate method. The core of the method looks like this:

- (void)didBeginContact:(SKPhysicsContact *)contact {
if (contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask) {
// Both bodies are in the same category
SKNode *nodeA = contact.bodyA.node;
SKNode *nodeB = contact.bodyB.node;

// What do we do with these nodes?
} else {
SKNode *attacker = nil;
SKNode *attackee = nil;

if (contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask) {
// Body A is attacking Body B
attacker = contact.bodyA.node;
attackee = contact.bodyB.node;
} else {
// Body B is attacking Body A
attacker = contact.bodyB.node;
attackee = contact.bodyA.node;
}
if ([attackee isKindOfClass:[PlayerNode class]]) {
self.playerLives--;
}
// What do we do with the attacker and the attackee?
}
}

Go ahead and add that method, but if you look at it right now, you’ll see that it doesn’t really do much yet. In fact, the only concrete result of that method is to reduce the number of player lives each time a falling enemy hits the player’s ship. But the enemies aren’t falling yet!

The idea behind this implementation is to look at the two colliding objects and to figure out whether they are of the same category (in which case, they are “friends” to one another) or if they are of different categories. If they are of different categories, we have to determine who is attacking whom. If you look at the order of the categories declared in PhysicsCategories.h, you’ll see that they are specified in order of increased “attackyness”: Player nodes can be attacked by Enemy nodes, which in turn can be attacked by PlayerMissile nodes. That means that we can use a simple greater-than comparison to figure out who is the “attacker” in this scenario.

For the sake of simplicity and modularity, we don’t really want the scene to decide how each object should react to being attacked by an enemy or bumped by another object. It’s much better to build those details into the affected node classes themselves. But, as you see in the method we’ve got, the only thing we’re sure of is that each side has an SKNode instance. Rather than coding up a big chain of if-else statements to ask each node which SKNode subclass it belongs to, we can use regular polymorphism to let each of our node classes handle things in its own way. In order for that to work, we have to add methods to SKNode, with default implementations that do nothing, and let our subclasses override them where appropriate. This calls for a category! Not a Sprite Kit physics category this time, but a genuine Objective-C @category definition.

Adding a Category to SKNode

To add a category to SKNode, right-click the TextShooter folder in Xcode’s Project Navigator and choose New File… from the pop-up menu. From the assistant’s iOS/Source section, choose Objective-C File, and then click Next. Give it a File name of Extra, select Category as the File Type, and choose SKNode as the class to which the category is being added. Now click Next again and create the files. Select the category header file SKNode+Extra.h and add the bold method declarations shown here:

#import <SpriteKit/SpriteKit.h>

@interface SKNode (Extra)

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact;
- (void)friendlyBumpFrom:(SKNode *)node;

@end

Switch over to the matching .m file and enter the following empty definitions:

#import "SKNode+Extra.h"

@implementation SKNode (Extra)

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
// default implementation does nothing
}

- (void)friendlyBumpFrom:(SKNode *)node {
// default implementation does nothing
}

@end

Now head back over to GameScene.m to finish up its part of the collision handling. Start by adding a new header at the top:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
#import "BulletNode.h"
#import "SKNode+Extra.h"

Next, go back to the didBeginContact: method, where you’ll add the bits that actually do some work:

- (void)didBeginContact:(SKPhysicsContact *)contact {
if (contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask) {
// Both bodies are in the same category
SKNode *nodeA = contact.bodyA.node;
SKNode *nodeB = contact.bodyB.node;

// What do we do with these nodes?
[nodeA friendlyBumpFrom:nodeB];
[nodeB friendlyBumpFrom:nodeA];
} else {
SKNode *attacker = nil;
SKNode *attackee = nil;

if (contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask) {
// Body A is attacking Body B
attacker = contact.bodyA.node;
attackee = contact.bodyB.node;
} else {
// Body B is attacking Body A
attacker = contact.bodyB.node;
attackee = contact.bodyA.node;
}
if ([attackee isKindOfClass:[PlayerNode class]]) {
self.playerLives--;
}
// What do we do with the attacker and the attackee?
[attackee receiveAttacker:attacker contact:contact];
[self.playerBullets removeChildrenInArray:@[attacker]];
[self.enemies removeChildrenInArray:@[attacker]];
}
}

All we added here were a few calls to our new methods. If the collision is “friendly fire,” such as two enemies bumping into each other, we’ll tell each of them that it received a friendly bump from the other. Otherwise, after figuring out who attacked whom, we tell the attackee that it’s come under attack from another object. Finally, we remove the attacker from whichever of the playerBullets or enemies nodes it may be in. We tell each of those nodes to remove the attacker, even though it can only be in one of them, but that’s OK. Telling a node to remove a child it doesn’t have isn’t an error—it just has no effect.

Adding Custom Collision Behavior to Enemies

Now that all that’s in place, we can implement some specific behaviors for our nodes by overriding the category methods we added to SKNode.

Select EnemyNode.m. At the top of the file, add an import of Geometry.h:

#import "PhysicsCategories.h"
#import "Geometry.h"

@implementation EnemyNode

Next, add the following two methods:

- (void)friendlyBumpFrom:(SKNode *)node {
self.physicsBody.affectedByGravity = YES;
}

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
self.physicsBody.affectedByGravity = YES;
CGVector force = VectorMultiply(attacker.physicsBody.velocity,
contact.collisionImpulse);
CGPoint myContact = [self.scene convertPoint:contact.contactPoint
toNode:self];
[self.physicsBody applyForce:force
atPoint:myContact];
}

The first of those, friendlyBumpFrom:, simply turns on gravity for the affected enemy. So, if one enemy is in motion and bumps into another, the second enemy will suddenly notice gravity and start falling downward.

The receiveAttacker:contact: method, which is called if the enemy is hit by a bullet, first turns on gravity for the enemy. However, it also uses the contact data that was passed in to figure out just where the contact occurred and applies a force to that point, giving it an extra push in the direction that the bullet was fired.

Showing Accurate Player Lives

Run the game, and you’ll see that you can shoot at enemies to knock them down. You’ll also see that any other enemies bumped into by a falling enemy will fall, as well.

Note At the start of each level, the world performs one step of its physics simulation to make sure that there aren’t physics bodies overlapping each other. This will produce an interesting side effect at higher levels, since there will be an increasing chance that multiple randomly placed enemies will occupy overlapping spaces. Whenever that happens, the enemies will be immediately shifted so they no longer overlap, and our collision-handling code will be triggered, which subsequently turns on gravity and lets them fall! This behavior wasn’t anything we planned on when we started building this game, but it turns out to be a happy accident that makes higher levels progressively more difficult, so we’re letting physics run its course!

If you let enemies hit you as they fall, the number of player lives decreases, but… hey wait, it just shows 5 all the time! The Lives display is set up when the level is created, but it’s never updated after that. Fortunately, this is easily fixed by implementing the setPlayerLives: setter inGameScene.m instead of using the automatically synthesized setter, like this:

- (void)setPlayerLives:(NSUInteger)playerLives {
_playerLives = playerLives;
SKLabelNode *lives = (id)[self childNodeWithName:@"LivesLabel"];
lives.text = [NSString stringWithFormat:@"Lives: %lu",
(unsigned long)_playerLives];
}

The preceding snippet uses the name we previously associated with the label (in the initWithSize:level: method) to find the label again and set a new text value. Play the game again, and you’ll see that, as you let enemies rain down on your player, the number of lives will decrease to zero. And then the game doesn’t end. After the next hit, you end up with a very large number of lives indeed, as you can see in Figure 17-10.

image

Figure 17-10. That’s a lot of lives

So what’s going on here? Well, we are using an unsigned integer to hold the number of lives. And when you’re using unsigned integers and dip below zero, you sort of wrap around that zero boundary and end up with the maximum allowed unsigned integer value instead!

The reason this problem appears is really because we haven’t written any code to detect the end of the game; that is, the point in time when the number of player lives hits zero. We’ll do that soon, but first let’s make our on-screen collisions a bit more stimulating.

Spicing Things Up with Particles

One of the nice features of Sprite Kit is the inclusion of a particle system. Particle systems are used in games to create visual effects simulating smoke, fire, explosions, and more. Right now, whenever our bullets hit an enemy or an enemy hits the player, the attacking object simply blinks out of existence. Let’s make a couple of particle systems to improve this situation!

Start out by pressing imageN to bring up the new file assistant. Select the iOS/Resource section on the left, and then choose SpriteKit Particle File on the right. Click Next, and on the following screen choose the Spark particle template. Click Next again and name this fileMissileExplosion.sks.

Your First Particle

You’ll see that Xcode creates the particle file and also adds a new resource called spark.png to the project. At the same time, the entire Xcode editing area switches over to the new particle file, showing you a huge, animated exploding thing.

We don’t want something quite this extravagant and enormous when our bullets hit enemies, so let’s reconfigure this thing. All the properties that define this particle’s animation are available in the SKNode Inspector, which you can bring up by pressing Opt-Cmd-7. Figure 17-11 shows both the massive explosion and the inspector.

image

Figure 17-11. Explosion city! The parameters shown on the right define how the default particle looks

Now, for our bullet hit, let’s make it a much smaller explosion. It will have a whole different set of parameters, all of which you configure right in the inspector. First, fix the colors to match what our game looks like by clicking the small color well in the Color Ramp at the bottom and setting it to black. Next, change the Background color to white and change the Blend Mode to Alpha. Now you’ll see that the flaming fountain has turned all inky.

The rest of the parameters are all numeric. Change them one at a time, setting them all as shown in Figure 17-12. At each step of the way, you’ll see the particle effect change until it eventually reaches its target appearance.

image

Figure 17-12. This is the final missile explosion particle effect we want

Now make another particle system, once again using the Spark template. Name this one EnemyExplosion.sks and set its parameters as shown in Figure 17-13.

image

Figure 17-13. Here’s the enemy explosion we want to create. In case you’re seeing this book in black and white, the color we’ve chosen in the Color Ramp at the bottom is deep red

Note After adding the second particle file, you may find two copies of the file spark.png in the Project Navigator and a warning in the Activity View. Fix this by right-clicking on one of the files and selecing Delete.

Putting Particles into the Scene

Now let’s start putting these particles to use. Switch over to EnemyNode.m and add the bold code shown here to the bottom of the receiveAttacker:contact: method:

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
self.physicsBody.affectedByGravity = YES;
CGVector force = VectorMultiply(attacker.physicsBody.velocity,
contact.collisionImpulse);
CGPoint myContact = [self.scene convertPoint:contact.contactPoint
toNode:self];
[self.physicsBody applyForce:force
atPoint:myContact];

NSString *path = [[NSBundle mainBundle] pathForResource:@"MissileExplosion"
ofType:@"sks"];
SKEmitterNode *explosion = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
explosion.numParticlesToEmit = 20;
explosion.position = contact.contactPoint;
[self.scene addChild:explosion];
}

Run the game, shoot some enemies, and you’ll see a nice little explosion where each bullet hits an enemy, as shown in Figure 17-14.

image

Figure 17-14. Bullets smash nicely after impact

Nice! Now let’s do something similar for those times an enemy smashes into a player’s ship. Select PlayerNode.m and add this method:

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
NSString *path = [[NSBundle mainBundle] pathForResource:@"EnemyExplosion"
ofType:@"sks"];
SKEmitterNode *explosion =
[NSKeyedUnarchiver unarchiveObjectWithFile:path];
explosion.numParticlesToEmit = 50;
explosion.position = contact.contactPoint;
[self.scene addChild:explosion];
}

Play again, and you’ll see a nice red splat every time an enemy hits the player, as shown in Figure 17-15.

image

Figure 17-15. Ouch!

These changes are pretty simple, but they improve the feel of the game substantially. Now when things collide, you have visual c.uences and can see that something happened.

The End Game

As we mentioned before, we currently have a small problem in the game. When the number of lives hits zero, we need to end the game. What we’ll do is create a new scene class to transition to when the game is over. You’ve seen us do a scene transition before, when moving from one level to the next. This will be similar, but with a new class.

So, create a new iOS/Cocoa Touch class. Use SKScene as the parent class and name the new class GameOverScene.

We’ll start with a very simple implementation that just displays “Game Over” text and does nothing more. We’ll accomplish this by adding this code to the @implementation in GameOverScene.m:

- (instancetype)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.backgroundColor = [SKColor purpleColor];
SKLabelNode *text = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
text.text = @"Game Over";
text.fontColor = [SKColor whiteColor];
text.fontSize = 50;
text.position = CGPointMake(self.frame.size.width * 0.5,
self.frame.size.height * 0.5);
[self addChild:text];
}
return self;
}

Now let’s switch back to GameScene.m. We’ll need to import the header for the new scene at the top:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
#import "BulletNode.h"
#import "SKNode+Extra.h"
#import "GameOverScene.h"

The basic action of what to do when the game ends is defined by a new method called triggerGameOver. Here, we show both an extra explosion and kick off a transition to the new scene we just created:

- (void)triggerGameOver {
self.finished = YES;

NSString *path = [[NSBundle mainBundle] pathForResource:@"EnemyExplosion"
ofType:@"sks"];
SKEmitterNode *explosion =
[NSKeyedUnarchiver unarchiveObjectWithFile:path];
explosion.numParticlesToEmit = 200;
explosion.position = _playerNode.position;
[self addChild:explosion];
[_playerNode removeFromParent];

SKTransition *transition =
[SKTransition doorsOpenVerticalWithDuration:1.0];
SKScene *gameOver = [[GameOverScene alloc] initWithSize:self.frame.size];
[self.view presentScene:gameOver transition:transition];
}

Next, create this new method that will check for the end of the game, call triggerGameOver if it’s time, and return either YES to indicate the game ended or NO to indicate that it’s still on:

- (BOOL)checkForGameOver {
if (self.playerLives == 0) {
[self triggerGameOver];
return YES;
}
return NO;
}

Finally, add a check to the existing update: method. It checks for the game-over state and only checks for a potential next-level transition if the game is still going. Otherwise, there’s a risk that the final enemy on a level could take the player’s final life and trigger two scene transitions at once!

- (void)update:(CFTimeInterval)currentTime {
if (self.finished) return;

[self updateBullets];
[self updateEnemies];
if (![self checkForGameOver]) {
[self checkForNextLevel];
}
}

Now run the game again, let falling enemies damage your ship five times, and you’ll see the Game Over screen, as shown in Figure 17-16.

image

Figure 17-16. That’s it, man. Game over, man—game over

At Last, a Beginning: Create a StartScene

This leads us to another problem: What do we do after the game is over? We could allow the player to tap to restart the game; but while thinking of that, a thought crossed my mind. Shouldn’t this game have some sort of start screen, so the player isn’t immediately thrust into a game at launch time? And shouldn’t the game-over screen lead you back there? Of course, the answer to both questions is yes! Go ahead and create another new iOS/Cocoa Touch class, once again using SKScene as the superclass, and this time naming it StartScene.

We’re going to make a super-simple start scene here. All it will do is display some text and start the game when the user taps anywhere. Add all the bold code shown here to StartScene.m to complete this class:

#import "StartScene.h"
#import "GameScene.h"

@implementation StartScene

- (instancetype)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.backgroundColor = [SKColor greenColor];

SKLabelNode *topLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
topLabel.text = @"TextShooter";
topLabel.fontColor = [SKColor blackColor];
topLabel.fontSize = 48;
topLabel.position = CGPointMake(self.frame.size.width * 0.5,
self.frame.size.height * 0.7);
[self addChild:topLabel];

SKLabelNode *bottomLabel = [SKLabelNode labelNodeWithFontNamed:
@"Courier"];
bottomLabel.text = @"Touch anywhere to start";
bottomLabel.fontColor = [SKColor blackColor];
bottomLabel.fontSize = 20;
bottomLabel.position = CGPointMake(self.frame.size.width * 0.5,
self.frame.size.height * 0.3);
[self addChild:bottomLabel];

}
return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKTransition *transition = [SKTransition doorwayWithDuration:1.0];
SKScene *game = [[GameScene alloc] initWithSize:self.frame.size];
[self.view presentScene:game transition:transition];
}

@end

Now go back to GameOverScene.m, so we can make the game-over scene perform a transition to the start scene. Add this header import:

#import "GameOverScene.h"
#import "StartScene.h"

And then add the following code:

- (void)didMoveToView:(SKView *)view {
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
SKTransition *transition = [SKTransition flipVerticalWithDuration:1.0];
SKScene *start = [[StartScene alloc] initWithSize:self.frame.size];
[self.view presentScene:start transition:transition];
});
}

As you saw earlier, the didMoveToView: method is called on any scene after it’s been put in place in a view. Here, we simply trigger a three-second pause, followed by a transition back to the start scene.

There’s just one more piece of the puzzle to make all our scenes transition to each other as they should. We need to change the app startup procedure so that, instead of jumping right into the game, it shows us the start screen instead. This takes us back to GameViewController.m, where we first import the header for our start scene:

#import "GameViewController.h"
#import "GameScene.h"
#import "StartScene.h"

Then, in the viewDidLoad method, we just replace the code to create one scene class with another:

// Create and configure the scene.
GameScene *scene =
[GameScene sceneWithSize:self.view.frame.size levelNumber:1];
SKScene * scene = [StartScene sceneWithSize:skView.bounds.size];

Now give it a whirl! Launch the app, and you’ll be greeted by the start scene. Touch the screen, play the game, die a lot, and you’ll get to the game-over scene. Wait a few seconds, and you’re back to the start screen, as shown in Figure 17-17.

image

Figure 17-17. Finally, we made it to the start screen!

A Sound Is Worth a Thousand Pictures

We’ve been working on a video game, and video games are known for being noisy, but ours is completely silent! Fortunately, Sprite Kit contains audio playback code that’s extremely easy to use. In the 17 – Sound Effects folder in the source code for this chapter, you’ll find the prepared audio files: enemyHit.wav, gameOver.wav, gameStart.wav, playerHit.wav, and shoot.wav. Drag all of them into Xcode’s Project Navigator.

Note These sound effects were created using the excellent, open source CFXR application (available from https://github.com/nevyn/cfxr). If you need quirky little sound effects, CFXR is hard to beat!

Now we’ll bake in easy playback for each of these sound effects. Start with BulletNode.m, adding the bold code to the end of the bulletFrom:toward: method, just before the return line:

[bullet runAction:[SKAction playSoundFileNamed:@"shoot.wav"
waitForCompletion:NO]];

Next, switch over to EnemyNode.m, adding these lines to the end of the receiveAttacker:contact: method:

[self runAction:[SKAction playSoundFileNamed:@"enemyHit.wav"
waitForCompletion:NO]];

Now do something extremely similar in PlayerNode.m, adding these lines to the end of the receiveAttacker:contact: method:

[self runAction:[SKAction playSoundFileNamed:@"playerHit.wav"
waitForCompletion:NO]];

Those are enough in-game sounds to satisfy for the moment. Go ahead and run the game at this point to try them out. I think you’ll agree that the simple addition of particles and sounds gives the game a much better feel.

Now let’s just add some effects for starting the game and ending the game. In StartScene.m, add these lines at the end of the touchesBegan:withEvent: method:

[self runAction:[SKAction playSoundFileNamed:@"gameStart.wav"
waitForCompletion:NO]];

And finally, add these lines to the end of the triggerGameOver method in GameScene.m:

[self runAction:[SKAction playSoundFileNamed:@"gameOver.wav"
waitForCompletion:NO]];

Now when you play the game, you’ll be inundated by comforting bleeps and bloops, just like when you were a kid! Or maybe when your parents were kids. Or your grandparents! Just trust me, all the games used to sound pretty much like this.

Making the Game a Little Harder: Force Fields

One of the more interesting new features added to Sprite Kit in iOS 8 is the ability to place force fields in a scene. A force field has a type, a location, a region in which it takes effect, and several other properties that specify how it behaves. The idea is that the field perturbs the motion of objects as they move through its region. There are various standard force fields that you can use, just by creating and configuring an instance and adding it to a scene. If you are feeling ambitious, you can even create custom force fields. For a list of the standard force fields and their behaviors, which include gravity fields, electric and magnetic fields, and turbulence, look at the API documentation for the SKFieldNode class.

To make our game a little more challenging, we’re going to add some radial gravity fields to the scene. Radial gravity fields act like a large mass concentrated at a point. As an object moves through the region of a radial gravity field, it will be deflected toward it (or away from it, if you want to configure it that way), much like a meteor passing close enough to the Earth would be as it flies past. We’re going to arrange for our gravity fields to act on missiles, so that you won’t always be able to directly aim at an enemy and be sure of hitting it. Let’s get started.

First, we need to add a new category to PhysicsCategories.h. Make the following change in that file, not forgetting to add the comma at the end of the definition of PlayerMissileCategory:

typedef NS_OPTIONS(uint32_t, PhysicsCategory) {
PlayerCategory = 1 << 1,
EnemyCategory = 1 << 2,
PlayerMissileCategory = 1 << 3,
GravityFieldCategory = 1 << 4
};

A field acts on a node if the fieldBitMask in the node’s physics body has any category in common with the field’s categoryBitMask. By default, a physics body’s fieldBitMask has all categories set. Since we don’t want enemies to be affected by the gravity field, we need to clear its fieldBitMask by adding the following code in EnemyNode.m:

- (void)initPhysicsBody {
SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
CGSizeMake(40, 40)];
body.affectedByGravity = NO;
body.categoryBitMask = EnemyCategory;
body.contactTestBitMask = PlayerCategory|EnemyCategory;
body.mass = 0.2;
body.angularDamping = 0.0f;
body.linearDamping = 0.0f;
body.fieldBitMask = 0;
self.physicsBody = body;
}

Make a similar change in PlayerNode.m:

- (void)initPhysicsBody {
SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
CGSizeMake(20, 20)];
body.affectedByGravity = NO;
body.categoryBitMask = PlayerCategory;
body.contactTestBitMask = EnemyCategory;
body.collisionBitMask = 0;
body.fieldBitMask = 0;
self.physicsBody = body;
}

The missile nodes will respond to the gravity field even if we don’t do anything, since their physics nodes have all field categories set by default, but it’s cleaner if we make this explicit, so make the following change in BulletNode.m:

- (instancetype)init {
if (self = [super init]) {
SKLabelNode *dot = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
dot.fontColor = [SKColor blackColor];
dot.fontSize = 40;
dot.text = @".";
[self addChild:dot];

SKPhysicsBody *body = [SKPhysicsBody bodyWithCircleOfRadius:1];
body.dynamic = YES;
body.categoryBitMask = PlayerMissileCategory;
body.contactTestBitMask = EnemyCategory;
body.collisionBitMask = EnemyCategory;
body.fieldBitMask = GravityFieldCategory;
body.mass = 0.01;

self.physicsBody = body;
self.name = [NSString stringWithFormat:@"Bullet %p", self];
}
return self;
}

The rest of the changes are going to be in the file GameScene.m. We’re going to add three gravity fields centered at random points just below the center of the scene. As we did with the missiles and enemies, we’ll add the force field nodes to a parent node that we’ll then add to the scene. Add the definition of the parent node to the class extension of GameScene:

@interface GameScene () <SKPhysicsContactDelegate>

@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) SKNode *enemies;
@property (strong, nonatomic) SKNode *playerBullets;
@property (strong, nonatomic) SKNode *forceFields;

@end

At the end of the initWithSize:level: method, add code to allocate the forceFields node, add it to the scene, and create the actual force field nodes:

_playerBullets = [SKNode node];
[self addChild:_playerBullets];

_forceFields = [SKNode node];
[self addChild:_forceFields];
[self createForceFields];

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

Finally, add the implementation of the createForceFields method:

- (void)createForceFields {
static int fieldCount = 3;
CGSize size = self.frame.size;
float sectionWidth = size.width/fieldCount;
for (NSUInteger i = 0; i < fieldCount; i++) {
CGFloat x = i * sectionWidth + arc4random_uniform(sectionWidth);
CGFloat y = arc4random_uniform(size.height * 0.25)
+ (size.height * 0.25);

SKFieldNode *gravityField = [SKFieldNode radialGravityField];
gravityField.position = CGPointMake(x, y);
gravityField.categoryBitMask = GravityFieldCategory;
gravityField.strength = 4;
gravityField.falloff = 2;
gravityField.region = [[SKRegion alloc]
initWithSize:CGSizeMake(size.width * 0.3, size.height * 0.1)];
[self.forceFields addChild:gravityField];

SKLabelNode *fieldLocationNode =
[SKLabelNode labelNodeWithFontNamed:@"Courier"];
fieldLocationNode.fontSize = 16;
fieldLocationNode.fontColor = [SKColor redColor];
fieldLocationNode.name = @"GravityField";
fieldLocationNode.text = @"*";
fieldLocationNode.position = CGPointMake(x, y);
[self.forceFields addChild:fieldLocationNode];
}
}

All force fields are represented by instances of the SKFieldNode class. For each type of field, the SKFieldNode class has a factory method that lets you create a node of that field’s type. Here, we use the radialGravityFieldNode method to create three instances of a radial gravity field and we place them in a band just below the center of the scene. The strength and falloff properties control how strong the gravity field is and how rapidly it diminishes with the distance from the field node. A falloff value of 2 makes the force proportional to the inverse square of the distance between the field node and the affected object, just like in the real world. A positive force makes the field node attract the other object. Experiment with different strength values, including negative ones, to see how the effect varies. We also create three SKLabelNodes at the same positions as the gravity force fields, so that the player can see where they are. That’s all we need to do. Build and run the app and watch what happens when your bullets fly close to one of the red asterisks in the scene!

Game On

Although TextShooter may be simple in appearance, the techniques you’ve learned in this chapter form the basis for all sorts of game development using Sprite Kit. You’ve learned how to organize your code across multiple node classes, group objects together using the node graph, and more. You’ve also been given a taste of what it’s like to build this sort of game one feature at a time, discovering each step along the way. Of course, we’re not showing you all of our own missteps made along the way—this book is already over 700 pages long without that—but even counting those, this app really was built from scratch, in roughly the order shown in this chapter, in just a few short hours.

Once you get going, Sprite Kit allows you to build up a lot of structure in a short amount of time. As you’ve seen, you can use text-based sprites if you don’t have images handy. And if you want to swap them out for real graphics later, it’s no problem. One early reader even pointed out a middle path: “Instead of plain old ASCII text in the strings in your source code, you can insert emoji characters by using Apple’s Character Viewer input source.” Accomplishing this is left as an exercise to the reader!