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

Beginning iPhone Development with Swift: Exploring the iOS SDK (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 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 Project… 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:

override func viewDidLoad() {
super.viewDidLoad()

if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true

skView.ignoresSiblingOrder = true

/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill

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:

if let scene = GameScene.unarchiveFromFile("GameScene") as? 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.

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

/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill

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 the presentScene(_, 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.swift 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.swift 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 .AspectFill. 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 values .AspectFit, .Fill and.ResizeFill. The results are shown in Figure 17-4.

image

Figure 17-4. Comparing the four scene rescale modes

Here’s what these modes do:

· SKSceneScaleMode.AspectFill 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.

· SKSceneScaleMode.AspectFit 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.

· SKSceneScaleMode.Fill 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, SKSceneScaleMode.ResizeFill 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 may warn that location is an unused variable—don’t worry about that, because we’ll fix it later):

class GameScene: SKScene {

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */

for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}

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

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.swift:

class GameScene: SKScene {
private var levelNumber: UInt
private var playerLives: Int
private var finished = false

class func scene(size:CGSize, levelNumber:UInt) -> GameScene {
return GameScene(size: size, levelNumber: levelNumber)
}

override convenience init(size:CGSize) {
self.init(size: size, levelNumber: 1)
}

init(size:CGSize, levelNumber:UInt) {
self.levelNumber = levelNumber
self.playerLives = 5
super.init(size: size)

backgroundColor = SKColor.whiteColor()

let lives = SKLabelNode(fontNamed: "Courier")
lives.fontSize = 16
lives.fontColor = SKColor.blackColor()
lives.name = "LivesLabel"
lives.text = "Lives: \(playerLives)"
lives.verticalAlignmentMode = .Top
lives.horizontalAlignmentMode = .Right
lives.position = CGPointMake(frame.size.width,
frame.size.height)
addChild(lives)

let level = SKLabelNode(fontNamed: "Courier")
level.fontSize = 16
level.fontColor = SKColor.blackColor()
level.name = "LevelLabel"
level.text = "Level \(levelNumber)"
level.verticalAlignmentMode = .Top
level.horizontalAlignmentMode = .Left
level.position = CGPointMake(0, frame.height)
addChild(level)
}

required init?(coder aDecoder: NSCoder) {
levelNumber = UInt(aDecoder.decodeIntegerForKey("level"))
playerLives = aDecoder.decodeIntegerForKey("playerLives")
super.init(coder: aDecoder)
}

override func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeInteger(Int(levelNumber), forKey: "level")
aCoder.encodeInteger(playerLives, forKey: "playerLives")
}

The first method, scene(size:, 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, init(), 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, after setting the initial values of levelNumber and playerLives properties. 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. After calling the superclass initializer, 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 type alias that is mapped to UIColor for an iOS app and 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(frame.size.width,
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 = .Right;

Setting the horizontalAlignmentMode property of the SKLabelNode to SKLabelHorizontalAlignmentMode.Right 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 = .Left;
level.position = CGPointMake(0, 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.

We added the init(coder:) and encodeWithCoder(coder:) methods because all Sprite Kit nodes, including SKScene, conform to the NSCoding protocol. This requires us to override init(coder:), so we also implement encodeWithCoder(coder:) for the sake of consistency, even though we won’t be archiving the scene object in this application. You’ll see the same pattern in all of the SKNode subclasses that we create, although we won’t implement the encodeWithCoder(coder:) method when the subclass has no additional state of its own, since the base class version does everything that we need in that case.

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

override func viewDidLoad() {
super.viewDidLoad()

if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
let scene = GameScene(size: view.frame.size, levelNumber: 1)

// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true

skView.ignoresSiblingOrder = true

/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill

skView.presentScene(scene)
}
}

Instead of loading the scene from the scene file, we’re using the scene(size:, 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.

Near the end of GameViewController.swift, you’ll find the following method:

override func prefersStatusBarHidden() -> Bool {
return true
}

This code makes the iOS status bar disappear while our game is running. The Xcode template includes this method because hiding the status bar 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 false 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.swift file that’s created, import the SpriteKit and Foundation frameworks and add the following code:

import UIKit
import SpriteKit
import Foundation

class PlayerNode: SKNode {
override init() {
super.init()
name = "Player \(self)"
initNodeGraph()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

private func initNodeGraph() {
let label = SKLabelNode(fontNamed: "Courier")
label.fontColor = SKColor.darkGrayColor()
label.fontSize = 40
label.text = "v"
label.zRotation = CGFloat(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, each SKNode 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 π (access to this constant is the reason that we had to import the Foundation framework). Since π radians are equal to 180°, this is just what we want.

Adding the Player to the Scene

Now switch back to GameScene.swift. Here, we’re going to add an instance of PlayerNode to the scene. Start off by adding a property to represent the player node:

class GameScene: SKScene {
private var levelNumber: UInt
private var playerLives: Int
private var finished = false
private let playerNode: PlayerNode = PlayerNode()

Continue by adding the following bold code at the end of the init(size:, levelNumber:) method:

addChild(level)

playerNode.position = CGPointMake(CGRectGetMidX(frame),
CGRectGetHeight(frame) * 0.1)
addChild(playerNode)
}

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.swift (you’ll get a compiler error when you add this code—we’ll fix it shortly):

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */

for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if location.y < CGRectGetHeight(frame) * 0.2 {
let target = CGPointMake(location.x, playerNode.position.y)
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 switch over to PlayerNode.swift and add the implementation of that method:

func moveToward(location: CGPoint) {
removeActionForKey("movement")
let distance = pointDistance(position, location)
let screenWidth = UIScreen.mainScreen().bounds.size.width
let duration = NSTimeInterval(2 * distance/screenWidth)

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 create a new Swift file called Geometry.swift and give it the following content:

import Foundation
import UIKit

// Takes a CGVector and a CGFLoat.
// Returns a new CGFloat where each component of v has been multiplied by m.

func vectorMultiply(v: CGVector, m: CGFloat) -> CGVector {
return CGVectorMake(v.dx * m, v.dy * m)
}

// Takes two CGPoints.
// Returns a CGVector representing a direction from p1 to p2.
func vectorBetweenPoints(p1: CGPoint, p2: CGPoint) -> CGVector {
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.
func vectorLength(v: CGVector) -> CGFloat {
return CGFloat(sqrtf(powf(Float(v.dx), 2) + powf(Float(v.dy), 2)))
}

// Takes two CGPoints. Returns a CGFloat containing the distance between them,
// calculated with Pythagoras' theorem.
func pointDistance(p1: CGPoint, p2: CGPoint) -> CGFloat {
return CGFloat(
sqrtf(powf(Float(p2.x - p1.x), 2) + powf(Float(p2.y - p1.y), 2)))
}

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. 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.

func moveToward(location: CGPoint) {
removeActionForKey("movement")
removeActionForKey("wobbling")

let distance = pointDistance(position, location)
let screenWidth = UIScreen.mainScreen().bounds.size.width
let duration = NSTimeInterval(2 * distance/screenWidth)

runAction(SKAction.moveTo(location, duration: duration),
withKey:"movement")

let wobbleTime = 0.3
let halfWobbleTime = wobbleTime/2
let wobbling = SKAction.sequence([
SKAction.scaleXTo(0.2, duration: halfWobbleTime),
SKAction.scaleXTo(1.0, duration: halfWobbleTime)
])
let wobbleCount = Int(duration/wobbleTime)

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 this code toEnemyNode.swift:

import UIKit
import SpriteKit

class EnemyNode: SKNode {
override init() {
super.init()
name = "Enemy \(self)"
initNodeGraph()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

private func initNodeGraph() {
let topRow = SKLabelNode(fontNamed: "Courier-Bold")
topRow.fontColor = SKColor.brownColor()
topRow.fontSize = 20
topRow.text = "x x"
topRow.position = CGPointMake(0, 15)
addChild(topRow)

let middleRow = SKLabelNode(fontNamed: "Courier-Bold")
middleRow.fontColor = SKColor.brownColor()
middleRow.fontSize = 20
middleRow.text = "x"
addChild(middleRow)

let bottomRow = SKLabelNode(fontNamed: "Courier-Bold")
bottomRow.fontColor = SKColor.brownColor()
bottomRow.fontSize = 20
bottomRow.text = "x x"
bottomRow.position = CGPointMake(0, -15)
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.swift. First, add a new property to hold the enemies that will be added to this level:

class GameScene: SKScene {
private var levelNumber: UInt
private var playerLives: Int
private var finished = false
private let playerNode: PlayerNode = PlayerNode()
private let enemies = SKNode()

You might think that we’d use an Array 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:

private func spawnEnemies() {
let count = UInt(log(Float(levelNumber))) + levelNumber
for var i: UInt = 0; i < count; i++ {
let enemy = EnemyNode()
let size = frame.size;
let x = arc4random_uniform(UInt32(size.width * 0.8))
+ UInt32(size.width * 0.1)
let y = arc4random_uniform(UInt32(size.height * 0.5))
+ UInt32(size.height * 0.5)
enemy.position = CGPointMake(CGFloat(x), CGFloat(y))
enemies.addChild(enemy)
}
}

Finally, add these lines near the end of the init(size:, levelNumber:) method to add the enemies node to the scene, and then call the spawnEnemies method:

addChild(playerNode)

addChild(enemies)
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 Swift File from the iOS section, and press Next. Give the new file the name PhysicsCategories.swift and save it, and then add the following code to it:

import Foundation

let PlayerCategory: UInt32 = 1 << 1
let EnemyCategory: UInt32 = 1 << 2
let PlayerMissileCategory: UInt32 = 1 << 3

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 by importing the SpriteKit framework and adding a property to hold this bullet’s thrust vector:

import UIKit
import SpriteKit

class BulletNode: SKNode {
var thrust: CGVector = CGVectorMake(0, 0)

}

Next, we implement the 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 SKPhysicsBodyinstance and 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. We’ll also add the init(coder:) and encodeWithCoder(coder:) methods.

override init() {
super.init()

let dot = SKLabelNode(fontNamed: "Courier")
dot.fontColor = SKColor.blackColor()
dot.fontSize = 40
dot.text = "."
addChild(dot)

let body = SKPhysicsBody(circleOfRadius: 1)
body.dynamic = true
body.categoryBitMask = PlayerMissileCategory
body.contactTestBitMask = EnemyCategory
body.collisionBitMask = EnemyCategory
body.mass = 0.01

physicsBody = body
name = "Bullet \(self)"
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let dx = aDecoder.decodeFloatForKey("thrustX")
let dy = aDecoder.decodeFloatForKey("thrustY")
thrust = CGVectorMake(CGFloat(dx), CGFloat(dy))
}

override func encodeWithCoder(aCoder: NSCoder) {
super.encodeWithCoder(aCoder)
aCoder.encodeFloat(Float(thrust.dx), forKey: "thrustX")
aCoder.encodeFloat(Float(thrust.dy), forKey: "thrustY")
}

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:

class func bullet(from start: CGPoint, toward destination: CGPoint)
-> BulletNode {
let bullet = BulletNode()
bullet.position = start
let movement = vectorBetweenPoints(start, destination)
let magnitude = vectorLength(movement)
let scaledMovement = vectorMultiply(movement, 1/magnitude)

let thrustMagnitude = CGFloat(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:

func applyRecurringForce() {
physicsBody!.applyForce(thrust)
}

Adding Bullets to the Scene

Now switch over to GameScene.swift to add bullets to the scene itself. For starters, add another property to contain all bullets in a single SKNode, just as you did earlier for enemies:

class GameScene: SKScene {
private var levelNumber: UInt
private var playerLives: Int
private var finished = false
private let playerNode: PlayerNode = PlayerNode()
private let enemies = SKNode()
private let playerBullets = SKNode()

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

addChild(enemies)
spawnEnemies()

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:

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */

for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if location.y < CGRectGetHeight(frame) * 0.2 {
let target = CGPointMake(location.x, playerNode.position.y)
playerNode.moveToward(target)
} else {
let bullet =
BulletNode.bullet(from: playerNode.position, toward: location)
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:

override func update(currentTime: CFTimeInterval) {
updateBullets()
}

private func updateBullets() {
var bulletsToRemove:[BulletNode] = []
for bullet in playerBullets.children as [BulletNode] {
// Remove any bullets that have moved off-screen
if !CGRectContainsPoint(frame, bullet.position) {
// Mark bullet for removal
bulletsToRemove.append(bullet)
continue
}

// Apply thrust to remaining bullets
bullet.applyRecurringForce()
}

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.swift. Add the following line to the init() method:

class EnemyNode: SKNode {
override init() {
super.init()
name = "Enemy \(self)"
initNodeGraph()
initPhysicsBody()
}

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

private func initPhysicsBody() {
let body = SKPhysicsBody(rectangleOfSize: CGSizeMake(40, 40))
body.affectedByGravity = false
body.categoryBitMask = EnemyCategory
body.contactTestBitMask = PlayerCategory | EnemyCategory
body.mass = 0.2
body.angularDamping = 0
body.linearDamping = 0
physicsBody = body
}

Then select PlayerNode.swift, where you’re going to do a pretty similar set of things. First, add the bold line shown here to the init() method:

override init() {
super.init()
name = "Player \(self)"
initNodeGraph()
initPhysicsBody()
}

Finally, add the new initPhysicsBody() method:

private func initPhysicsBody() {
let body = SKPhysicsBody(rectangleOfSize: CGSizeMake(20, 20))
body.affectedByGravity = false
body.categoryBitMask = PlayerCategory
body.contactTestBitMask = EnemyCategory
body.collisionBitMask = 0
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:

private func updateEnemies() {
var enemiesToRemove:[EnemyNode] = []
for node in enemies.children as [EnemyNode] {
if !CGRectContainsPoint(frame, node.position) {
// Mark enemy for removal
enemiesToRemove.append(node)
continue
}
}

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:

override func update(currentTime: CFTimeInterval) {
if finished {
return
}
updateBullets()
updateEnemies()
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:

private func checkForNextLevel() {
if enemies.children.isEmpty {
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:

private func goToNextLevel() {
finished = true

let label = SKLabelNode(fontNamed: "Courier")
label.text = "Level Complete!"
label.fontColor = SKColor.blueColor()
label.fontSize = 32
label.position = CGPointMake(frame.size.width * 0.5,
frame.size.height * 0.5)
addChild(label)

let nextLevel = GameScene(size: frame.size, levelNumber: levelNumber + 1)
nextLevel.playerLives = playerLives
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.swift.

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 GameScene class:

class GameScene: SKScene, 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 init(size:, levelNumber:) method:

physicsWorld.gravity = CGVectorMake(0, -1)
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:

func didBeginContact(contact: SKPhysicsContact!) {
if contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask {
// Both bodies are in the same category
let nodeA = contact.bodyA.node!
let nodeB = contact.bodyB.node!

// What do we do with these nodes?
} else {
var attacker: SKNode
var attackee: SKNode

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 is PlayerNode {
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.swift, 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 class extension.

Adding a Class Extension to SKNode

To add an extension 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 Swift File, and then click Next. Name the file SKNode+Extra.swift, and press Create. Open the file in the editor and add the code shown here:

import Foundation
import SpriteKit

extension SKNode {
func receiveAttacker(attacker: SKNode, contact: SKPhysicsContact) {
// Default implementation does nothing
}

func friendlyBumpFrom(node: SKNode) {
// Default implementation does nothing
}
}

Now head back over to GameScene.swift to finish up its part of the collision handling. Go back to the didBeginContact() method, where you’ll add the bits that actually do some work:

func didBeginContact(contact: SKPhysicsContact!) {
if contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask {
// Both bodies are in the same category
let nodeA = contact.bodyA.node!
let nodeB = contact.bodyB.node!

// What do we do with these nodes?
nodeA.friendlyBumpFrom(nodeB)
nodeB.friendlyBumpFrom(nodeA)
} else {
var attacker: SKNode
var attackee: SKNode

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 is PlayerNode {
playerLives--
}

// What do we do with the attacker and the attackee?
attackee.receiveAttacker(attacker, contact: contact)
playerBullets.removeChildrenInArray([attacker])
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.swift and add the following two overrides:

override func friendlyBumpFrom(node: SKNode) {
physicsBody!.affectedByGravity = true
}

override func receiveAttacker(attacker: SKNode,
contact: SKPhysicsContact) {
physicsBody!.affectedByGravity = true
let force = vectorMultiply(attacker.physicsBody!.velocity,
contact.collisionImpulse)
let myContact =
scene!.convertPoint(contact.contactPoint, toNode: self)
physicsBody!.applyForce(force, atPoint: myContact)
}

The first of those methods, 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 adding a property observer to the playerLivesproperty in GameScene.swift, like this:

class GameScene: SKScene, SKPhysicsContactDelegate {
private var levelNumber: UInt
private var playerLives: Int {
didSet {
let lives = childNodeWithName("LivesLabel") as SKLabelNode
lives.text = "Lives: \(playerLives)"
}
}

The preceding snippet uses the name we previously associated with the label (in the init(size:, levelNumber:) 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 negative number of lives, as you can see in Figure 17-10.

image

Figure 17-10. That’s a strange number of lives!

The reason this problem appears is 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.swift and add the bold code shown here to the bottom of the receiveAttacker(_, contact:) method:

override func receiveAttacker(attacker: SKNode, contact: SKPhysicsContact) {
physicsBody!.affectedByGravity = true
let force = vectorMultiply(attacker.physicsBody!.velocity,
contact.collisionImpulse)
let myContact = scene!.convertPoint(contact.contactPoint, toNode: self)
physicsBody!.applyForce(force, atPoint: myContact)

let path = NSBundle.mainBundle().pathForResource("MissileExplosion",
ofType: "sks")
let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile(path!)
as SKEmitterNode
explosion.numParticlesToEmit = 20
explosion.position = contact.contactPoint
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.swift and add this method:

override func receiveAttacker(attacker: SKNode, contact: SKPhysicsContact) {
let path = NSBundle.mainBundle().pathForResource("EnemyExplosion",
ofType: "sks")
let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile(path!)
as SKEmitterNode
explosion.numParticlesToEmit = 50
explosion.position = contact.contactPoint
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 consequences 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 GameOverScene.swift:

import UIKit
import SpriteKit

class GameOverScene: SKScene {

override init(size: CGSize) {
super.init(size: size)
backgroundColor = SKColor.purpleColor()
let text = SKLabelNode(fontNamed: "Courier")
text.text = "Game Over"
text.fontColor = SKColor.whiteColor()
text.fontSize = 50
text.position = CGPointMake(frame.size.width/2, frame.size.height/2)
addChild(text)
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}

Now let’s switch back to GameScene.swift. 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:

private func triggerGameOver() {
finished = true

let path = NSBundle.mainBundle().pathForResource("EnemyExplosion",
ofType: "sks")
let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile(path!)
as SKEmitterNode
explosion.numParticlesToEmit = 200
explosion.position = playerNode.position
scene!.addChild(explosion)
playerNode.removeFromParent()

let transition = SKTransition.doorsOpenVerticalWithDuration(1)
let gameOver = GameOverScene(size: frame.size)
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 true to indicate the game ended or false to indicate that it’s still on:

private func checkForGameOver() -> Bool {
if playerLives == 0 {
triggerGameOver()
return true
}
return false
}

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!

override func update(currentTime: CFTimeInterval) {
if finished {
return
}
updateBullets()
updateEnemies()
if (!checkForGameOver()) {
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.swift to complete this class:

import UIKit
import SpriteKit

class StartScene: SKScene {

override init(size: CGSize) {
super.init(size: size)
backgroundColor = SKColor.greenColor()

let topLabel = SKLabelNode(fontNamed: "Courier")
topLabel.text = "TextShooter"
topLabel.fontColor = SKColor.blackColor()
topLabel.fontSize = 48
topLabel.position = CGPointMake(frame.size.width/2,
frame.size.height * 0.7)
addChild(topLabel)

let bottomLabel = SKLabelNode(fontNamed: "Courier")
bottomLabel.text = "Touch anywhere to start"
bottomLabel.fontColor = SKColor.blackColor()
bottomLabel.fontSize = 20
bottomLabel.position = CGPointMake(frame.size.width/2,
frame.size.height * 0.3)
addChild(bottomLabel)
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

override func touchesBegan(touches: NSSet,
withEvent event: UIEvent) {
let transition = SKTransition.doorwayWithDuration(1.0)
let game = GameScene(size:frame.size)
view!.presentScene(game, transition: transition)
}
}

Now go back to GameOverScene.swift, so we can make the game-over scene perform a transition to the start scene. Add the following code:

override func didMoveToView(view: SKView) {
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, 3 * Int64(NSEC_PER_SEC)),
dispatch_get_main_queue()) {
let transition = SKTransition.flipVerticalWithDuration(1)
let start = StartScene(size: self.frame.size)
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.swift. In theviewDidLoad() method, we just replace the code to create one scene class with another:

override func viewDidLoad() {
super.viewDidLoad()

/* Pick a size for the scene */
let scene = GameScene(size: view.frame.size, levelNumber: 1)
let scene = StartScene(size: view.frame.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.swift, adding the bold code to the end of the bullet(from:, toward:) method, just before the return line:

bullet.runAction(SKAction.playSoundFileNamed("shoot.wav",
waitForCompletion: false))

Next, switch over to EnemyNode.swift, adding these lines to the end of the receiveAttacker(_, contact:) method:

runAction(SKAction.playSoundFileNamed("enemyHit.wav",
waitForCompletion: false))

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

runAction(SKAction.playSoundFileNamed("playerHit.wav",
waitForCompletion: false))

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.swift, add these lines at the end of the touchesBegan(_, withEvent:) method:

runAction(SKAction.playSoundFileNamed("gameStart.wav",
waitForCompletion: false))

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

runAction(SKAction.playSoundFileNamed("gameOver.wav",
waitForCompletion: false))

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.swift. Make the following change in that file:

let PlayerCategory: UInt32 = 1 << 1
let EnemyCategory: UInt32 = 1 << 2
let PlayerMissileCategory: UInt32 = 1 << 3
let GravityFieldCategory: UInt32 = 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.swift:

private func initPhysicsBody() {
let body = SKPhysicsBody(rectangleOfSize: CGSizeMake(40, 40))
body.affectedByGravity = false
body.categoryBitMask = EnemyCategory
body.contactTestBitMask = PlayerCategory | EnemyCategory
body.mass = 0.2
body.angularDamping = 0
body.linearDamping = 0
body.fieldBitMask = 0
physicsBody = body
}

Make a similar change in PlayerNode.swift:

private func initPhysicsBody() {
let body = SKPhysicsBody(rectangleOfSize: CGSizeMake(20, 20))
body.affectedByGravity = false
body.categoryBitMask = PlayerCategory
body.contactTestBitMask = EnemyCategory
body.collisionBitMask = 0
body.fieldBitMask = 0
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.swift:

override init() {
super.init()

let dot = SKLabelNode(fontNamed: "Courier")
dot.fontColor = SKColor.blackColor()
dot.fontSize = 40
dot.text = "."
addChild(dot)

let body = SKPhysicsBody(circleOfRadius: 1)
body.dynamic = true
body.categoryBitMask = PlayerMissileCategory
body.contactTestBitMask = EnemyCategory
body.collisionBitMask = EnemyCategory
body.fieldBitMask = GravityFieldCategory
body.mass = 0.01

physicsBody = body
name = "Bullet \(self)"
}

The rest of the changes are going to be in the file GameScene.swift. 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 as a new property:

class GameScene: SKScene, SKPhysicsContactDelegate {
private var levelNumber: UInt
private var playerLives: Int {
didSet {
let lives = childNodeWithName("LivesLabel") as SKLabelNode
lives.text = "Lives: \(playerLives)"
}
}
private var finished = false
private let playerNode: PlayerNode = PlayerNode()
private let enemies = SKNode()
private let playerBullets = SKNode()
private let forceFields = SKNode()

At the end of the init(size:, levelNumber:) method, add code to add the forceFields node to the scene and create the actual force field nodes:

addChild(playerBullets)

addChild(forceFields)
createForceFields()

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

Finally, add the implementation of the createForceFields() method:

private func createForceFields() {
let fieldCount = 3
let size = frame.size
let sectionWidth = Int(size.width)/fieldCount
for var i = 0; i < fieldCount; i++ {
let x = CGFloat(i * sectionWidth +
arc4random_uniform(UInt32(sectionWidth)))
let y = CGFloat(arc4random_uniform(UInt32(size.height * 0.25))
+ UInt32(size.height * 0.25))

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

let fieldLocationNode = SKLabelNode(fontNamed: "Courier")
fieldLocationNode.fontSize = 16
fieldLocationNode.fontColor = SKColor.redColor()
fieldLocationNode.name = "GravityField"
fieldLocationNode.text = "*"
fieldLocationNode.position = CGPointMake(x, y)
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 threeSKLabelNodes 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!