In this chapter, many of the Sprite Kit Framework features outlined in the previous chapter will be used to create a game-based app. In particular, this tutorial will demonstrate the practical use of scenes, textures, sprites, labels, and actions. In addition, the app created in this chapter will also use physics bodies to demonstrate the use of collisions and simulated gravity.
This tutorial will also demonstrate using the Xcode Sprite Kit Level, Live, and Action editors combined with Swift code to create a Sprite Kit-based game.
About the Sprite Kit Demo Game
The game created in this chapter consists of a single animated character that shoots arrows across the scene when the screen is tapped. For the game’s duration, balls fall from the top of the screen, with the objective being to hit as many balls as possible with the arrows.
The completed game will comprise the following two scenes:
- GameScene – The scene which appears when the game is first launched. The scene will announce the game’s name and invite the user to touch the screen to begin the game. The game will then transition to the second scene.
- ArcheryScene – The scene where the gameplay takes place. Within this scene, the archer and ball sprites are animated, and the physics behavior and collision detection are implemented to make the game work.
In terms of sprite nodes, the game will include the following:
- Welcome Node – An SKLabelNode instance that displays a msage to the user on the Welcome Scene.
- Archer Node – An SKSpriteNode instance to represent the archer game character. The animation frames that cause the archer to load and launch an arrow are provided via a sequence of image files contained within a texture atlas.
- Arrow Node – An SKSpriteNode instance used to represent the arrows as the archer character shoots them. This node has associated with it a physics body so that collisions can be detected and to make sure it responds to gravity.
- Ball Node – An SKSpriteNode represents the balls that fall from the sky. The ball has associated with it a physics body for gravity and collision detection purposes.
- Game Over Node – An SKLabelNode instance that displays the score to the user at the end of the game. The overall architecture of the game can be represented hierarchically, as outlined in Figure 93-1:
In addition to the nodes outlined above, the Xcode Live and Action editors will be used to implement animation and audio actions, which will be triggered from within the app’s code.
Creating the SpriteKitDemo Project
To create the project, launch Xcode and select the Create a new Xcode project option from the welcome screen (or use the File -> New -> Project…) menu option. Next, on the template selection panel, choose the iOS Game template option. Click on the Next button to proceed and on the resulting options screen, name the product SpriteKitDemo and choose Swift as the language in which the app will be developed. Finally, set the Game Technology menu to SpriteKit. Click Next and choose a suitable location for the project files. Once selected, click Create to create the project.
Reviewing the SpriteKit Game Template Project
The selection of the SpriteKit Game template has caused Xcode to create a template project with a demonstration incorporating some pre-built Sprite Kit behavior. This template consists of a View Controller class (GameViewController.swift), an Xcode Sprite Kit scene file (GameScene.sks), and a corresponding GameScene class file (GameScene.swift). The code within the GameViewController.swift file loads the scene design contained within the GameScene.sks file and presents it on the view to be visible to the user. This, in turn, triggers a call to the didMove(to view:) method of the GameScene class as implemented in the GameScene.swift file. This method creates an SKLabelNode displaying text that reads “Hello, World!”.
The GameScene class also includes a variety of touch method implementations that create SKShapeNode instances into which graphics are drawn when triggered. These nodes, in turn, are displayed in response to touches and movements on the device screen. To see the template project in action, run it on a physical device or the iOS simulator and perform tapping and swiping motions on the display.
As impressive as this may be, given how little code is involved, this bears no resemblance to the game that will be created in this chapter, so some of this functionality needs to be removed to provide a clean foundation on which to build. Begin the tidying process by selecting and editing the GameScene.swift file to remove the code to create and present nodes in the scene. Once modified, the file should read as follows:
class GameScene: SKScene {
override func didMove(to view: SKView) {
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
Code language: Swift (swift)
With these changes, it is time to start creating the SpriteKitDemo game.
Restricting Interface Orientation
The game created in this tutorial assumes that the device on which it is running will be in landscape orientation. Therefore, to prevent the user from attempting to play the game with a device in portrait orientation, the Device Orientation properties for the project need to be restricted. To achieve this, select the SpriteKitDemo entry located at the top of the Project Navigator and, in the resulting General settings panel, change the device orientation settings so that only the Landscape options are selected both for iPad and iPhone devices:
Modifying the GameScene SpriteKit Scene File
As previously outlined, Xcode has provided a SpriteKit scene file (GameScene.sks) for a scene named GameScene together with a corresponding class declaration contained within the GameScene.swift file. The next task is to repurpose this scene to act as the welcome screen for the game. Begin by selecting the GameScene.sks file so that it loads into the SpriteKit Level Editor, as shown in Figure 93-3:
When working with the Level Editor to design SpriteKit scenes, there are several key areas of importance, each of which has been labeled in the above figure:
- A – Scene Canvas – This is the canvas onto which nodes may be placed, positioned, and configured.
- B – Attribute Inspector Panel – This panel provides a range of configuration options for the currently selected item in the editor panel. This allows SKNode and SKAction objects to be customized within the editor environment.
- C – Library Button – This button displays the Library panel containing a range of node and effect types that can be dragged and dropped onto the scene.
- D – Animate/Layout Button – Toggles between the editor’s simulation and layout editing modes. Simulate mode provides a useful mechanism for previewing the scene behavior without compiling and running the app.
- E – Zoom Buttons – Buttons to zoom in and out of the scene canvas.
- F – Live Editor – The live editor allows actions and animations to be placed within a timeline and simulated within the editor environment. It is possible, for example, to add animation and movement actions within the live editor and play them back live within the scene canvas.
- G – Timeline View Slider – Pans back and forth through the view of the live editor timeline.
- H – Playback Speed – When in Animation mode, this control adjusts the playback speed of the animations and actions contained within the live editor panel.
- I – Scene Graph View – This panel provides an overview of the scene’s hierarchy and can be used to select, delete, duplicate and reposition scene elements within the hierarchy.
Within the scene editor, click on the “Hello, World!” Label node and press the keyboard delete key to remove it from the scene. With the scene selected in the scene canvas, click on the Color swatch in the Attribute Inspector panel and use the color selection dialog to change the scene color to a shade of green. Remaining within the Attributes Inspector panel, change the Size setting from Custom to iPad 9.7” in Landscape mode:
Click on the button (marked C in Figure 93-3 above) to display the Library panel, locate the Label node object, and drag and drop an instance onto the center of the scene canvas. With the label still selected, change the Text property in the inspector panel to read “SpriteKitDemo – Tap Screen to Play”. Remaining within the inspector panel, click on the T next to the font name and use the font selector to assign a 56-point Marker Felt Wide font to the label from the Fun font category. Finally, set the Name property for the label node to “welcomeNode”. Save the scene file before proceeding.
With these changes complete, the scene should resemble that of Figure 93-5:
Creating the Archery Scene
As previously outlined, the game’s first scene is a welcome screen on which the user will tap to begin playing within a second scene. Add a new class to the project to represent this second scene by selecting the File -> New -> File… menu option. In the file template panel, make sure that the Cocoa Touch Class template is selected in the main panel. Click on the Next button and configure the new class to be a subclass of SKScene named ArcheryScene. Click on the Next button and create the new class file within the project folder.
The new scene class will also require a corresponding SpriteKit scene file. Select File -> New -> File… once again, this time selecting SpriteKit Scene from the Resource section of the main panel (Figure 93-6). Click Next, name the scene ArcheryScene and click the Create button to add the scene file to the project.
Edit the newly added ArcheryScene.swift file and modify it to import the SpriteKit Framework as follows:
import UIKit
import SpriteKit
class ArcheryScene: SKScene {
}
Code language: Swift (swift)
Transitioning to the Archery Scene
Clearly, having instructed the user to tap the screen to play the game, some code needs to be written to make this happen. This behavior will be added by implementing the touchesBegan method in the GameScene class. Rather than move directly to ArcheryScene, some effects will be added as an action and transition.
When implemented, the SKAction will cause the node to fade from view, while an SKTransition instance will be used to animate the transition from the current scene to the archery scene using a “doorway” style of animation. Implement these requirements by adding the following code to the touchesBegan method in the GameScene. swift file:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let welcomeNode = childNode(withName: "welcomeNode") {
let fadeAway = SKAction.fadeOut(withDuration: 1.0)
welcomeNode.run(fadeAway, completion: {
let doors = SKTransition.doorway(withDuration: 1.0)
if let archeryScene = ArcheryScene(fileNamed: "ArcheryScene") {
self.view?.presentScene(archeryScene, transition: doors)
}
})
}
}
Code language: Swift (swift)
Before moving on to the next steps, we will take some time to provide more detail on the above code.
From within the context of the touchesBegan method, we have no direct reference to the welcomeNode instance. However, we know that when it was added to the scene in the SpriteKit Level Editor, it was assigned the name “welcomeNode”. Using the childNode(withName:) method of the scene instance, therefore, a reference to the node is being obtained within the touchesBegan method as follows:
if let welcomeNode = childNode(withName: "welcomeNode") {
Code language: Swift (swift)
The code then checks that the node was found before creating a new SKAction instance configured to cause the node to fade from view over a one-second duration:
let fadeAway = SKAction.fadeOut(withDuration: 1.0)
Code language: Swift (swift)
The action is then executed on the welcomeNode. A completion block is also specified to be executed when the action completes. This block creates an instance of the ArcheryScene class preloaded with the scene contained within the ArcheryScene.sks file and an appropriately configured SKTransition object. The transition to the new scene is then initiated:
let fadeAway = SKAction.fadeOut(withDuration: 1.0)
welcomeNode.run(fadeAway, completion: {
let doors = SKTransition.doorway(withDuration: 1.0)
if let archeryScene = ArcheryScene(fileNamed: "ArcheryScene") {
self.view?.presentScene(archeryScene, transition: doors)
}
})
Code language: Swift (swift)
Compile and run the app on an iPad device or simulator in landscape orientation. Once running, tap the screen and note that the label node fades away and that after the transition to the ArcheryScene takes effect, we are presented with a gray scene that now needs to be implemented.
Adding the Texture Atlas
Before textures can be used on a sprite node, the texture images must first be added to the project. Textures take the form of image files and may be added individually to the project’s asset catalog. However, for larger numbers of texture files, it is more efficient (both for the developer and the app) to create a texture atlas. In the case of the archer sprite, this will require twelve image files to animate an arrow’s loading and subsequent shooting. A texture atlas will be used to store these animation frame images. The images for this project can be found in the sample code download, which can be obtained from the following web page:
https://www.ebookfrenzy.com/web/ios16/
Within the code sample archive, locate the folder named sprite_images. Located within this folder is the archer. atlas sub-folder, which contains the animation images for the archer sprite node.
To add the atlas to the project, select the Assets catalog file in the Project Navigator to display the image assets panel. Locate the archer.atlas folder in a Finder window and drag and drop it onto the asset catalog panel so that it appears beneath the existing AppIcon entry, as shown in the following figure:
Designing the Archery Scene
The layout for the archery scene is contained within the ArcheryScene.sks file. Select this file so that it loads into the Level Editor environment. With the scene selected in the canvas, use the Attributes Inspector panel to change the color property to white and the Size property to landscape iPad 9.7”.
From within the SpriteKit Level Editor, the next task is to add the sprite node representing the archer to the scene. Display the Library panel, select the Media Library tab as highlighted in Figure 93-8 below, and locate the archer001.png texture image file:
Once located, change the Size property in the Attributes Inspector to iPad 9.7”, then drag and drop the texture onto the canvas and position it so that it is located in the vertical center of the scene at the left-hand edge, as shown in the following figure:
With the archer node selected, use the Attributes Inspector panel to assign the name “archerNode” to the sprite. The next task is to define the physical outline of the archer sprite. The SpriteKit system will use this outline when deciding whether the sprite has been involved in a collision with another node within the scene. By default, the physical shape is assumed to be a rectangle surrounding the sprite texture (represented by the blue boundary around the node in the scene editor). Another option is to define a circle around the sprite to represent the physical shape. A much more accurate approach is to have SpriteKit define the physical shape of the node based on the outline of the sprite texture image. With the archer node selected in the scene, scroll down within the Attribute Inspector panel until the Physics Definition section appears. Then, using the Body Type menu, change the setting to Alpha mask:
Before proceeding with the next phase of the development process, test that the scene behaves as required by clicking on the Animate button located along the bottom edge of the editor panel. Note that the archer slides down and disappears off the bottom edge of the scene. This is because the sprite is configured to be affected by gravity. For the game’s purposes, the archer must be pinned to the same location and not subject to the laws of gravity. Click on the Layout button to leave simulation mode, select the archer sprite and, within the Physical Definition section, turn the Pinned option on and the Dynamic, Allows Rotation, and Affected by Gravity options off. Re-run the animation to verify that the archer sprite now remains in place.
Preparing the Archery Scene
Select the ArcheryScene.swift file and modify it as follows to add some private variables and implement the didMove(to:) method:
import UIKit
import SpriteKit
class ArcheryScene: SKScene {
var score = 0
var ballCount = 20
override func didMove(to view: SKView) {
let archerNode = self.childNode(withName: "archerNode")
archerNode?.position.y = 0
archerNode?.position.x = -self.size.width/2 + 40
self.initArcheryScene()
}
.
.
}
Code language: Swift (swift)
When the archer node was added to the ArcheryScene, it was positioned using absolute X and Y coordinates. This means the node will be positioned correctly on an iPad with a 9.7” screen but not on any other screen sizes. Therefore, the first task performed by the didMove method is to position the archer node correctly relative to the screen size. Regarding the scene, position 0, 0 corresponds to the screen’s center point. Therefore, to position the archer node in the vertical center of the screen, the y-coordinate is set to zero. The code then obtains the screen’s width, performs a basic calculation to identify a position 40 points in from the screen’s left-hand edge, and assigns it to the x-coordinate of the node.
The above code then calls another method named initArcheryScene which now needs to be implemented as follows within the ArcheryScene.swift file ready for code which will be added later in the chapter:
func initArcheryScene() {
}
Code language: Swift (swift)
Preparing the Animation Texture Atlas
When the user touches the screen, the archer sprite node will launch an arrow across the scene. For this example, we want the sprite character’s loading and shooting of the arrow to be animated. The texture atlas already contains the animation frames needed to implement this (named sequentially from archer001.png through to archer012.png), so the next step is to create an action to animate this sequence of frames. One option would be to write some code to perform this task. A much easier option, however, is to create an animation action using the SpriteKit Live Editor.
Begin by selecting the ArcheryScene.sks file so that it loads into the editor. Once loaded, the first step is to add an AnimateWithTextures action within the timeline of the live editor panel. Next, within the Library panel, scroll down the list of objects until the AnimateWithTextures Action object appears. Once located, drag and drop an instance of the object onto the live editor timeline for the archerNode as indicated in Figure 93-11:
With the animation action added to the timeline, the action needs to be configured with the texture sequence to be animated. With the newly added action selected in the timeline, display the Media Library panel so that the archer texture images are listed. Next, use the Command-A keyboard sequence to select all of the images in the library and then drag and drop those images onto the Textures box in the Animate with Textures attributes panel, as shown in Figure 93-12:
Test the animation by clicking on the Animate button. The archer sprite should animate through the sequence of texture images to load and shoot the arrow.
Compile and run the app and tap on the screen to enter the archery scene. On appearing, the animation sequence will execute once. The animation sequence should only run when the user taps the screen to launch an arrow. Having this action within the timeline, therefore, does not provide the required behavior for the game. Instead, the animation action needs to be converted to a named action reference, placed in an action file, and triggered from within the touchesBegan method of the archer scene class.
Creating the Named Action Reference
With the ArcherScene.sks file loaded into the level editor, right-click on the Animate with Textures action in the timeline and select the Convert to Reference option from the popup menu:
In the Create Action panel, name the action animateArcher and change the File menu to Create New File. Next, click on the Create button and, in the Save As panel, navigate to the SpriteKitDemo subfolder of the main project folder and enter ArcherActions into the Save As: field before clicking on Create.
Since the animation action is no longer required in the timeline of the archer scene, select the ArcherScene.sks file, right-click on the Animate with Texture action in the timeline, and select Delete from the menu.
Triggering the Named Action from the Code
With the previous steps completed, the project now has a named action (named animateArcher) which can be triggered each time the screen is tapped by adding some code to the touchesBegan method of the ArcheryScene. swift file. With this file selected in the Project Navigator panel, implement this method as follows:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let archerNode = self.childNode(withName: "archerNode"),
let animate = SKAction(named: "animateArcher") {
archerNode.run(animate)
}
}
Code language: Swift (swift)
Run the app and touch the screen within the Archery Scene. Each time a touch is detected, the archer sprite will run through the animation sequence of shooting an arrow.
Creating the Arrow Sprite Node
At this point in the tutorial, the archer sprite node goes through an animation sequence of loading and shooting an arrow, but no actual arrow is being launched across the scene. To implement this, a new sprite node must be added to the ArcheryScene. This node will be textured with an arrow image and placed to the right of the archer sprite at the end of the animation sequence. Then, a physics body will be associated with the arrow, and an impulse force will be applied to it to propel it across the scene as though shot by the archer’s bow. This task will be performed entirely in code to demonstrate the alternative to using the action and live editors.
Begin by locating the ArrowTexture.png file in the sprite_images folder of the sample code archive and drag and drop it onto the left-hand panel of the Assets catalog screen beneath the archer texture atlas entry. Next, add a new method named createArrowNode within the ArcheryScene.swift file so that it reads as follows:
func createArrowNode() -> SKSpriteNode {
let arrow = SKSpriteNode(imageNamed: "ArrowTexture.png")
if let archerNode = self.childNode(withName: "archerNode"),
let archerPosition = archerNode.position as CGPoint?,
let archerWidth = archerNode.frame.size.width as CGFloat? {
arrow.position = CGPoint(x: archerPosition.x + archerWidth,
y: archerPosition.y)
arrow.name = "arrowNode"
arrow.physicsBody = SKPhysicsBody(rectangleOf:
arrow.frame.size)
arrow.physicsBody?.usesPreciseCollisionDetection = true
}
return arrow
}
Code language: JavaScript (javascript)
The code creates a new SKSpriteNode object, positions it to the right of the archer sprite node, and assigns the name arrowNode. A physics body is then assigned to the node, using the node’s size as the boundary of the body and enabling precision collision detection. Finally, the node is returned.
Shooting the Arrow
A physical force needs to be applied to propel the arrow across the scene. The arrow sprite’s creation and propulsion must be timed to occur at the end of the archer animation sequence. This timing can be achieved via some minor modifications to the touchesBegan method:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let archerNode = self.childNode(withName: "archerNode"),
let animate = SKAction(named: "animateArcher") {
let shootArrow = SKAction.run({
let arrowNode = self.createArrowNode()
self.addChild(arrowNode)
arrowNode.physicsBody?.applyImpulse(CGVector(dx: 60, dy: 0))
})
let sequence = SKAction.sequence([animate, shootArrow])
archerNode.run(sequence)
}
}
Code language: JavaScript (javascript)
A new SKAction object is created, specifying a block of code to be executed. This run block calls the createArrowNode method, adds the new node to the scene, and then applies an impulse force of 60.0 on the
X-axis of the scene. An SKAction sequence comprises the previously created animation action and the new run block action. This sequence is then run on the archer node.
When executed with these changes, touching the screen should now cause an arrow to be launched after the archer animation completes. Then, as the arrow flies across the scene, it gradually falls toward the bottom of the display. This behavior is due to gravity’s effect on the physics body assigned to the node.
Adding the Ball Sprite Node
The game’s objective is to score points by hitting balls with arrows. So, the next logical step is adding the ball sprite node to the scene. Begin by locating the BallTexture.png file in the sprite_images folder of the sample code package and drag and drop it onto the Assets.xcassets catalog.
Next, add the corresponding createBallNode method to the ArcheryScene.swift file as outlined in the following code fragment:
func createBallNode() {
let ball = SKSpriteNode(imageNamed: "BallTexture.png")
let screenWidth = self.size.width
ball.position = CGPoint(x: CGFloat.random(
in: -screenWidth/2 ..< screenWidth/2-100),
y: self.size.height-50)
ball.name = "ballNode"
ball.physicsBody = SKPhysicsBody(circleOfRadius:
(ball.size.width/2))
ball.physicsBody?.usesPreciseCollisionDetection = true
self.addChild(ball)
}
Code language: PHP (php)
This code creates a sprite node using the ball texture and then sets the initial position at the top of the scene but a random position on the X-axis. Since position 0 on the X-axis corresponds to the horizontal center of the screen (as opposed to the far left side), some calculations are performed to ensure that the balls can fall from most of the screen’s width using random numbers for the X-axis values.
The node is assigned a name and a circular physics body slightly less than the radius of the ball texture image. Finally, precision collision detection is enabled, and the ball node is added to the scene.
Next, modify the initArcheryScene method to create an action to release a total of 20 balls at one-second intervals:
func initArcheryScene() {
let releaseBalls = SKAction.sequence([SKAction.run({
self.createBallNode() }),
SKAction.wait(forDuration: 1)])
self.run(SKAction.repeat(releaseBalls,
count: ballCount))
}
Code language: PHP (php)
Run the app and verify that the balls now fall from the top of the scene. Then, attempt to hit the balls as they fall by tapping the background to launch arrows. Note, however, that when an arrow hits a ball, it simply bounces off:
The goal for the completed game is to have the balls burst with a sound effect when hit by the arrow and for a score to be presented at the end of the game. The steps to implement this behavior will be covered in the next chapters.
The balls fall from the top of the screen because they have been assigned a physics body and are subject to the simulated forces of gravity within the Sprite Kit physical world. To reduce the effects of gravity on both the arrows and balls, modify the didMove(to view:) method to change the current gravity setting on the scene’s physicsWorld object:
override func didMove(to view: SKView) {
let archerNode = self.childNode(withName: "archerNode")
archerNode?.position.y = 0
archerNode?.position.x = -self.size.width/2 + 40
self.physicsWorld.gravity = CGVector(dx: 0, dy: -1.0)
self.initArcheryScene()
}
Code language: PHP (php)
Summary
The goal of this chapter has been to create a simple game for iOS using the Sprite Kit framework. In creating this game, topics such as using sprite nodes, actions, textures, sprite animations, and physical forces have been used to demonstrate the use of the Xcode Sprite Kit editors and Swift code.
In the next chapter, this game example will be further extended to demonstrate the detection of collisions.