Berg Briscoe Hafizji 2D IOS TvOS Games 2015
Berg Briscoe Hafizji 2D IOS TvOS Games 2015
by Tutorials
By the raywenderlich.com Tutorial Team
This book and all corresponding materials (such as source code) are provided
on an "as is" basis, without warranty of any kind, express or implied, including
but not limited to the warranties of merchantability, fitness for a particular
purpose, and noninfringement. In no event shall the authors or copyright
holders be liable for any claim, damages or other liability, whether in action of
contract, tort or otherwise, arising from, out of or in connection with the
software or the use or other dealings in the software.
All trademarks and registered trademarks appearing in this book are the
property of their respective owners.
Dedications
"To my wonderful wife and family, who make it possible to do what I do."
— Mike Berg
“To my family and loved ones who always support me and my ambitions.”
— Neil North
“To Caroline, Nina and Lucy – for your endless encouragement and support.”
— Toby Stephens
Harken all genders! You may or may not have noticed that all of this book’s
authors are men. This is unfortunate, and not by design. If you are a woman
developing for iOS and are interested in joining the Tutorial Team to write about
gaming topics, we’d love to hear from you! !
Vinnie Prabhu created all of the music and sounds for the
games in this book. Vinnie is a music composer/software
engineer from Northern Virginia who has done music and
sound work for concerts, plays and video games. He's also a
staff member on OverClocked ReMix, an online community for
music and video game fans. You can find Vinnie on Twitter as
@palpablevt.
raywenderlich.com 7
2D iOS & tvOS Games by Tutorials
raywenderlich.com 8
2D iOS & tvOS Games by Tutorials
raywenderlich.com 9
2D iOS & tvOS Games by Tutorials
raywenderlich.com 10
2D iOS & tvOS Games by Tutorials
raywenderlich.com 11
2D iOS & tvOS Games by Tutorials
raywenderlich.com 12
2D iOS & tvOS Games by Tutorials
raywenderlich.com 13
2D iOS & tvOS Games by Tutorials
raywenderlich.com 14
2D iOS & tvOS Games by Tutorials
raywenderlich.com 15
2D iOS & tvOS Games by Tutorials
raywenderlich.com 16
I Introduction
In this book, you will learn how to make iOS and tvOS games in Swift using Apple’s
built-in 2D game framework: Sprite Kit. However, this raises a number of
questions:
• Why Sprite Kit? Sprite Kit is Apple's built-in framework for making 2D games
on iOS. It's easy to learn, especially if you already have some Swift or iOS
experience.
• Why iOS? For a game developer, there’s no better platform. The development
tools are well-designed and easy to learn, and the App Store makes it incredibly
simple to distribute your game to a massive audience – and get paid for it!
• Why tvOS? Just recently Apple released a new Apple TV, along with the ability
for developers to write their own games for it. And one of the great things about
Sprite Kit is it's cross-platform on iOS, OS X, and tvOS. If you get your game
running on iOS it's incredibly easy to get it working on the other platforms as
well. And it's super exciting to get your games to run on the big screen!
• Why Swift? Swift is an easy language to get started with, especially if you are a
beginner to the iOS platform. In addition, we believe Swift is the way of the
future for iOS development, so take this as an opportunity to develop your Swift
skills early!
• Why 2D? As impressive as 3D games may be, 2D games are a lot easier to
make. The artwork is far less complicated, and programming is faster and
doesn’t require as much math. All of this allows you as a developer to focus on
creating killer gameplay.
If you’re a beginner, making 2D games is definitely the best way to get started.
If you’re an advanced developer, you can still make a 2D game much faster than a
3D game. Since it’s not necessarily the case that you earn more money with 3D
games, why not go for the easier win? Plus, some people (like myself) prefer 2D
games anyway!
raywenderlich.com 17
2D iOS & tvOS Games by Tutorials Introduction
So rest easy – with iOS, tvOS, 2D games and Sprite Kit, you’re making great
choices!
This year at WWDC, Apple announced a brand new set of APIs called GameplayKit.
These are a set of APIs that make it easy to add pathfinding, AI, and other cool
features into your games.
Then there's the elephant in the room - tvOS, which now allows us to create games
for the living room!
These changes were so significant that rather than trying to give them a token
coverage in an update to iOS Games by Tutorials, we decided it would be better to
revamp the book completely. Hence this book!
If you have already read iOS Games by Tutorials and you're wondering what's new
in this book, here are the highlights:
• Zombie Conga: Chapters 1-4 are mostly the same as in iOS Games by
Tutorials. Chapter 5, "Camera" is completely new, covering the new SKCamera
class introduced in iOS 9. We also moved coverage of Labels to this game in
Chapter 6, "Labels". Finally, we added a new chapter on porting the game to
tvOS in Chapter 7, "Beginning tvOS".
• Cat Nap: These chapters have been heavily refactored to make use of new
features introduced in the Sprite Kit Scene Editor in iOS 9, such as reference
nodes. Chapter 12, "Crop, Video, and Shape Nodes" is completely rewritten to
cover the topic more effectively. Finally, we added a new chapter on some more
advance tvOS porting techniques in Chapter 13, "Intermediate tvOS".
• Drop Charge: This is a new game and new set of chapters (Chapters 14-17). In
these chapters, you'll review previous material in the book and learn about
GameplayKit state machines, particle systems, and juice.
• Dino Defense: This is a second new game and new set of chapters (Chapters
18-20). In these chapters, you'll take a deep dive into GameplayKit and learn
about its Entity-Component System, Pathfinding, and Agents, Goals, and
Behaviors features.
• Delve: This is a third brand new game and new set of chapters (Chapters
21-24). In these chapters, you'll dive into more advanced concepts like tile map
games, procedural levels, and GameplayKit randomization.
raywenderlich.com 18
2D iOS & tvOS Games by Tutorials Introduction
As you can see, it's a major overhaul. If you've read the book before and have
limited time, the best thing to do would be to focus on the new games, or the
chapters that interest you most.
There are a lot of game programming books out there, and many of them are quite
good, so this is a lofty goal! Here’s what we’ve done to try to accomplish it:
• Learn by making games: All the books teach the high-level concepts and show
code snippets, but many leave you on your own to put together a complete,
functioning game. In this book, you will learn by making five games in a variety
of genres – games that are actually fun. Our hope is that you can and will reuse
techniques or code from these games to make your own games.
• Focus on polish: The key to making a hit game is polish – adding loads of well-
considered details that set your game apart. Because of this, we’ve put our
money where our mouths are and invested in a top-notch artist and sound
designer to create resources for the games in this book. We’ve also included a
chapter all about polishing your game with special effects – otherwise known as
adding “Juice” – which we think you will love.
After you finish reading this book, please let me know if you think we were
successful in meeting these goals. You can email me anytime at
[email protected].
raywenderlich.com 19
2D iOS & tvOS Games by Tutorials Introduction
We hope you enjoy the book, and we can’t wait to see what games you come up
with!
To resolve this, with iOS 7 Apple released a new framework for making 2D games:
Sprite Kit. Its API is very similar to Cocos2D, with similar types for the sprites,
actions and scenes that Cocos2D developers know and love, so fans of the older
framework will have no trouble getting up to speed. Sprite Kit also has a few extra
bells and whistles, like support for playing videos, making shapes and applying
special image effects.
The Sprite Kit API is well-designed and easy to use, especially for beginners. Best of
all, you can use it knowing that it’s fully supported by Apple and heavily optimized
to make 2D games on iOS.
From here on out, if you want to make a 2D game on iOS, tvOS, or MacOS X, we
definitely recommend you use Sprite Kit rather than other game frameworks.
There’s one big exception: if you want to make a cross platform game (i.e. for
Android, Windows, etc). Sprite Kit is an Apple-only API so it will be more
challenging to port your game from Sprite Kit to other platforms than using other
options such as Unity.
If you just want to make something simple for Apple platforms only, Sprite Kit is
the way to go. So let’s get you up to speed with Sprite Kit!
raywenderlich.com 20
2D iOS & tvOS Games by Tutorials Introduction
• A Mac running OS X Mountain Lion or later. This is so you can install the
latest version of the required development tool: Xcode.
• Xcode 7.1 or later. Xcode is the main development tool for iOS. You need to
use Xcode 7.1 or later in this book, because Xcode 7.1 is the first version of
Xcode that supports tvOS development. You can download the latest version of
Xcode for free from the Apple developer site here: https://developer.apple.com/
xcode/download/
• A new Apple TV [optional]: You do not need an new Apple TV since you can
work with the Apple TV simulator, but it's deinitely handy to test with a physical
remote - plus awesome to see the games on the big screen!
If you don’t have the latest version of Xcode installed, be sure to do that before
continuing with the book.
This book does require some basic knowledge of Swift. If you do not know Swift,
you can still follow along with the book because all of the instructions are in step-
by-step format. However, there will likely be parts that are confusing due to gaps in
your knowledge. Before beginning this book, you might want to go through our
Swift Apprentice series, which covers the basics of Swift development:
• www.raywenderlich.com/store
raywenderlich.com 21
2D iOS & tvOS Games by Tutorials Introduction
Our suggestion is to skim through the early chapters and focus more on the later,
more advanced chapters, or where you have a particular interest.
Don’t worry – you can jump right into any chapter in the book, because we’ll always
have a starter project waiting for you!
raywenderlich.com 22
2D iOS & tvOS Games by Tutorials Introduction
Throughout this section you will create an action game called Zombie Conga, where
you take the role of a happy-go-lucky zombie who just wants to party!
1. Chapter 1, Sprites: Get started by adding your first sprites to the game: the
background and the zombie.
2. Chapter 2, Manual Movement: You’ll make the zombie follow your touches
around the screen and get a crash-course in basic 2D vector math.
3. Chapter 3, Actions: You’ll add cats and crazy cat ladies to the game, as well
as basic collision detection and gameplay.
4. Chapter 4, Scenes: You’ll add a main menu to the game, as well as win and
lose scenes.
5. Chapter 5, Camera: You’ll make the game scroll from left to right, and finally,
add the conga line itself.
6. Chapter 6, Labels: You'll add a label to show the zombie's lives and the
number of cats in his conga line.
raywenderlich.com 23
2D iOS & tvOS Games by Tutorials Introduction
7. Chapter 7, Beginning tvOS: You'll get Zombie Conga working on tvOS, in just
a few simple steps!
In the process, you will create a physics puzzle game called Cat Nap, where you
take the role of a cat who has had a long day and just wants to go to bed.
8. Chapter 8, Scene Editor: You’ll begin by creating the first level of the game.
By the end, you'll have a better understanding of Xcode's level designer, better
known as the scene editor.
10. Chapter 10, Intermediate Physics: You’ll learn about physics-based collision
detection and create custom classes for your Sprite Kit nodes.
raywenderlich.com 24
2D iOS & tvOS Games by Tutorials Introduction
11. Chapter 11, Advanced Physics: You’ll add two more levels to the game as
you learn about interactive bodies, joints between bodies, composed bodies and
more.
12. Chapter 12, Crop, Video and Shape Nodes: You’ll add special new blocks to
Cat Nap while learning about additional types of nodes that allow you to do
amazing things—like play videos, crop images and create dynamic shapes.
13. Chapter 13, Intermediate tvOS: In this last chapter you are going to bring
Cat Nap to the silver screen. You are going to take the fully developed game
and add support for tvOS so the player can relax on their couch and play the
game using only the remote.
raywenderlich.com 25
2D iOS & tvOS Games by Tutorials Introduction
In the process, you will create a game called Drop Charge, where you're a space
hero with a mission to blow up an alien space ship - and escape with your life
before it explodes. To do this, you must jump from platform to platform, collecting
special boosts along the way. Just be careful not to fall into the red hot lava!
14. Chapter 14, Making Drop Charge: You'll put together the basic gameplay
using the scene editor and code, flexing the Sprite Kit muscles you've developed
working through previous chapters.
15. Chapter 15, State Machines: You'll learn what state machines are and how to
use them.
16. Chapter 16, Particle Systems: You'll learn how to use particle systems to
create amazing special effects.
17. Chapter 17, Juice Up Your Game: You'll trick out your game with music,
sound, animation, more particles and other special effects, experiencing for
yourself the benefits of mastering the details.
raywenderlich.com 26
2D iOS & tvOS Games by Tutorials Introduction
In the proces, you'll create a fun tower defense game called Dino Defense where
you construct a perfect defense to save your village from an onslaught of angry
dinosaurs!
18. Chapter 18, Entity-Component System: You'll learn all about modeling your
game's objects using the new GKEntity and GKComponent objects provided with
GameplayKit, and you'll use what you've learned to implement your first
dinosaur and tower.
20. Chapter 20, Agents, Goals and Behaviors: Finally, you'll add a second
dinosaur to your game that will use a GKAgent with GKGoal and GKBehavior
objects to move across the scene as a more organic alternative to pathfinding.
In the proces, you'll create a tile-based dungeon crawler called Delve where you try
to guide your miner through a rock-elemental infested dungeon.
raywenderlich.com 27
2D iOS & tvOS Games by Tutorials Introduction
21. Chapter 21, Tile Map Games: You'll learn techniques for building tile map
levels, including how to create a fully functional tile map game.
22. Chapter 22, Randomization: Take advantage of the new GameplayKit class
GKRandom to generate the game world.
23. Chapter 23, Procedural Levels: Remove some of the random aspects of the
level generation to make the process more predictable, but still an adventure
into the unknown.
24. Chapter 24, Game Controllers: This game is perfect for external game
controllers; you'll be adding a tvOS target and exploring how to use the Apple
TV remote as a game controller.
In these bonus chapters, you’ll learn about some APIs other than Sprite Kit that are
good to know when making games for iOS. In particular, you will learn how add
Game Center leaderboards and achievements into your game, use the new iOS 9
ReplayKit API, and add iAds into your game.
In the process, you will integrate these APIs into a top-down racing game called
Circuit Racer, where you take the role of an elite racecar driver out to set a world
record. It would be no problem if it weren’t for the debris on the track!
raywenderlich.com 28
2D iOS & tvOS Games by Tutorials Introduction
You will work with this game across four chapters, in stages:
25. Chapter 25, Game Center Achievements: Enable Game Center for your
game and award the user achievements for accomplishing certain feats.
26. Chapter 26, Game Center Leaderboards: Set up various leaderboards for
your game and track and report the player’s scores.
27. Chapter 27, ReplayKit: You'll learn how to allow players to record and share
videos of their games with iOS 9's new ReplayKit.
28. Chapter 28, iAd: You'll learn how to integrate iAds into your game so you can
have a nice source of revenue!
We have also included a bonus chapter about making your own game art:
29. Chapter 29, Making Art for Programmers: If you liked the art in these mini-
games and want to learn how to either hire an artist or make some art of your
own, look no further than this chapter! This chapter guides you through drawing
a cute cat in the style of this book with Illustrator.
Book updates
Since you’ve purchased the PDF version of this book, you get free access to any
updates we may make to the book!
The best way to get update notifications is to sign up for our monthly newsletter.
This includes a list of the tutorials that came out on raywenderlich.com that month,
any important news like book updates or new books, and a list of our favorite iOS
development links for that month. You can sign up here:
• www.raywenderlich.com/newsletter
raywenderlich.com 29
2D iOS & tvOS Games by Tutorials Introduction
License
By purchasing 2D iOS & tvOS Games by Tutorials, you acquire the following license:
• You are allowed to use and/or modify the source code provided with 2D iOS &
tvOS Games by Tutorials in as many games as you want, with no attribution
required.
• You are allowed to use and/or modify all art, music and sound effects that are
included with 2D iOS & tvOS Games by Tutorials in as many games as you want,
but must include this attribution line somewhere inside your game: “Artwork/
sounds: from 2D iOS & tvOS Games by Tutorials book, available at http://
www.raywenderlich.com”.
• The source code included in 2D iOS & tvOS Games by Tutorials is for your
personal use only. You are NOT allowed to distribute or sell the source code in 2D
iOS & tvOS Games by Tutorials without prior authorization.
• This book is for your personal use only. You are NOT allowed to sell this book
without prior authorization, or distribute it to friends, co-workers or students –
they would need to purchase their own copy.
All materials provided with this book are provided on an “as-is” basis, without
warranty of any kind, express or implied, including but not limited to the warranties
of merchantability, fitness for a particular purpose and non-infringement. In no
event shall the authors or copyright holders be liable for any claim, damages or
other liability, whether in an action of contract, tort or otherwise, arising from, out
of or in connection with the software or the use or other dealings in the software.
All trademarks and registered trademarks appearing in this guide are the property
of their respective owners.
Acknowledgements
We would like to thank many people for their assistance in making this book
possible:
• Our families: For bearing with us during this hectic time as we worked all hours
of the night to get this book ready for publication!
• Ricardo Quesada: Ricardo is the lead developer of Cocos2D, which got many of
us into making games. Sprite Kit seems to draw quite a bit of inspiration from
raywenderlich.com 30
2D iOS & tvOS Games by Tutorials Introduction
raywenderlich.com 31
Section I: Getting Started
This section covers the basics of making 2D games with Sprite Kit. These are the
most important techniques, the ones you’ll use in almost every game you make. By
the time you reach the end of this section, you’ll be ready to make your own simple
game.
Throughout this section you will create an action game called Zombie Conga, where
you take the role of a happy-go-lucky zombie who just wants to party!
Chapter 1: Sprites
Chapter 2: Manual Movement
Chapter 3: Actions
Chapter 4: Scenes
Chapter 5: Camera
Chapter 6: Labels
Chapter 7: Beginning tvOS
raywenderlich.com 32
1 Chapter 1: Sprites
By Ray Wenderlich
Now that you know what Sprite Kit is and why you should use it, it’s time to try it
out for yourself!
The first minigame you will build in this book is called Zombie Conga. Here’s what it
will look like when you’re finished:
In Zombie Conga, you take the role of a happy-go-lucky zombie who wants to
party!
Luckily, the beach town you occupy has an overly abundant cat population. You
raywenderlich.com 33
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
simply need to bite them and they’ll join your zombie conga line.
But watch out for crazy cat ladies! These wizened warriors in red dresses won’t take
kindly to anyone stealing their beloved cats and will do their best to make the
zombie rest in peace—permanently.
You will build this game across the next seven chapters, in stages:
1. Chapter 1, Sprites: You are here! Get started by adding your first sprites to
the game: the background and the zombie.
2. Chapter 2, Manual Movement: You’ll make the zombie follow your touches
around the screen and get a crash-course in basic 2D vector math.
3. Chapter 3, Actions: You’ll add cats and crazy cat ladies to the game, as well
as basic collision detection and gameplay.
4. Chapter 4, Scenes: You’ll add a main menu to the game, as well as win and
lose scenes.
5. Chapter 5, Camera: You’ll make the game scroll from left to right, and finally,
add the conga line itself.
6. Chapter 6, Labels: You'll add a label to show the zombie's lives and the
number of cats in his conga line.
7. Chapter 7, Beginning tvOS: You'll get Zombie Conga working on tvOS, in just
a few simple steps!
Getting started
Start Xcode and select File\New\Project... from the main menu. Select the iOS
\Application\Game template and click Next.
raywenderlich.com 34
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Enter ZombieConga for the Product Name, choose Swift for Language, SpriteKit
for Game Technology, Universal for Devices and click Next.
Select somewhere on your hard drive to save your project and click Create. At this
point, Xcode will generate a simple Sprite Kit starter project for you.
Take a look at what Sprite Kit made. In Xcode’s toolbar, select the iPhone 6 and
raywenderlich.com 35
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
click Play.
After a brief splash screen, you’ll see a single label that says, “Hello, World!” When
you click on the screen, a rotating space ship will appear.
In Sprite Kit, a single object called a scene controls each “screen” of your app. A
scene is a subclass of Sprite Kit’s SKScene class.
Right now this app just has a single scene, GameScene. Open GameScene.swift and
you’ll see the code that displays the label and the rotating space ship. It’s not
important to understand this code quite yet—you’re going to remove it all and build
your game one step at a time.
raywenderlich.com 36
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
For now, delete everything in GameScene.swift and replace it with the following:
import SpriteKit
didMoveToView() is the method that Sprite Kit calls before it presents your scene in
a view; it’s a good place to do some initial setup of your scene’s contents. Here, you
simply set the background color to black.
Zombie Conga is designed to run in landscape mode, so let’s configure the app for
this. Select the ZombieConga project in the project navigator and then select the
ZombieConga target. Go to the General tab and make sure only Landscape Left
and Landscape Right are checked:
You also need to modify this in one more spot. Open Info.plist and find the
Supported interface orientations (iPad) entry. Delete the entries for Portrait
(bottom home button) and Portrait (top home button) that you see there, so
only the landscape options remain.
raywenderlich.com 37
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
The Sprite Kit template automatically creates a file named GameScene.sks. You
can edit this file with Xcode’s built-in scene editor to lay out your game scene
visually. Think of the scene editor as a simple Interface Builder for Sprite Kit.
You’ll learn all about the scene editor in Chapter 7, “Scene Editor”, but you won’t be
using it for Zombie Conga, as it will be easier and more instructive to create the
sprites programmatically instead.
So, control-click GameScene.sks, select Delete and then select Move to Trash.
Since you’re no longer using this file, you’ll have to modify the template code
appropriately.
import UIKit
import SpriteKit
raywenderlich.com 38
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Previously, the view controller loaded the scene from GameScene.sks, but now it
creates the scene by calling an initializer on GameScene instead.
Notice that when you create the scene, you pass in a hardcoded size of
2048x1536 and set the scale mode to AspectFill. This is a good time for a quick
discussion about how this game is designed to work as a universal app.
Note: This section is optional and for those who are especially curious. If
you’re eager to get coding as soon as possible, feel free to skip to the next
section, “Adding the art”.
We’ve designed all the games in this book as universal apps, which means they will
work on the iPhone and the iPad.
The scenes for the games in this book have been designed with a base size of
2048x1536, or reversed for portrait orientation, with the scale mode set to aspect
fill. Aspect fill instructs Sprite Kit to scale the scene’s content to fill the entire
screen, even if Sprite Kit needs to cut off some of the content to do so.
This results in your scene appearing as-is on the iPad Retina, which has a resolution
of 2048x1536, but as scaled/cropped on the iPhone to fit the phone’s smaller size
and different aspect ratio.
Here are a few examples of how the games in this book will look in landscape
orientation on different devices, moving from smallest to largest aspect ratio:
raywenderlich.com 39
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
• iPad Retina [4:3 or 1.33]: Displayed as-is to fit the 2048x1536 screen size.
• iPad Non-Retina [4:3 or 1.33]: Aspect fill will scale a 2048x1536 visible area
by 0.5 to fit the 1024x768 screen.
• iPhone 4S [3:2 or 1.5]: Aspect fill will scale a 2048x1366 visible area by 0.47
to fit the 960x640 screen.
• iPhone 5 [16:9 or 1.77]: Aspect fill will scale a 2048x1152 visible area by 0.56
to fit the 1136x640 screen.
• iPhone 6 [16:9 or 1.77]: Aspect fill will scale a 2048x1152 visible area by 0.64
to fit the 1334x750 screen.
• iPhone 6 Plus [16:9 or 1.77]: Aspect fill will scale a 2048x1152 visible area by
0.93 to fit the 1920x1080 screen.
Since aspect fill will crop the scene on the top and bottom for iPhones, we’ve
designed the games in this book to have a main “playable area” that is guaranteed
to be visible on all devices. Basically, the games will have a 192-pixel margin on the
top/bottom in landscape and the left/right in portrait, in which you should avoid
putting essential content. We’ll show you how to visualize this later in the book.
Note that you need only one set of art for this to work: the art to fit the maximum
screen size, 2048x1536. The art will be downscaled on devices other than the iPad
Retina.
raywenderlich.com 40
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Note: The con of this approach is that the art will be bigger than necessary for
some devices, such as the iPhone 4s, thereby wasting texture memory and
space. The pro of this approach is that the game stays nice and simple and
works well on all devices.
An alternate approach would be to add different images for each device and
scale factor (i.e. iPad @1x, iPad @2x, iPhone@2x, iPhone @3x), leveraging the
power of Apple's asset catalogs. However, at the time of writing this chapter,
Sprite Kit does not properly load the correct image from the asset catalog
based on device and scale factor in all cases, so we will stay with this simple
route for now.
In Xcode, open Assets.xcassets, select the Spaceship entry and press your
delete key to remove it— unfortunately, this is not a game about space zombies!
At this point, only AppIcon will remain:
raywenderlich.com 41
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Then drag all of the files from starter\resources\images into the left sidebar:
By including your images in the asset catalog, behind the scenes Xcode will build
texture atlases containing these images and use them in your game, which will
automatically increase performance.
raywenderlich.com 42
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Launch screen
There's one last thing you should do to get this game started on the right foot:
configure the launch screen.
The launch screen is what iOS displays when your app is first loading, which usually
takes a few seconds. A launch image gives the player the impression that your app
is starting quickly—the default black screen, needless to say, does not. For Zombie
Conga, you’ll show a splash screen with the name of the game.
Your app actually has a launch screen already. When you launched your app earlier,
you may have noticed a brief, blank white screen. That was it!
In iOS, apps have a special launch screen file; this is basically a storyboard,
LaunchScreen.storyboard in this project, that you can configure to present
something onscreen while your app is loading. The advantage of this over the old
method of just displaying an image is that you can use Auto Layout to have much
finer control of how this screen looks on different devices.
Let’s try this out. Open LaunchScreen.storyboard. You'll see the following:
In the Object Library on the right sidebar, drag an image view into the view and
resize it to fill the entire area:
raywenderlich.com 43
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Next, you need to set the image view so that it always has the same width and
height as its containing view. To do this, make sure the image view is selected and
then click the Pin button in the lower right—it looks like a tie fighter. In the Add
New Constraints screen, click the four light-red lines so that the image view is
pinned to each edge. Make sure that Constrain to margins isn't checked and that
all values are set to 0, then click Add 4 Constraints:
With the image view still selected, make sure the Attributes Inspector is selected—
raywenderlich.com 44
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
it's the fourth tab on the right. Set the Image to MainMenu and set the View
Mode to Aspect Fill:
Build and run your app again. This time, you'll see a brief Zombie Conga splash
screen:
This may not look like much, but you now have a starting point upon which to build
your first Sprite Kit game.
raywenderlich.com 45
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Let’s move on to the next task, which also happens to be one of the most important
and common when making games: displaying an image on the screen.
Displaying a sprite
When making a 2D game, you usually put images on the screen representing your
game’s various elements: the hero, enemies, bullets and so on. Each of these
images is called a sprite.
Sprite Kit has a special class called SKSpriteNode that makes it easy to create and
work with sprites. This is what you’ll use to add all your sprites to the game. Let’s
give it a try.
Creating a sprite
Open GameScene.swift and add this line to didMoveToView(), right after you set
the background color:
You don’t need to pass the image’s extension, as Sprite Kit will automatically
determine that for you.
Build and run, ignoring the warning for now. Ah, you thought it was simple, but at
raywenderlich.com 46
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
To do this, add this line of code right after the previous line:
addChild(background)
You’ll learn about nodes and scenes later. For now, build and run again, and you’ll
see part of the background appear in the bottom left of the screen:
Obviously, that’s not quite what you want. To get the background in the correct
spot, you have to set its position.
Positioning a sprite
By default, Sprite Kit positions sprites at (0, 0), which in Sprite Kit represents the
bottom left. Note that this is different from the UIKit coordinate system in iOS,
where (0, 0) represents the top left.
Try positioning the background somewhere else by setting the position property.
Add this line of code right before calling addChild(background):
Here you set the background to the center of the screen. Even though this is a
single line of code, there are four important things to understand:
1. The type of the position property is CGPoint, which is a simple structure that has
x and y components:
struct CGPoint {
var x: CGFloat
var y: CGFloat
}
raywenderlich.com 47
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
2. You can easily create a new CGPoint with the initializer shown above.
3. Since you’re writing this code in an SKScene subclass, you can access the size of
the scene at any time with the size property. The size property’s type is CGSize,
which is a simple structure like CGPoint that has width and height components.
struct CGSize {
var width: CGFloat
var height: CGFloat
}
4. A sprite’s position is within the coordinate space of its parent node, which in this
case is the scene itself. You’ll learn more about this in Chapter 5, “Camera”.
Note: You may notice you can’t see the entire background on iPhone devices
—parts of it overlap on the top and bottom. This is by design, so the game
works on both the iPad and the iPhone, as discussed in the "Universal app
support" section earlier in this chapter.
This explains why you could only see the upper half of the sprite earlier. Before you
set the position, it defaulted to (0, 0), which placed the center of the sprite in the
lower-left corner of the screen, so you could only see the top half.
You can change this behavior by setting a sprite’s anchor point. Think of the anchor
point as “the spot within a sprite that you pin to a particular position”. Here's an
illustration showing a sprite positioned at the center of the screen, but with
different anchor points:
raywenderlich.com 48
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
To see how this works, find the line that sets the background’s position to the
center of the screen and replace it with the following:
background.anchorPoint = CGPoint.zero
background.position = CGPoint.zero
CGPoint.zero is a handy shortcut for (0, 0). Here, you set the anchor point of the
sprite to (0, 0) to pin the lower-left corner of the sprite to whatever position you set
—in this case, also (0, 0).
Build and run, and the image is still in the right spot:
This works because now you're pinning the lower-left corner of the background
image to the lower-left corner of the scene.
Here you changed the anchor point of the background for learning purposes.
However, usually you can leave the anchor point at its default of (0.5, 0.5), unless
you have a specific need to rotate the sprite around a particular point—an example
of which is described in the next section.
So, in short: when you set the position of a sprite, by default you are positioning
the center of the sprite.
raywenderlich.com 49
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Rotating a sprite
To rotate a sprite, you simply set its zRotation property. Try it out on the
background sprite by adding this line right before the call to addChild():
background.zRotation = CGFloat(M_PI) / 8
Rotation values are in radians, which are units used to measure angles. This
example rotates the sprite π / 8 radians, which is equal to 22.5 degrees. Also notice
that you convert M_PI, which is a Double, into a CGFloat. You do this because
zRotation requires a CGFloat and Swift doesn’t automatically convert between types
like some other languages do.
Note: I don’t know about you, but I find it easier to think about rotations in
degrees rather than in radians. Later in the book, you’ll create helper routines
to convert between degrees and radians.
Build and run, and check out your rotated background sprite:
This demonstrates an important point: Sprites are rotated about their anchor
points. Since you set this sprite’s anchor point to (0, 0), it rotates around its
bottom-left corner.
Note: Remember that on the iPhone, the bottom-left of this image is actually
offscreen! If you’re not sure why this is, refer back to the "Universal app
support" section earlier in this chapter.
Try rotating the sprite around the center instead. Replace the lines that set the
position and anchor point with these:
Build and run, and this time the background sprite will have rotated about the
raywenderlich.com 50
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
center:
This is all good to know! But for Zombie Conga, you don’t want a rotated
background, so comment out that line:
// background.zRotation = CGFloat(M_PI) / 8
If you’re wondering when you might want to change the anchor point in a game,
imagine you’re creating a character’s body out of different sprites—one each for the
head, torso, left arm, right arm, left leg and right leg:
If you wanted to rotate these body parts at their joints, you’d have to modify the
anchor point for each sprite, as shown in the diagram above.
But again, usually you should leave the anchor point at default unless you have a
specific need, like the one shown here.
raywenderlich.com 51
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
sprite’s size defaults to the size of the image. In Sprite Kit, the class representing
this image is called a texture.
Add these lines after the call to addChild() to get the size of the background and
log it to the console:
Build and run, and in your console output, you'll see something like this:
Sometimes it’s useful to get the size of a sprite programmatically, as you do above,
instead of hard-coding numbers. Your code will be much more robust and adaptable
for it.
Everything that appears on the screen in Sprite Kit derives from a class called
SKNode. Both the scene class (SKScene) and the sprite class (SKSpriteNode) derive
from SKNode.
SKSpriteNode inherits a lot of its capabilities from SKNode. It turns out that the
position and rotation properties are derived from SKNode rather than being particular
to SKSpriteNode. This means that, just as you can set the position or rotation of a
sprite, you can do the same thing with the scene itself or with anything else that
raywenderlich.com 52
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
You can think of everything that appears on the screen together as a graph of
nodes, often referred to as a scene graph. Here’s an example of what such a
graph might look like for Zombie Conga if there were one zombie, two cats and one
crazy cat lady in the game:
You’ll learn more about nodes and the neat things you can do with them in Chapter
5, "Camera”. For now, you’ll add your sprites as direct children of the scene.
skView.ignoresSiblingOrder = true
• If ignoresSiblingOrder is false, Sprite Kit will draw each node’s children with
the same zPosition in the order in which they were added to their parent.
In general, it’s good to set this property to true, because it allows Sprite Kit to
perform optimizations under the hood to make your game run faster.
However, setting this property to true can cause problems if you’re not careful. For
example, if you were to add a zombie to this scene at the same zPosition as the
background—which would happen if you left them at the default position of 0—
Sprite Kit might draw the background on top of the zombie, covering the zombie
from the player’s view. And if zombies are scary, just imagine invisible ones!
To avoid this, you’ll set the background’s zPosition to -1. This way, Sprite Kit will
draw it before anything else you add to the scene, which will default to a zPosition
of 0.
raywenderlich.com 53
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
background.zPosition = -1
Finishing touches
That’s it for this chapter! As you can see, adding a sprite to a scene takes only
three or four lines of code:
Now it’s time for you to test your newfound knowledge by adding the zombie to the
scene.
Challenges
It’s important for you to practice what you’ve learned, on your own, so each
chapter in this book has one to three challenges, progressing from easy to hard.
I highly recommend giving all the challenges a try, because while following a step-
by-step tutorial is educational, you’ll learn a lot more by solving a problem by
yourself. In addition, each chapter will continue where the previous chapter’s
challenges left off, so you'll want to stay in the loop!
If you get stuck, you can find solutions in the resources for this chapter—but to get
the most from this book, give these your best shot before you look!
If you’ve got it right, you'll see the zombie appear onscreen like so:
raywenderlich.com 54
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
Run your game on the iPad Air 2 simulator to prove it works there, as well—just
with a bigger viewable area!
However, it’s good to know where to find more information in case you ever have
questions or get stuck. I highly recommend you check out Apple’s SKNode Class
Reference and SKSpriteNode Class Reference, as these cover the two classes you’ll
use most often in Sprite Kit, and it’s good to have a basic familiarity with the
properties and methods they contain.
You can find the references in Xcode by selecting Help\Documentation and API
Reference from the main menu and searching for SKNode or SKSpriteNode.
raywenderlich.com 55
2D iOS & tvOS Games by Tutorials Chapter 1: Sprites
And now for your second challenge: Use the information in these docs to double
(scale to 2x) the zombie’s size. Answer this question: Did you use a method of
SKSpriteNode or SKNode to do this?
raywenderlich.com 56
2 Chapter 2: Manual Movement
By Ray Wenderlich
If you completed the challenges from the previous chapter, you now have a rather
large zombie on the screen:
Note: If you were unable to complete the challenges or skipped ahead from
the previous chapter, don’t worry—simply open the starter project from this
chapter to pick up where the previous chapter left off.
Of course, you want the sprite to move around, not just stand there—this zombie’s
got an itch to boogie!
1. As you might have noticed in the previous chapter—if you looked at the
template code provided by Apple—you can make a sprite move using a concept
called actions. You’ll learn more about actions in the next chapter.
2. You can make a sprite move in the more “classic” way—and that’s to set the
position manually over time. It’s important to learn this way first, because it
affords the most control and will help you understand what actions do for you.
raywenderlich.com 57
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
However, to set a sprite’s position over time, you need a method that the game
calls periodically as it runs. This introduces a new topic: the Sprite Kit game loop.
Each individual picture that you draw is called a frame. Games typically try to draw
frames between 30 to 60 times per second so that the animations feel smooth. This
rate of drawing is called the frame rate, or specifically frames per second (FPS).
By default, Sprite Kit displays this in the bottom-right corner of your game:
Note: It’s handy of Sprite Kit to show your frames per second onscreen by
default, because you want to keep an eye on the FPS as you develop your
game to make sure your game is performing well. Ideally, you want at least 30
FPS.
You should only pay attention to the FPS display on an actual device, though,
raywenderlich.com 58
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
Behind the scenes, Sprite Kit runs an endless loop, often referred to as the game
loop, which looks like this:
This illustrates that each frame, Sprite Kit does the following:
1. Calls a method on your scene called update(). This is where you can put
code that you want to run every frame—making it the perfect spot for code that
updates the position or rotation of your sprites.
2. Does some other stuff. You’ll revisit the game loop in other chapters, filling in
your understanding of the rest of this diagram as you go.
3. Renders the scene. Sprite Kit then draws all of the objects that are in your
scene graph, issuing OpenGL draw commands for you behind the scenes.
raywenderlich.com 59
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
1. Keep update() fast. For example, you want to avoid slow algorithms in this
method since it’s called each frame.
2. Keep your node count as low as possible. For example, it’s good to remove
nodes from the scene graph when they’re offscreen and you no longer need
them.
Now you know that update() is called each frame and is a good spot to update the
positions of your sprites—so let’s make this zombie move!
To start, you’ll implement a simple but not ideal method: moving the zombie a fixed
amount per frame.
Before you begin, open GameScene.swift and comment out the line in
didMoveToView() that sets the zombie to double its size:
This line was just a test, so you don’t need it anymore. Zombies scare me enough
in normal size! :]
Here, you update the position of the zombie to be eight more points along the x-
axis than last time, and keep the same position along the y-axis. This makes the
zombie move from left to right.
Build and run, and you’ll see the zombie move across the screen:
raywenderlich.com 60
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
This is great stuff, but the movement feels a bit jagged or irregular. To see why,
let’s go back to the Sprite Kit game loop.
Remember, Sprite Kit tries to draw frames as quickly as possible. However, there
will usually be some variance in the amount of time it takes to draw each frame:
sometimes a bit slower, sometimes a bit quicker.
This means the amount of time between calls to your update() loop can vary. To see
this yourself, add some code to print out the time elapsed since the last update.
Add these variables to GameScene’s property section, right after the zombie property:
Here, you create properties to keep track of the last time Sprite Kit called update(),
and the delta time since the last update, often abbreviated as dt.
if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
} else {
dt = 0
}
lastUpdateTime = currentTime
print("\(dt*1000) milliseconds since last update")
raywenderlich.com 61
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
Here, you calculate the time since the last call to update() and store that in dt, then
log out the time in milliseconds (1 second = 1000 milliseconds).
Build and run, and you’ll see something like this in the console:
As you can see, the amount of time between calls to update() always varies slightly.
Note: Sprite Kit tries to call your update method 60 times a second (every
~16 milliseconds). However, if it takes too long to update and render a frame
of your game, Sprite Kit may call your update method less frequently, and the
FPS will drop. You can see that here—some frames are taking over 30
milliseconds.
You're seeing such a low FPS because you're running on the simulator. As
mentioned earlier, you can’t count on the simulator for accurate performance
measurements. If you try running this code on a device, you should see a
much higher FPS.
Note that even if your game runs at a smooth 60 FPS, there will always be
some small variance in how often Sprite Kit calls your update method.
Therefore, you need to take the delta time into account in your calculations—
and you'll learn how to do that next!
Since you’re updating the position of the zombie a fixed amount per frame rather
than taking this time variance into consideration, you’re likely to wind up with
movement that looks jagged or irregular.
raywenderlich.com 62
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
The correct solution is to figure out how far you want the zombie to move per
second and then multiply this by the fraction of a second since the last update. Let’s
give it a shot.
You’re saying that in one second, the zombie should move 480 points, about 1/4 of
the scene width. You set the type to CGFloat, because you’ll be using this value in
calculations with other CGFloats inside a CGPoint.
So far, you’ve used CGPoints to represent positions. However, it’s also quite
common and handy to use CGPoints to represent 2D vectors.
raywenderlich.com 63
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
The diagram above shows an example of a 2D vector you might use to represent
the zombie’s movement. You can see that the orientation of the arrow shows the
direction in which the zombie should move, while the arrow’s length indicates
how far the zombie should move in a second. The direction and length together
represent the zombie’s velocity–you can think of it as how far and in what
direction the zombie should move in 1 second.
However, note that the velocity has no set position. After all, you should be able to
make the zombie move in that direction, at that speed, no matter where the zombie
starts.
You’ve refactored the code into a reusable method that takes the sprite to be
moved and a velocity vector by which to move it. Let’s go over this line by line:
1. Velocity is in points per second, and you need to figure out how many points to
move the zombie this frame. To determine that, this section multiplies the
points per second by the fraction of seconds since the last update. You now
have a point representing the zombie’s position—which you can also think of as
a vector from the origin to the zombie’s position—as well as a vector
representing the distance and direction to move the zombie this frame:
raywenderlich.com 64
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
2. To determine the zombie’s new position, simply add the vector to the point:
You can visualize this with the diagram above, but in code you simply add the x-
and y-components of the point and the vector together.
Note: To learn more about vectors, check out this great guide: http://
www.mathsisfun.com/algebra/.
Finally, inside update(), replace the line that sets the zombie’s position with the
following:
moveSprite(zombie,
velocity: CGPoint(x: zombieMovePointsPerSec, y: 0))
Build and run, and now the zombie moves much more smoothly across the screen.
raywenderlich.com 65
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
Look at the console log, and you’ll also see that the zombie is now moving a
different number of points each frame, based on how much time has elapsed.
If your zombie’s movement still looks jittery, be sure to try it on a device instead of
on the simulator, which has different performance characteristics.
Your goal is for the zombie to move toward the point the player taps and keep
moving even after passing the tap location, until the player taps another location to
draw his attention. There are four steps to make this work—let’s cover them one at
a time.
Subtracting points and vectors is similar to adding them, but instead of adding the
x- and y- components, you—that’s right—subtract them! :]
raywenderlich.com 66
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
This diagram illustrates that if you subtract the zombie position from the tap
position, you get a vector showing the offset amount. You can see this even more
clearly if you move the offset vector so it begins from the zombie’s position:
By subtracting these two positions, you get something with a direction and a
length. Call this the offset vector.
You’re not done writing this method; this is only the beginning!
Think of the offset vector as the hypotenuse of a right triangle, where the lengths of
the other two sides of the triangle are defined by the x- and y- components of the
vector:
raywenderlich.com 67
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
You want to find the length of the hypotenuse. To do this, you can use the
Pythagorean theorem. You may remember this simple formula from geometry—it
says that the length of the hypotenuse is equal to the square root of the sum of the
squares of the two sides.
Put this theory into practice. Add the following line to the bottom of
moveZombieToward():
• The length is the length of the line between the zombie’s current position and
the tap location.
So you’re halfway there—your vector points in the right direction, but isn’t the right
length. How do you make a vector pointing in the same direction as the offset
vector, but of a certain length?
The first step is to convert the offset vector into a unit vector, which means a
vector of length 1. According to geometry, you can do this by simply dividing the
offset vector’s x- and y- components by the offset vector’s length.
raywenderlich.com 68
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
Once you have this unit vector, which you know is of length 1, it’s easy to multiply
it by zombieMovePointsPerSec to make it the exact length you want.
Now you’ve got a velocity vector with the correct direction and length. There’s only
one step left!
raywenderlich.com 69
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
To see this in action, implement these touch handling methods for GameScene, as
follows:
func sceneTouched(touchLocation:CGPoint) {
moveZombieToward(touchLocation)
}
Finally, inside update(), edit the call to moveSprite() so it passes in velocity (based
on the touch) instead of the preset amount:
That’s it! Build and run, and now the zombie will chase your taps. Just don’t get too
close—he’s hungry!
Note: You can also use gesture recognizers with Sprite Kit. These can be
especially handy if you’re trying to implement complicated gestures, such as
pinching or rotating.
You can add the gesture recognizer to the scene's view in didMoveToView(),
and you can use SKScene’s convertPointFromView() and SKNode’s
convertPoint(toNode:) methods to get the touch in the coordinate space you
raywenderlich.com 70
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
need.
For a demonstration of this, see the sample code for this chapter, where I’ve
included a commented-out demonstration of gesture recognizers for you. Since
it does the same thing as the touch handlers you implemented, comment out
your touch handlers when you run with the gesture recognizers if you want to
be sure the gestures are working.
To do this, you need to check if the newly calculated position is beyond any of the
screen edges and make the zombie bounce away, if so. Add this new method:
func boundsCheckZombie() {
let bottomLeft = CGPointZero
let topRight = CGPoint(x: size.width, y: size.height)
First, you make constants for the bottom-left and top-right coordinates of the
scene.
Then, you check the zombie’s position to see if it’s beyond or on any of the screen
edges. If it is, you clamp the position and reverse the appropriate velocity
component to make the zombie bounce in the opposite direction.
boundsCheckZombie()
Build and run, and you have a zombie bouncing around the screen. I told you he
raywenderlich.com 71
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
Run the game on the iPad simulator, and you’ll see the game works as expected.
Does this give you a clue as to what’s going on?
Recall from the "Universal app support" section in Chapter 1 that Zombie Conga has
been designed with a 4:3 aspect ratio (2048x1536). However, you want to support
up to a 16:9 aspect ratio (1136x640), which is what the iPhone 5, 6, and 6 Plus
use.
Let’s take a look at what happens with a 16:9 device. Since you’ve configured the
scene to use aspect fill, Sprite Kit first calculates the largest 16:9 rectangle that fits
within the 2048x1536 space: that's 2048x1152. It then centers that rectangle and
scales it to fit the actual screen size; for example, the iPhone 6's 1134x750 screen
requires scaling by 0.64.
raywenderlich.com 72
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
This means that on 16:9 devices, there are 192-point gaps at the top and bottom of
the scene that won’t be visible (1536 - 1152 = 384. 384 / 2 = 192). Hence, you
should avoid critical gameplay in those areas—such as letting the zombie move in
those gaps.
Let’s solve this problem. First, add a new property to GameScene to store the
playable rectangle:
raywenderlich.com 73
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
1. Zombie Conga supports aspect ratios from 3:2 (1.33) to 16:9 (1.77). Here you
make a constant for the max aspect ratio supported: 16:9 (1.77).
2. With aspect fit, regardless of aspect ratio, the playable width will always be
equal to the scene width. To calculate the playable height, you divide the scene
width by the max aspect ratio.
3. You want to center the playable rectangle on the screen, so you determine the
margin on the top and bottom by subtracting the playable height from the scene
height and dividing the result by 2.
4. You put it all together to make a centered rectangle on the screen, with the max
aspect ratio.
6. Whenever you override the default initializer of a Sprite Kit node, you must also
override the required NSCoder initializer, which is used when you're loading a
scene from the scene editor. Since you're not using the scene editor in this
game, you simply add a placeholder implementation that logs an error.
To visualize this, add a helper method to draw this playable rectangle to the screen:
func debugDrawPlayableArea() {
let shape = SKShapeNode()
let path = CGPathCreateMutable()
CGPathAddRect(path, nil, playableRect)
shape.path = path
shape.strokeColor = SKColor.redColor()
shape.lineWidth = 4.0
addChild(shape)
}
For the moment, don’t worry about how this works; you’ll learn all about
SKShapeNodes in Chapter 11, “Crop, Video and Shape Nodes”. For now, consider this
a black box that draws the debug rectangle to the screen.
debugDrawPlayableArea()
And finally, modify the first two lines in boundsCheckZombie() to take into
consideration the y-values in playableRect:
raywenderlich.com 74
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
Build and run, and you’ll see the zombie now bounces correctly, according to the
playable rectangle, drawn in red and matched to the corners of the screen:
Then build and run on an iPad simulator, and you’ll see the zombie bounces
correctly there, as well, according to the playable rectangle:
The playable area outlined in red is exactly what you see on the iPhone device,
which has the largest supported aspect ratio, 16:9.
Now that you have a playable rectangle, you simply need to make sure the rest of
the gameplay takes place in this box—and your zombie can party everywhere!
raywenderlich.com 75
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
You already have a vector that includes the direction the zombie is facing: velocity.
You just need to find the rotation angle to get the zombie facing in that direction.
Once again, think of the direction vector as the hypotenuse of a right triangle. You
want to find the angle:
You may remember from trigonometry the mnemonic SOH CAH TOA, where the last
part stands for:
Since you have the lengths of the opposite and adjacent sides, you can rewrite the
above formula as follows to get the angle of rotation:
If none of this trigonometry rings any bells, don’t worry. Just think of it as a
formula that you type in to get the angle—that’s all you need to know.
raywenderlich.com 76
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
This uses the equation from above. It includes a bunch of casting because CGFloat
is defined as a Double on 64-bit machines and as a Float on 32-bit machines.
This works because the zombie image faces to the right. If the zombie image were
instead facing the top of the screen, you’d have to add an additional rotation to
compensate, because an angle of 0 points to the right.
Build and run, and the zombie rotates to face the direction in which he’s moving:
Congratulations, you’ve given your zombie life! The sprite moves smoothly, bounces
off the edges of the screen and rotates on both the iPhone and the iPad—a great
start to a game.
But you’re not done yet. It’s time for you to try some of this stuff on your own to
make sure you’ve got it down.
Challenges
This chapter has three challenges, and they’re particularly important ones.
Performing these challenges will give you useful practice with vector math and
introduce new math utilities you’ll use throughout the rest of the book.
As always, if you get stuck, you can find solutions in the resources for this chapter
—but give it your best shot first!
raywenderlich.com 77
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
So far in this chapter, you’ve done all of this yourself inline. That’s a fine way of
doing things, but it can get tedious and repetitive in practice. It’s also error-prone.
Create a new file with the iOS\Source\Swift File template and name it MyUtils.
Then replace the contents of MyUtils.swift with the following:
import Foundation
import CoreGraphics
In Swift, you can make operators like +, -, * and / work on any type you want.
Here, you make them work on CGPoint (sometimes in combination with CGFloat).
[TODO: I want to get rid of the parentheses here, but I'm confused because I don't
see CGFloat anywhere in the above code.]
Now you can add points like the ones below—but don’t add this anywhere; it's just
an example:
raywenderlich.com 78
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
Now you can subtract, multiply or divide a CGPoint by another CGPoint. You can also
multiply and divide points by scalar CGFloat values, as below—again, don’t add this
anywhere; it's just an example:
extension CGPoint {
raywenderlich.com 79
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
The #if/#endif block is true when the app is running on 32-bit architecture. In this
case, CGFloat is the same size as Float, so this code makes versions of atan2 and
sqrt that accept CGFloat/Float values rather than the default of Double, allowing
you to use atan2 and sqrt with CGFloats, regardless of the device’s architecture.
Next, the class extension adds some handy methods to get the length of the point,
return a normalized version of the point (i.e., length 1) and get the angle of the
point.
Using these helper functions will make your code a lot more concise and clean. For
example, look at moveSprite(velocity:):
Simplify the first line by multiplying velocity and dt using the * operator, and avoid
the cast. Also, simplify the final line by adding the sprite’s position and amount to
move using the += operator.
Your challenge is to modify the rest of Zombie Conga to use this new helper code,
and verify that the game still works as expected. When you’re done, you should
have the following calls, including the two mentioned already:
• += operator: 1 call
• - operator: 1 call
• * operator: 2 calls
• normalized: 1 call
• angle: 1 call
You'll also notice when you’re done that your code is a lot cleaner and easier to
raywenderlich.com 80
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
understand. In future chapters, you’ll use a math library we made that’s very
similar to the one you created here.
That’s the behavior you want for Zombie Conga, but in another game, you might
want the zombie to stop where you tap. Your challenge is to modify the game to do
this.
• Inside update(), check the distance between the last touch location and the
zombie’s position. If that remaining distance is less than or equal to the amount
the zombie will move this frame (zombieMovePointsPerSec * dt), then set the
zombie’s position to the last touch location and the velocity to zero. Otherwise,
call moveSprite(velocity:) and rotateSprite(direction:) like normal.
boundsCheckZombie() should always occur.
• To do this, use the - operator once and call length() once using the helper code
from the previous challenge.
To do this, you need two new helper routines. Add these to the bottom of
MyUtils.swift (to type π, use Option-p):
let π = CGFloat(M_PI)
raywenderlich.com 81
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
extension CGFloat {
func sign() -> CGFloat {
return (self >= 0.0) ? 1.0 : -1.0
}
}
sign() returns 1 if the CGFloat is greater than or equal to 0; otherwise it returns -1.
shortestAngleBetween() returns the shortest angle between two angles. It’s not as
simple as subtracting the two angles, for two reasons:
1. Angles “wrap around” after 360 degrees (2 * M_PI). In other words, 30 degrees
and 390 degrees represent the same angle.
2. Sometimes the shortest way to rotate between two angles is to go left, and
other times to go right. For example, if you start at 0 degrees and want to turn
to 270 degrees, it’s shorter to turn -90 degrees than to turn 270 degrees. You
don’t want your zombie turning the long way around—he may be undead, but
he’s not stupid!
So this routine finds the difference between the two angles, chops off any amount
greater than 360 degrees and then decides if it’s faster to go right or left.
raywenderlich.com 82
2D iOS & tvOS Games by Tutorials Chapter 2: Manual Movement
• Use shortestAngleBetween() to find the distance between the current angle and
the target angle. Call this shortest.
• Figure out the amount to rotate this frame based on rotateRadiansPerSec and dt.
Call this amtToRotate.
• If the absolute value of shortest is less than the amtToRotate, use that instead.
• Don’t forget to update the call to rotate the sprite in update() so that it uses the
new parameter.
If you’ve completed all three of these challenges, great work! You really understand
moving and rotating sprites, using the “classic” approach of updating the values
yourself over time.
Ah, but the classic, while essential to understand, always gives way to the modern.
In the next chapter, you’ll learn how Sprite Kit can make some of these common
tasks much easier, through the magic of actions!
raywenderlich.com 83
3 Chapter 3: Actions
By Ray Wenderlich
So far, you’ve learned how to move and rotate Sprite Kit nodes—a node being
anything that appears onscreen—by manually setting their positions and rotations
over time.
This do-it-yourself approach works and is quite powerful, but Sprite Kit provides an
easier way to move sprites incrementally: actions.
Actions allow you to do things like rotate, scale or change a sprite’s position over
time—with only one line of code! You can also chain actions together to create
movement combinations quite easily.
In this chapter, you’ll learn all about Sprite Kit actions as you add enemies,
collectibles and basic gameplay logic to your game.
You’ll see how actions can simplify your game-coding life, and by the time you’ve
finished this chapter, Zombie Conga will be action-packed!
Note: This chapter begins where the previous chapter’s Challenge 3 left off. If
you were unable to complete the challenges or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up where the previous chapter left off.
Move action
Right now, your zombie’s “life” is a bit too carefree. Let’s add action to this game by
introducing enemies to dodge: crazy cat ladies!
raywenderlich.com 84
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Open GameScene.swift and create the start of a new method to spawn an enemy:
func spawnEnemy() {
let enemy = SKSpriteNode(imageNamed: "enemy")
enemy.position = CGPoint(x: size.width + enemy.size.width/2,
y: size.height/2)
addChild(enemy)
}
This code is a review from the previous two chapters: You create a sprite and
position it at the vertical center of the screen, just out of view to the right.
Now you’d like to move the enemy from the right of the screen to the left. If you
were to do this manually, you might update the enemy’s position each frame
according to a velocity.
No need to trouble yourself with that this time! Simply add these two lines of code
to the bottom of spawnEnemy():
To create an action in Sprite Kit, you call one of several static constructors on the
SKAction class, such as the one you see here, moveTo(duration:). This particular
constructor returns an action that moves a sprite to a specified position over a
specified duration (in seconds).
Here, you set up the action to move the enemy along the x-axis at whatever speed
is necessary to take it from its current position to just off the left side of the screen
in two seconds.
Once you’ve created an action, you need to run it. You can run an action on any
raywenderlich.com 85
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Give it a try! For now, call this method inside didMoveToView(), right after calling
addChild(zombie):
spawnEnemy()
Build and run, and you'll see the crazy cat lady race across the screen:
Not bad for only two lines of code, eh? You could have even done it with a single
line of code if you didn’t need to use the actionMove constant for anything else.
Here you saw an example of moveTo(duration:), but there are a few other move
action variants:
raywenderlich.com 86
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
You’ll see this pattern of “[action] to” and “[action] by” variants for other action
types, as well. In general, you can use whichever of these is more convenient for
you—but keep in mind that if either works, the “[action] by” actions are preferable
because they're reversible. For more on this topic, keep reading.
Sequence action
The real power of actions lies in how easily you can chain them together. For
example, say you want the cat lady to move in a V—down toward the bottom of the
screen, then up to the goal position.
To do this, replace the lines that create and run the move action in spawnEnemy()
with the following:
// 1
let actionMidMove = SKAction.moveTo(
CGPoint(x: size.width/2,
y: CGRectGetMinY(playableRect) + enemy.size.height/2),
duration: 1.0)
// 2
let actionMove = SKAction.moveTo(
CGPoint(x: -enemy.size.width/2, y: enemy.position.y),
duration:1.0)
// 3
let sequence = SKAction.sequence([actionMidMove, actionMove])
// 4
enemy.runAction(sequence)
1. Here you create a new move action, just like you did before, except this time it
represents the “mid-point” of the action—the bottom middle of the playable
rectangle.
2. This is the same move action as before, except you’ve decreased the duration to
raywenderlich.com 87
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
1.0, since it will now represent moving only half the distance: from the bottom
of the V, offscreen to the left.
3. Here’s the new sequence action! As you can see, it’s incredibly simple—you use
the sequence: constructor and pass in an Array of actions. The sequence action
will run one action after another.
4. You call runAction() in the same way as before, but pass in the sequence action
this time.
That’s it! Build and run, and you’ll see the crazy cat lady “bounce” off the bottom of
the playable rectangle:
The sequence action is one of the most useful and commonly used actions—
chaining actions together is just so powerful! You’ll use the sequence action many
times in this chapter and throughout the rest of this book.
Wait-for-duration action
The wait-for-duration action does exactly what you’d expect: It makes the sprite
wait for a period of time, during which the sprite does nothing.
“What’s the point of that?” you may be wondering. Well, wait-for-duration actions
only truly become interesting when combined with a sequence action.
For example, let’s make the cat lady briefly pause when she reaches the bottom of
the V-shape. To do this, replace the line in spawnEnemy() that creates a sequence
with the following lines:
raywenderlich.com 88
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Build and run, and now the cat lady will briefly pause at the bottom of the V:
Run-block action
At times, you’ll want to run your own block of code in a sequence of actions. For
example, let's say you want to log a message when the cat lady reaches the bottom
of the V.
To do this, replace the line in spawnEnemy() that creates a sequence with the
following lines:
To create a run-block action, simply call runBlock() and pass in a block of code to
execute.
Build and run, and when the cat lady reaches the bottom of the V, you'll see the
following in the console:
Reached bottom!
Note: If your project still includes the print statements from earlier chapters,
now would be a great time to remove them. Otherwise, you’ll have to search
your console for the above log statement—it’s doubtful you’ll notice it within
the sea of messages scrolling by.
While you’re at it, you should remove any comments as well, to keep your
project nice and clean.
Of course, you can do far more than log a message here—since it’s an arbitrary
code block, you can do anything you want!
raywenderlich.com 89
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
You should be aware of one more action related to running blocks of code:
Reversing actions
Let’s say you want to make the cat lady go back the way she came: After she
moves in a V to the left, she should move in a V back to the right.
One way to do this would be, after she goes offscreen to the left, to have her run
the existing actionMidMove action to go back to the middle, and creating a new
moveTo(duration:) action to send her back to the start position.
But Sprite Kit gives you a better option. You can reverse certain actions in Sprite Kit
simply by calling reversedAction() on them, resulting in a new action that is the
opposite of the original action.
For example, if you run a moveByX(y:duration:) action, you can run the reverse of
that action to go back the other way:
Not all actions are reversible—for example, moveTo(duration:) is not. To find out if
an action is reversible, look it up in the SKAction class reference, which indicates it
plainly.
raywenderlich.com 90
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Let’s try this out. First, replace the declarations of actionMidMove and actionMove in
spawnEnemy() with the following code:
raywenderlich.com 91
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Now replace the line in spawnEnemy() that creates sequence with the following lines:
Then, you create the reverse of those actions by calling reversedAction() on each,
and insert them into the sequence.
Build and run, and now the cat lady will go one way, then back the other way:
Because sequence actions are also reversible, you can simplify the above code as
follows. Remove the lines where you create the reversed actions and replace the
sequence creation with the following lines:
This simply creates a sequence of actions that moves the sprite one way, and then
reverses the sequence to go back the other way.
Astute observers may have noticed that the first half of the sequence logs a
message as soon as the sprite reaches the bottom of the screen, but on the way
back, the message isn’t logged until after the sprite has waited at the bottom for
one second.
This is because the reversed sequence is the exact opposite of the original, unlike
raywenderlich.com 92
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
your first implementation of the reversal. Later in this chapter, you’ll read about the
group action, which you could use to fix this behavior.
Repeating actions
So far, so good, but what if you want the cat lady to repeat this sequence multiple
times? Of course, there’s an action for that!
Let’s go with the endless variant. Replace the line that runs your action in
spawnEnemy() with the following two lines:
Here, you create an action that repeats the sequence of other actions endlessly,
and run that repeat action on the enemy.
Build and run, and now your cat lady will continuously bounce back and forth. I told
you she’s crazy!
• Move actions
• Sequence actions
• Wait-for-duration actions
• Run-block actions
• Reversing actions
• Repeating actions
Next, you’re going to put all of these together in a new and interesting way to make
raywenderlich.com 93
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
cat ladies spawn periodically, so your zombie can never get too comfortable.
Periodic spawning
Right now, the game spawns a single cat lady at launch. To prepare for periodic
spawning, you’ll revert the spawnEnemy() code to the original version that simply
moves the cat lady from right to left. You’ll also introduce random variance so the
cat lady doesn’t always spawn at the same y-position.
First things first: You need a helper method to generate a random number within a
range of values. Add this new method to MyUtils.swift, alongside the other math
utilities you added in the challenges section of the previous chapter:
extension CGFloat {
static func random() -> CGFloat {
return CGFloat(Float(arc4random()) / Float(UInt32.max))
}
This extends CGFloat to add two new methods: The first gives a random number
between 0 and 1, and the second gives a random number between specified
minimum and maximum values.
It’s not important for you to understand these methods beyond that. But if you’re
really curious, you can read the following note:
Note: random() calls arc4random(), which gives you a random integer between
0 and the largest value possible to store with an unsigned 32-bit integer,
represented by UInt32.max. If you divide that number by UInt32.max, you get a
float between 0 and 1.
Here’s how random(min:max:) works. If you multiply the result of random()—
remember, that's a float between 0 and 1—by the range of values (max - min),
you’ll get a float between 0 and the range. If you add to that the min value,
you’ll get a float between min and max.
This is a very simple way of generating a random number. If you need more
advanced control, check out Chapter 20, "Randomization".
raywenderlich.com 94
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
func spawnEnemy() {
let enemy = SKSpriteNode(imageNamed: "enemy")
enemy.position = CGPoint(
x: size.width + enemy.size.width/2,
y: CGFloat.random(
min: CGRectGetMinY(playableRect) + enemy.size.height/2,
max: CGRectGetMaxY(playableRect) - enemy.size.height/2))
addChild(enemy)
let actionMove =
SKAction.moveToX(-enemy.size.width/2, duration: 2.0)
enemy.runAction(actionMove)
}
You’ve modified the fixed y-position to be a random value between the bottom and
top of the playable rectangle, and you’ve reverted the movement back to the
original implementation—well, the moveToX(duration:) variant of the original
implementation, anyway.
Now it’s time for some action. Inside didMoveToView(), replace the call to
spawnEnemy() with the following:
runAction(SKAction.repeatActionForever(
SKAction.sequence([SKAction.runBlock(spawnEnemy),
SKAction.waitForDuration(2.0)])))
Note that you’re running the action on the scene itself. This works because the
scene is a node, and any node can run actions.
Build and run, and the crazy cat ladies will spawn endlessly, at varying positions:
raywenderlich.com 95
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Remove-from-parent action
If you keep the game running for a while, there’s a problem.
You can’t see it, but there are a big army of cat ladies offscreen to the left. This is
because you never remove the cat ladies from the scene after they've finished
moving.
A never-ending list of nodes in a game is not a good thing. This node army will
eventually consume all of the memory on the device, and at that point, the OS will
automatically terminate your app, which from a user’s perspective will look like your
app crashed.
To keep your game running smoothly, a good rule of thumb is “If you don’t need it
anymore, remove it.” And as you may have guessed, there’s an action for that, too!
When you no longer need a node and want to remove it from the scene, you can
either call removeFromParent() directly or use the remove-from-parent action.
Give this a try. Replace the call to runAction() inside spawnEnemy() with the
following:
Build and run, and now your nodes will clean up properly. Ah—much better!
Note: removeFromParent() removes the node that’s running that action from its
parent. This raises a question: What happens to actions after you run them?
Calling runAction() stores a strong reference to the action you give it, so
won’t that slowly eat up your memory?
The answer is no. Sprite Kit nodes do you the favor of automatically removing
their references to actions when the actions finish running. So you can tell a
node to run an action and then forget about it, feeling confident that you
haven’t leaked any memory.
Animation action
This one is super useful, because animations add a lot of polish and fun to your
game.
To run an animation action, you first need to gather a list of images called textures
that make up the frames of the animation. A sprite has a texture assigned to it, but
you can always swap out the texture with a different one at runtime by setting the
texture property on the sprite.
In fact, this is what animations do for you: automatically swap out your sprite’s
raywenderlich.com 96
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Zombie Conga already includes some animation frames for the zombie. As you can
see below, you have four textures to use as frames to show the zombie walking:
You can then repeat this endlessly for a continuous walk animation.
Give it a shot. First, create a property for the zombie animation action:
Then, add the following code to init(size:), right before the call to
super.init(size:):
// 1
var textures:[SKTexture] = []
// 2
for i in 1...4 {
textures.append(SKTexture(imageNamed: "zombie\(i)"))
}
// 3
textures.append(textures[2])
textures.append(textures[1])
// 4
zombieAnimation = SKAction.animateWithTextures(textures,
timePerFrame: 0.1)
1. You create an array that will store all of the textures to run in the animation.
The first for loop adds frames 1 to 4, which is most of the “forward walk.”
raywenderlich.com 97
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
3. This adds frames 3 and 2 to the list—remember, the textures array is 0-based.
In total, the textures array now contains the frames in this order: 1, 2, 3, 4, 3,
2. The idea is to loop this for a continuous animation.
4. Once you have the array of textures, running the animation is easy—you simply
create and run an action with animateWithTextures(timePerFrame:).
zombie.runAction(SKAction.repeatActionForever(zombieAnimation))
This runs the action wrapped in a repeat-forever action, which will seamlessly cycle
through the frames 1,2,3,4,3,2,1,2,3,4,3,2,1,2....
Build and run, and now your zombie will strut in style!
Stopping action
Your zombie’s off to a good start, but there’s one annoying thing: When the zombie
stops moving, his animation keeps running. Ideally, you’d like to stop the animation
when the zombie stops moving.
In Sprite Kit, whenever you run an action, you can give the action a key by using a
variant of runAction() called runAction(withKey:). This is handy because it allows
you to stop the action by calling removeActionForKey().
func startZombieAnimation() {
if zombie.actionForKey("animation") == nil {
zombie.runAction(
SKAction.repeatActionForever(zombieAnimation),
withKey: "animation")
}
}
func stopZombieAnimation() {
zombie.removeActionForKey("animation")
}
raywenderlich.com 98
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
The first method starts the zombie animation. It runs the animation as before, but
tags it with a key called “animation”.
Also note that the method first uses actionForKey() to make sure there isn’t
already an action running with the key “animation”; if there is, the method doesn’t
bother running another one.
The second method stops the zombie animation by removing the action with the
key “animation”.
Now go to didMoveToView() and comment out the line that runs the action there:
// zombie.runAction(
// SKAction.repeatActionForever(zombieAnimation))
startZombieAnimation()
And call stopZombieAnimation() inside update(), right after the line of code that sets
velocity = CGPointZero:
stopZombieAnimation()
Build and run, and now your zombie will only move when he should!
Scale action
You have an animated zombie and some crazy cat ladies, but the game is missing
one very important element: cats! Remember, the player’s goal is to gather as
many cats as she can into the zombie’s conga line.
In Zombie Conga, the cats won’t move from right to left like the cat ladies do—
instead, they’ll appear at random locations on the screen and remain stationary.
Rather than have the cats appear instantly, which would be jarring, you’ll start
them at a scale of 0 and grow them to a scale of 1 over time. This will make the
cats appear to “pop in” to the game.
func spawnCat() {
// 1
let cat = SKSpriteNode(imageNamed: "cat")
cat.position = CGPoint(
x: CGFloat.random(min: CGRectGetMinX(playableRect),
max: CGRectGetMaxX(playableRect)),
y: CGFloat.random(min: CGRectGetMinY(playableRect),
max: CGRectGetMaxY(playableRect)))
cat.setScale(0)
addChild(cat)
raywenderlich.com 99
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
// 2
let appear = SKAction.scaleTo(1.0, duration: 0.5)
let wait = SKAction.waitForDuration(10.0)
let disappear = SKAction.scaleTo(0, duration: 0.5)
let removeFromParent = SKAction.removeFromParent()
let actions = [appear, wait, disappear, removeFromParent]
cat.runAction(SKAction.sequence(actions))
}
1. You create a cat at a random spot inside the playable rectangle. You set the
cat’s scale to 0, which makes the cat effectively invisible.
You want the cats to spawn continuously from the start of the game, so add the
following inside didMoveToView(), just after the line that spawns the enemies:
runAction(SKAction.repeatActionForever(
SKAction.sequence([SKAction.runBlock(spawnCat),
SKAction.waitForDuration(1.0)])))
This is very similar to the way you spawned the enemies. You run a sequence that
calls spawnCat(), waits for one second and then repeats.
Build and run, and you’ll see cats pop in and out of the game:
raywenderlich.com 100
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
1.0 and you scale it by 2.0, it is now at 2x. If you scale it by 2.0 again, it is now
at 4x. Note that you couldn't use scaleBy(duration:) in the previous example,
because anything multiplied by 0 is still 0!
• scaleXBy(y:duration:): Another “by” variant, but this one allows you to scale x
and y independently.
Rotate action
The cats in this game should be appealing enough that the player wants to pick
them up, but right now they’re just sitting motionless.
Let’s give them some charm by making them wiggle back and forth while they sit.
To do this, you need the rotate action. To use it, you call the
rotateByAngle(duration:) constructor, passing in the angle (in radians) by which to
rotate.
Replace the declaration of the wait action in spawnCat() with the following:
cat.zRotation = -π / 16.0
let leftWiggle = SKAction.rotateByAngle(π/8.0, duration: 0.5)
let rightWiggle = leftWiggle.reversedAction()
let fullWiggle = SKAction.sequence([leftWiggle, rightWiggle])
let wiggleWait = SKAction.repeatAction(fullWiggle, count: 10)
Then, inside the declaration of the actions array, replace the wait action with
wiggleWait, as shown below:
Then you create leftWiggle, which rotates counterclockwise by 22.5 degrees over a
period of 0.5 seconds. Since the cat starts out rotated clockwise by 11.25 degrees,
this results in the cat being rotated counterclockwise by 11.25 degrees.
raywenderlich.com 101
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Because this is a “by” variant, it's reversible, so you use reversedAction() to create
rightWiggle, which simply rotates back the other way to where the cat started.
You create a fullWiggle by rotating left and then right. Now the cat has completed
its wiggle and is back to its start position. This “full wiggle” takes a total of one
second, so in wiggleWait, you repeat this 10 times to have a 10-second wiggle
duration.
Build and run, and now your cats look like they’ve had some catnip!
Group action
So far, you know how to run actions one after another in sequence, but what if you
want to run two actions at exactly the same time? For example, in Zombie Conga,
you want to make the cats scale up and down slightly as they’re wiggling.
For this sort of multitasking, you can use what’s called the group action. It works in
a similar way as the sequence action, where you pass in a list of actions. However,
instead of running them one at a time, a group action runs them all at once.
Let’s try this out. Replace the declaration of the wiggleWait action in spawnCat()
with the following:
This code creates a sequence similar to that of the wiggle sequence, except it scales
up and down instead of wiggling left and right.
The code then sets up a group action to run the wiggling and scaling at the same
time. To use a group action, you simply provide it with the list of actions that should
run simultaneously.
raywenderlich.com 102
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Now replace wiggleWait with groupWait inside the declaration of the actions array,
as shown below:
Build and run, and your cats will bounce with excitement:
Note: The duration of a group action is equal to the longest duration of any of
the actions it contains. So if you include an action that takes one second and
another that takes 10 seconds, both actions will begin to run at the same time,
and after one second, the first action will be complete. The group action will
continue to execute for nine more seconds until the other action is complete.
Collision detection
You’ve got a zombie, you’ve got cats, you’ve even got crazy cat ladies—but you
don’t have a way to detect when they collide.
There are multiple ways to detect collisions in Sprite Kit, including by using the
built-in physics engine, as you’ll learn in Chapter 9, “Intermediate Physics”. In this
chapter, you’ll take the simplest and easiest approach: bounding-box collision
detection.
1. You need a way of getting all of the cats and cat ladies in a scene into lists, so
that you can check for collisions one by one. An easy solution is to give nodes a
name when you create them, allowing you to use
enumerateChildNodesWithName(usingBlock:) on the scene to find all the nodes
with a certain name.
2. Once you have the lists of cats and cat ladies, you can loop through them to
check for collisions. Each node has a frame property that gives you a rectangle
representing the node’s location onscreen.
raywenderlich.com 103
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
3. If you have the frame for either a cat or a cat lady, and the frame for the
zombie, you can use the built-in method CGRectIntersectsRect() to see if they
collide.
Let’s give this a shot. First, set the name for each node. Inside spawnEnemy(), right
after creating the enemy sprite, add this line:
enemy.name = "enemy"
Similarly, inside spawnCat(), right after creating the cat sprite, add this line:
cat.name = "cat"
func checkCollisions() {
var hitCats: [SKSpriteNode] = []
enumerateChildNodesWithName("cat") { node, _ in
let cat = node as! SKSpriteNode
if CGRectIntersectsRect(cat.frame, self.zombie.frame) {
hitCats.append(cat)
}
}
for cat in hitCats {
zombieHitCat(cat)
}
Here, you enumerate through any child of the scene that has the name “cat” or
“enemy” and cast it to an SKSpriteNode, since you know it’s a sprite node if it has
that name.
You then check if the frame of the cat or enemy intersects with the frame of the
zombie. If there is an intersection, you simply add the cat or enemy to an array to
raywenderlich.com 104
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
keep track of it. After you finish enumerating the nodes, you loop through the
hitCats and hitEnemies arrays and call a method that removes the cat or enemy
from the scene.
Note that you don’t remove the nodes from within the enumeration. It’s unsafe to
remove a node while enumerating over a list of them, and doing so can crash your
app.
Also, notice that you do a little trick for the cat lady. Remember that the frame of a
sprite is the sprite’s entire image, including transparent space:
That means if the zombie went into the area of transparent space at the top of the
cat lady image, it would “count” as a hit. Totally unfair!
To resolve this, you shrink the bounding box a little by using CGRectInset(). It’s not
a perfect solution, but it’s a start. You’ll learn a better way to do this in Chapter 10,
“Advanced Physics”.
Add the following call to your collision detection method at the end of update():
checkCollisions()
Build and run, and now when you collide with a cat or enemy, it disappears from
the scene. It’s your first small step toward the zombie apocalypse!
raywenderlich.com 105
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Earlier, you learned that during Sprite Kit’s game loop, first update() gets called,
then some “other stuff” occurs, and finally Sprite Kit renders the screen:
One of the things in the “other stuff” section is the evaluation of the actions you’ve
been learning about in this chapter:
Herein lies the problem with your current collision detection method. You check for
collisions at the end of the update() loop, but Sprite Kit doesn’t evaluate the actions
until after this update() loop. Therefore, your collision detection code is always one
frame behind!
As you can see in your new event loop diagram, it would be much better to perform
collision detection after Sprite Kit evaluates the actions and all the sprites are in
their new spots. So comment out the call at the end of update():
raywenderlich.com 106
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
// checkCollisions()
You probably won’t notice much of a difference in this case, because the frame rate
is so fast, it’s hard to tell it was behind. But it could be quite noticeable in other
games, so it’s best to do things properly.
Sound action
The last type of action you’ll learn about in this chapter also happens to be one of
the most fun—it’s the action that plays sound effects!
First, you need to add sounds to your project. In the resources for this chapter, find
the folder named Sounds and drag it into your project. Make sure that Copy items
if needed, Create Groups and the ZombieConga target are selected, and click
Finish.
Now for the code. Add this line to the end of zombieHitCat():
runAction(SKAction.playSoundFileNamed("hitCat.wav",
waitForCompletion: false))
runAction(SKAction.playSoundFileNamed("hitCatLady.wav",
waitForCompletion: false))
Here, you play the appropriate sound action for each type of collision. Build and
run, move the zombie around and enjoy the sounds of the smash-up!
Sharing actions
In the previous section, perhaps you noticed a slight pause the first time the sound
plays. This can occur whenever the sound system loads a sound file for the first
time. The solution to this problem also demonstrates one of the most powerful
features of Sprite Kit’s actions: sharing.
The SKAction object doesn't itself maintain any state, and that allows you to do
raywenderlich.com 107
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
let actionMove =
SKAction.moveToX(-enemy.size.width/2, duration: 2.0)
But you're creating this action for every cat lady. Instead, you could create an
SKAction property, store this action in it and then use that property wherever you're
currently using actionMove.
Note: In fact, you could modify Zombie Conga so it reuses most of the actions
you’ve created so far. This would reduce the amount of memory your system
uses, but that’s a performance improvement you probably don’t need to make
in such a simple game.
The application is loading the sound the first time you create an action that uses it.
So to prevent the sound delay, you can create the actions in advance and then use
them when necessary.
These properties hold shared instances of the sound actions you want to run.
Finally, replace the line that plays the sound in zombieHitCat() with the following:
runAction(catCollisionSound)
And replace the line that plays the sound in zombieHitEnemy() with the following:
runAction(enemyCollisionSound)
Now you're reusing the same sound actions for all collisions rather than creating a
new one for each collision.
Build and run again. You'll no longer experience any pauses before the sound
effects play.
As for music, stay tuned (no pun intended!)—you’ll learn about that in the next
chapter, where you’ll wrap up the core gameplay by adding a win/lose scene to the
game.
But before you move on, be sure to get some practice with actions by working
raywenderlich.com 108
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Challenges
This chapter has three challenges, and as usual, they progress from easiest to
hardest.
Be sure to do these challenges. As a Sprite Kit developer, you’ll be using actions all
the time, so it’s important to practice with them before moving further.
As always, if you get stuck, you can find solutions in the resources for this chapter
—but give it your best shot first!
Open the project in Xcode and build and run. You’ll see something like the
following:
Each scene in the app demonstrates a particular set of actions, shown as the part of
the label before the backslash. This first example demonstrates the various move
actions.
Each time you tap the screen, you’ll see a new set of actions. As the scenes
transition, you’ll also see different transition effects, shown as the part of the label
after the backslash.
Your challenge is to flip through each of these demos, then take a look at the code
to answer the following questions:
1. What action constructor would you use to make a sprite follow a certain pre-
defined path?
raywenderlich.com 109
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
2. What action constructor would you use to make a sprite 50% transparent,
regardless of what its current transparency settings are?
3. What are “custom actions” and how do they work at a high level?
You can check your answers in a comment at the top of GameScene.swift in the
solution project for this chapter.
Usually in a video game, you'd resolve this problem by making the player sprite
invincible for a few seconds after it gets hit, so the player has time to get his or her
bearings.
Your challenge is to modify the game to do just this. When the zombie collides with
a cat lady, he should become temporarily invincible instead of destroying the cat
lady.
While the zombie is invincible, he should blink. To do this, you can use the custom
blink action that's included in ActionsCatalog. Here’s the code for your convenience:
If you’d like a detailed explanation of this method, see the comment in the solution
for the previous challenge. Here are some hints for solving this challenge:
• You should create a variable property to track whether or not the zombie is
invincible.
• If the zombie is invincible, you shouldn’t bother enumerating the scene’s cat
ladies.
• If the zombie collides with a cat lady, don’t remove the cat lady from the scene.
Instead, set the zombie as invincible. Next, run a sequence of actions that first
makes the zombie blink 10 times over three seconds, then runs the block of code
described below.
• The block of code should set hidden to false on the zombie, making sure he’s
visible at the end no matter what, and set the zombie as no longer invincible.
raywenderlich.com 110
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
Your challenge is to fix that. You’ll modify the game so that when the zombie
collides with a cat, instead of disappearing, the cat joins your conga line!
In the process of doing this, you’ll get more practice with actions, and you’ll also
review the vector math material you learned in the last chapter. Yes, that stuff still
comes in handy when working with actions!
First, when the zombie collides with a cat, don’t remove the cat from the scene.
Instead, do the following:
4. Run an action to make the cat turn green over 0.2 seconds. If you’re not sure
what action to use for this, check out ActionsCatalog.
After this, there are three more things you have to do:
1. Create a constant CGFloat property to keep track of the cat’s move points per
second. Set it to 480.0.
2. Set the zombie’s zPosition to 100, which will make the zombie appear on top of
the other sprites. Larger z values are “out of the screen” and smaller values are
“into the screen”, and the default value is 0.
3. Make a new method called moveTrain. The basic idea for this method is that
every so often, you make each cat move toward the current position of the
previous cat. This creates a conga line effect!
raywenderlich.com 111
2D iOS & tvOS Games by Tutorials Chapter 3: Actions
func moveTrain() {
var targetPosition = zombie.position
enumerateChildNodesWithName("train") {
node, _ in
if !node.hasActions() {
let actionDuration = 0.3
let offset = // a
let direction = // b
let amountToMovePerSec = // c
let amountToMove = // d
let moveAction = // e
node.runAction(moveAction)
}
targetPosition = node.position
}
}
You need to fill in a through d by using the CGPoint operator overloads and utility
functions you created last chapter, and e by creating the appropriate action. Here
are some hints:
1. You need to figure out the offset between the cat’s current position and the
target position.
2. You need to figure out a unit vector pointing in the direction of the offset.
3. You need to get a vector pointing in the direction of the offset, but with a length
of the cat’s move points per second. This represents the amount and direction
the cat should move in a second.
5. You should move the cat a relative amount based on the amountToMove.
And that’s it—who said you couldn’t herd cats? If you got this working, you’ve truly
made this game live up to its name: Zombie Conga!
raywenderlich.com 112
4 Chapter 4: Scenes
By Ray Wenderlich
Zombie Conga is beginning to look like a real game. It has character movement,
enemies, sounds, animation, collision detection—and if you finished the challenges
from the last chapter, even its namesake: a conga line!
However, right now all the action takes place in a single scene of the game: the
default GameScene created for you by the Sprite Kit project template.
In Sprite Kit, you don’t have to place everything within the same scene. Instead,
you can create multiple unique scenes, one for each “screen” of the app, much like
how view controllers work in iOS development.
In this short chapter, you’ll add two new scenes: one for when the player wins or
loses the game and another for the main menu. You’ll also learn a bit about using
the cool transitions you saw in the ActionsCatalog demo from last chapter’s
Challenge 1.
But first, you need to wrap up some gameplay logic so you can detect when the
player should win or lose the game. Let’s get started!
raywenderlich.com 113
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
Note: This chapter begins where the previous chapter's Challenge 3 left off. If
you were unable to complete the challenges or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up where the previous chapter left off.
• Win Condition: If the player creates a conga line of 15 or more cats, the player
wins!
• Lose Condition: The player will start with five lives. If the player spends all of
his or her lives, the player loses.
Right now, when a crazy cat lady collides with the zombie, nothing bad happens—
there’s only a sound. To make this game challenging, you’ll change this so collisions
with a cat lady result in the following effects:
Let’s make it so. Inside GameScene.swift, add a new property to keep track of the
zombie’s lives and another to keep track of whether the game is over:
var lives = 5
var gameOver = false
Next, add this new helper method to make the zombie lose two cats from his conga
line:
func loseCats() {
// 1
var loseCount = 0
enumerateChildNodesWithName("train") { node, stop in
// 2
var randomSpot = node.position
randomSpot.x += CGFloat.random(min: -100, max: 100)
randomSpot.y += CGFloat.random(min: -100, max: 100)
// 3
node.name = ""
node.runAction(
SKAction.sequence([
SKAction.group([
SKAction.rotateByAngle(π*4, duration: 1.0),
SKAction.moveTo(randomSpot, duration: 1.0),
SKAction.scaleTo(0, duration: 1.0)
]),
SKAction.removeFromParent()
raywenderlich.com 114
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
]))
// 4
loseCount++
if loseCount >= 2 {
stop.memory = true
}
}
}
1. Here, you set up a variable to track the number of cats you’ve removed from
the conga line so far, then you enumerate through the conga line.
3. You run a little animation to make the cat move toward the random spot,
spinning around and scaling to 0 along the way. Finally, the animation removes
the cat from the scene. You also set the cat’s name to an empty string so it’s no
longer considered a normal cat or a cat in the conga line.
4. You update the variable that's tracking the number of cats you’ve removed from
the conga line. Once you’ve removed two or more, you set the stop Boolean to
true, which causes Sprite Kit to stop enumerating the conga line.
Now that you have this helper method, call it in zombieHitEnemy(), right after
playing the enemy collision sound, and add a line to subtract 1 from the lives
counter:
loseCats()
lives--
You’re ready to add the code that checks if the player should win or lose. Begin with
the lose condition. Add this to the end of update():
Here, you check if the number of remaining lives is 0 or less, and you make sure
the game isn’t already over. If both of these conditions are met, you set the game
to be over and log out a message.
To check for the win condition, you’ll make a few modifications to moveTrain().
First, add this variable at the beginning of the method:
var trainCount = 0
You’ll use trainCount to keep track of the number of cats in the train. Increment
this counter with the following line inside the enumerateChildNodesWithName() block,
before the call to hasActions():
raywenderlich.com 115
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
trainCount++
Here, you check if there are more than 15 cats in the train, and you make sure the
game isn’t over already. If both of these conditions are met, you set the game to be
over and log out a message.
When you do, you’ll see the following message in the console:
You win!
That’s great, but when the player wins the game, you want something a bit more
dramatic to happen. Let’s create a proper game over scene.
For now, you’re going to keep things simple with a bare-bones new scene. In
Xcode’s main menu, select File\New\File..., select the iOS\Source\Swift File
template and click Next.
raywenderlich.com 116
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
Enter GameOverScene.swift for Save As, make sure the ZombieConga target is
checked and click Create.
Open GameOverScene.swift and replace its contents with some bare-bones code
for the new class:
import Foundation
import SpriteKit
With this, you’ve created an empty class, derived from SKScene, which defaults to a
blank screen when presented. Later in this chapter, you’ll return to this scene to
add artwork and logic.
Now, how do you get to this new scene from your original scene?
Transitioning to a scene
There are three steps to transition from one scene to another:
1. Create the new scene. First, you create an instance of the new scene itself.
Typically, you’d use the default init(size:) initializer, although you can always
raywenderlich.com 117
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
choose to create your own custom initializer if you want to be able to pass in
extra parameters. Later in this chapter, you’ll do just that.
2. Create a transition object. Next, you create a transition object to specify the
type of animation you’d like to use to display the new scene. For example, there
are crossfade transitions, flip transitions, door-opening transitions and many
more.
Open GameScene.swift and add the following lines in moveTrain(), right after the
code that logs “You Win!” to the console (within the if statement):
// 1
let gameOverScene = GameOverScene(size: size)
gameOverScene.scaleMode = scaleMode
// 2
let reveal = SKTransition.flipHorizontalWithDuration(0.5)
// 3
view?.presentScene(gameOverScene, transition: reveal)
Notice that after creating the game over scene, you set its scale mode to the same
as the current scene’s scale mode to make sure the new scene behaves the same
way across different devices.
Now add the exact same lines as above to update(), right after the code that logs
“You lose!” to the console (again, within the if statement):
// 1
let gameOverScene = GameOverScene(size: size)
gameOverScene.scaleMode = scaleMode
// 2
let reveal = SKTransition.flipHorizontalWithDuration(0.5)
// 3
view?.presentScene(gameOverScene, transition: reveal)
Build and run, and either win or lose the game. Feel free to cheat and change the
number of cats to win to less than 15—after all, you’re the developer!
raywenderlich.com 118
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
Whether you win or lose, when you do, you’ll see the scene transition to a new
blank scene:
That’s really all there is to scene transitions! Now that you have a new scene, you
can do whatever you like in it, just as you did in GameScene.
For Zombie Conga, you’ll modify this new scene to show either a “You Win” or a
“You Lose” background. To make this possible, you need to create a custom scene
initializer to pass in either the win or lose condition.
Here, you add a custom initializer that takes just one extra parameter: a Boolean
that should be true if the player won and false if the player lost. You store this
value in a property named won.
Next, implement didMoveToView() to configure the scene when it's added to the
view hierarchy:
raywenderlich.com 119
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
background.position =
CGPoint(x: self.size.width/2, y: self.size.height/2)
self.addChild(background)
// More here...
}
This looks at the won Boolean and chooses the proper background image to set and
sound effect to play.
In Zombie Conga, you want to display the game over scene for a few seconds and
then automatically transition back to the main scene. To do this, add these lines of
code right after the “More here...” comment:
By now, this is all review for you. The code runs a sequence of actions on the
scene, first waiting for three seconds and then calling a block of code. The block of
code creates a new instance of GameScene and transitions to that with a flip
animation.
One last step: You need to modify your code in GameScene to use this new custom
initializer. Open GameScene.swift and inside update(), change the line that
creates the GameOverScene to indicate that this is the lose condition:
Inside moveTrain(), change the same line, but indicate that this is the win
condition:
raywenderlich.com 120
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
Build and run, and play until you win the game. When you do, you’ll see the win
scene, which will then flip back to a new game after a few seconds:
Now that your game is close to done, it’s a good time to turn off the debug drawing
for the playable rectangle. Comment out this line in didMoveToView():
// debugDrawPlayableArea()
Background music
You almost have a complete game, but you’re missing one thing: awesome
background music!
Luckily, we’ve got you covered. Open MyUtils.swift and add the following to the
bottom of the file:
import AVFoundation
do {
try backgroundMusicPlayer = AVAudioPlayer(contentsOfURL: url)
backgroundMusicPlayer.numberOfLoops = -1
backgroundMusicPlayer.prepareToPlay()
backgroundMusicPlayer.play()
} catch {
print("Could not create audio player!")
return
}
}
raywenderlich.com 121
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
Sprite Kit has no built-in way to play background music, so you’ll have to fall back
on other iOS APIs to do it. One easy way to play music in iOS is to use the
AVAudioPlayer class inside the AVFoundation framework. The above helper code
uses an AVAudioPlayer to play some background music in an endless loop.
playBackgroundMusic("backgroundMusic.mp3")
Here, you make the game play the background music when the scene first loads.
Finally, you need to stop the background music when the player switches scenes, so
they can hear the “you win” or “you lose” sound effects. To do this, add this line
right after the “You Win!” log line in moveTrain():
backgroundMusicPlayer.stop()
Also add that same line right after the “You Lose!” log line in update():
backgroundMusicPlayer.stop()
Challenges
This was a short and sweet chapter, and the challenges will be equally so. With only
one challenge for this chapter, it's time to add a main menu scene to the game.
As always, if you get stuck, you can find the solution in the resources for this
chapter—but give it your best shot first!
Zombie Conga’s main menu scene will be very simple: It will show an image and
allow the player to tap to continue straight to a new game. This will effectively be
the same as the splash screen, except it will allow the player more time to get his
or her bearings.
Your challenge is to implement a main menu scene that shows the MainMenu.png
image as a background and upon a screen tap, uses a “doorway” transition over 1.5
seconds to transition to the main action scene.
raywenderlich.com 122
2D iOS & tvOS Games by Tutorials Chapter 4: Scenes
4. Build and run, and make sure the main menu image appears. So far, so good!
If you’ve gotten this working, congratulations! You now have a firm understanding
of how to create and transition between multiple scenes in Sprite Kit.
raywenderlich.com 123
5 Chapter 5: Camera
By Ray Wenderlich
The red box shows what you can see on the screen, but the level continues beyond
to the right. As the player moves Mario to the right, you can think of the
background as moving to the left:
raywenderlich.com 124
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
There are two ways to accomplish this kind of scrolling in Sprite Kit:
1. Move the background. Make the player, enemies and power-ups children of
the “background layer.” Then, to scroll the game, you can simply move the
background layer from right to left, and its children will move with it.
2. Move the camera. New in iOS 9, Sprite Kit includes SKCameraNode, which
makes creating scrolling games even easier. You simply add a camera node to
the scene, and the camera node's position represents the center of the current
view.
In this chapter, you're going to use SKCameraNode to scroll the game, since this is
the easiest method and likely what developers will use the most, now that it's
available. It's time to get scrolling!
Note: This chapter begins where the previous chapter’s Challenge 1 left off. If
you were unable to complete the challenges or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up where the previous chapter left off.
1. Create an SKCameraNode;
2. Add it to the scene and set the scene's camera property to the camera node;
3. Set the camera node's position, which will represent the center of the screen.
Give this a try. Open GameScene.swift and add the following new property for the
camera node:
This completes step 1. Next, add these lines to the end of didMoveToView():
addChild(cameraNode)
camera = cameraNode
cameraNode.position = CGPoint(x: size.width/2, y: size.height/2)
This completes steps 2 and 3, centering the view in the middle of the scene.
Build and run on your iPad Air 2 simulator (I'll explain why the iPad Air and not the
iPhone later), and you'll see the following:
raywenderlich.com 125
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
The game works as before, except now you're using a camera node. To see the
benefit of this, make the camera follow the zombie by adding this line of code to
the end of update():
cameraNode.position = zombie.position
Build and run on your iPad Air 2 simulator, and you'll see that the camera now
follows the zombie:
raywenderlich.com 126
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
That was easy! But right now, the background is only sized to match the visible
area. You don't want your zombie walking through the void, so comment out that
line for now.
// cameraNode.position = zombie.position
Now try running this on your iPhone 6 simulator. At the time of writing, there
appears to be a bug that makes the background a little off-center:
Don't worry too much about how these work; just remember that you should use
getCameraPosition() and setCameraPosition() instead of getting or setting the
camera's position directly.
Try this out in didMoveToView() by replacing the line that sets the camera's position
with the following:
raywenderlich.com 127
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
Build and run on your iPhone 6 simulator, and you'll see the scene is now centered
correctly!
A scrolling background
As you may remember from Chapter 2, you're using a background named
background1 that's the same size as the scene itself. Your project contains a
second background named background2 that’s designed to be placed to the right
of background1, like so:
Your first task is simple: combine these two background images into a single node
so you can easily scroll them both at the same time.
raywenderlich.com 128
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
// 2
let background1 = SKSpriteNode(imageNamed: "background1")
background1.anchorPoint = CGPoint.zero
background1.position = CGPoint(x: 0, y: 0)
backgroundNode.addChild(background1)
// 3
let background2 = SKSpriteNode(imageNamed: "background2")
background2.anchorPoint = CGPoint.zero
background2.position =
CGPoint(x: background1.size.width, y: 0)
backgroundNode.addChild(background2)
// 4
backgroundNode.size = CGSize(
width: background1.size.width + background2.size.width,
height: background1.size.height)
return backgroundNode
}
1. You create a new SKNode to contain both background sprites as children. In this
case, instead of using SKNode directly, you use an SKSpriteNode with no texture.
This is so you can conveniently set the size property on the SKSpriteNode to the
combined size of the background images.
2. You create an SKSpriteNode for the first background image and pin the bottom-
left of the sprite to the bottom-left of backgroundNode.
3. You create an SKSpriteNode for the second background image and pin the
bottom-left of the sprite to the bottom-right of background1 inside
backgroundNode.
4. You set the size of the backgroundNode based on the size of the two background
images.
Next, replace the code that creates the background sprite in didMoveToView() with
the following:
This simply creates the background using your new helper method rather than
basing it on a single background image.
Also note that before, you had the background centered onscreen. Here, you pin
the lower-left corner to the lower-left of the scene, instead.
Changing the anchor point to the lower-left like this will make it easier to calculate
positions when the time comes. You also name the background, “background”, so
you can readily find it.
raywenderlich.com 129
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
Your goal is to make this camera scroll from left to right. To do this, add a property
for the camera's scrolling speed:
func moveCamera() {
let backgroundVelocity =
CGPoint(x: cameraMovePointsPerSec, y: 0)
let amountToMove = backgroundVelocity * CGFloat(dt)
cameraNode.position += amountToMove
}
This calculates the amount the camera should move this frame, and updates the
camera's position accordingly.
Finally, call this new method inside update(), right after the call to moveTrain():
moveCamera()
But as the screen scrolls, the zombie disappears offscreen, the cats stop spawning
—and eventually, you see the void:
Don't worry. It's not the end of the world yet; it's only a minor zombie apocalypse!
raywenderlich.com 130
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
Then, as you scroll both images from right to left, as soon as one of the images
goes offscreen, you simply reposition it to the right:
To do this, replace the code that creates the background node in didMoveToView():
with the following:
for i in 0...1 {
let background = backgroundNode()
background.anchorPoint = CGPointZero
background.position =
CGPoint(x: CGFloat(i)*background.size.width, y: 0)
background.name = "background"
addChild(background)
}
Also, if you still have the lines that get and log the background’s size, comment
them out.
The above wraps the code in a for-loop that creates two copies of the background
and then sets their positions, so the second copy begins after the first ends.
raywenderlich.com 131
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
This is a helper method that calculates the current "visible playable area". You'll be
using this for calculations throughout the rest of the chapter.
enumerateChildNodesWithName("background") { node, _ in
let background = node as! SKSpriteNode
if background.position.x + background.size.width <
self.cameraRect.origin.x {
background.position = CGPoint(
x: background.position.x + background.size.width*2,
y: background.position.y)
}
}
You check to see if the right-hand side of the background is less than the left hand
side of the current visible playable area—in other words, if it's offscreen.
Remember, you set the anchor point of the background to the bottom-left.
If part of the background is offscreen, you simply move the background node to the
right by double the width of the background. Since there are two background
nodes, this places the first node immediately to the right of the second.
Build and run, and now you have an continuously scrolling background! You saved
the world from ending—even if it still has zombies.
raywenderlich.com 132
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
This code assumes that the visible portion of the scene never changes from its
original position. To correct that assumption, change the lines above so they look
like this:
Here you grab the coordinates from the visible playable area, rather than
hardcoding a fixed position.
The cats have a similar problem. Inside spawnCat(), change the lines that set the
cat's position to the following:
cat.position = CGPoint(
x: CGFloat.random(min: CGRectGetMinX(cameraRect),
max: CGRectGetMaxX(cameraRect)),
y: CGFloat.random(min: CGRectGetMinY(cameraRect),
max: CGRectGetMaxY(cameraRect)))
cat.zPosition = 50
This updates the cat so it spawns within the visible playable area rather than at a
hardcoded position. You also update the cat's zPosition to make sure it stays on
raywenderlich.com 133
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
There's one last thing: Since the background is continuously scrolling, your
gameplay will be a lot more dynamic if you disable the code that stops the zombie
once he reaches the target point - this way the zombie will always keep running.
Remember, this was the zombie's original behavior before your second challenge in
Chapter 2.
To let your zombie loose, comment out the relevant code in update(), as shown
below:
/*
if let lastTouchLocation = lastTouchLocation {
let diff = lastTouchLocation - zombie.position
if (diff.length() <= zombieMovePointsPerSec * CGFloat(dt)) {
zombie.position = lastTouchLocation
velocity = CGPointZero
stopZombieAnimation()
} else {
*/
moveSprite(zombie, velocity: velocity)
rotateSprite(zombie, direction: velocity, rotateRadiansPerSec:
zombieRotateRadiansPerSec)
/*}
}*/
Build and run, and now most of the gameplay works smoothly:
w00t, you're almost done—the only thing left to fix are the enemies! And that
challenge is left to you. :]
Challenges
There's only one challenge this time: fixing the gameplay for the enemies.
As always, if you get stuck, you can find the solutions in the resources for this
chapter—but give it your best shot first!
raywenderlich.com 134
2D iOS & tvOS Games by Tutorials Chapter 5: Camera
Look at spawnEnemy() and you'll see this is because you're still selecting the spawn
point assuming the camera never moves, rather than using the currently visible
playable area.
Your challenge is to modify this method to instead spawn enemies right outside of
the currently visible playable area. Also, be sure to set the enemies' zPosition to
match that of the cat so they don't appear below the background.
After you do this, you'll notice that as the level goes on, the enemies spawn faster
and faster. Find out why this is and fix it.
If you got this working, congratulations - you now have a complete scrolling game!
There's just one bit of polish to wrap things up before we move on to another
game: adding some labels to the game.
raywenderlich.com 135
6 Chapter 6: Labels
By Ray Wenderlich
It's often useful in games to display text to keep your player informed. For
example, currently in Zombie Conga, there's no indication of how many lives you
have remaining—which can be quite frustrating if you die unexpectedly!
In this chapter, you'll learn how to display fonts and text within your game.
Specifically, you'll add two labels to Zombie Conga: one to display your current lives
and one to display your count of cats.
Note: This chapter begins where the previous chapter’s Challenge 1 left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up where the previous chapter left off.
raywenderlich.com 136
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
Some font families have even more variants; the "Avenir" family has 12!
iOS ships with a number of built-in font families and fonts, so before you start using
labels, you need to know what's available to you. To find out, you'll create a simple
Sprite Kit project that lets you see these different fonts at a glance.
Enter AvailableFonts for the Product Name, select Swift as the language,
SpriteKit as the Game Technology, Universal for Devices and then click Next.
Select a location on your hard drive to store the project and click Create. You now
have a simple Sprite Kit project open in Xcode that you'll use to list the font families
and fonts available in iOS.
You want this app to run in portrait mode, so select the AvailableFonts project in
the project navigator and then select the AvailableFonts target. Go to the General
tab, check Portrait and uncheck all other orientations.
Just like in Zombie Conga, you'll be creating this scene programmatically rather
than using the scene editor. To do this, select GameScene.sks and delete it from
your project. Then, open GameViewController.swift and replace the contents
with the following:
import UIKit
import SpriteKit
raywenderlich.com 137
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
skView.presentScene(scene)
}
This is the same code you used in Zombie Conga; it simply creates and presents
GameScene to the screen.
It's time to add that code. Open GameScene.swift and replace its contents with
the following:
import SpriteKit
func showCurrentFamily() {
// TODO: Coming soon...
}
You begin by displaying the font family with index 0. Every time the user taps, you
advance to display the next font family name. In iOS, you can get a list of the built-
in font family names by calling UIFont.familyNames().
The code to display the fonts in the current font family will be in
showCurrentFamily(), so implement that now by placing the following code inside
that method:
// 1
removeAllChildren()
// 2
raywenderlich.com 138
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
// 3
let fontNames =
UIFont.fontNamesForFamilyName(familyName)
// 4
for (idx, fontName) in fontNames.enumerate() {
let label = SKLabelNode(fontNamed: fontName)
label.text = fontName
label.position = CGPoint(
x: size.width / 2,
y: (size.height * (CGFloat(idx+1))) /
(CGFloat(fontNames.count)+1))
label.fontSize = 50
label.verticalAlignmentMode = .Center
addChild(label)
}
1. You remove all of the children from the scene so that you start with a blank
slate.
2. You get the current family name based on the index that you increment with
each tap. You also log out the family name, in case you're curious about it.
3. UIFont has another helper method to get the names of the fonts within a family,
named fontNamesForFamilyName(). You call this here and store the results.
4. You then loop through the block and create a label using each font; the text of
each label displays the name of the corresponding font. Since labels are the
subject of this chapter, you'll review them in more detail next.
Creating a label
Creating a label is easy: You simply call SKLabelNode(fontNamed:) and pass in the
name of the font:
The most important property to set is the text, because this is what you want the
font to display.
label.text = fontName
You also usually want to set the font size (unless you want the default of 32 points).
label.fontSize = 50
Finally, just as with any other node, you position it and add it as a child of another
node—in this case, the scene itself:
raywenderlich.com 139
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
label.position = yourPosition
addChild(label)
For now, don't worry too much about the math you're using to position the labels.
Also, don't worry about your use of verticalAlignmentMode—that’s simply a little
code magic to space the labels evenly up and down the screen. You’ll learn more
about alignment later in this chapter.
Build and run. Now, every time you tap the screen, you’ll see a different built-in
font family:
Tap through to get an idea of what's available. Try to find the font named
"Chalkduster"—you'll be using that shortly in Zombie Conga.
raywenderlich.com 140
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
This app will also be a handy reference in the future, when you're wondering what
font would be the perfect match for your game.
Open your Zombie Conga project, using either your post-challenge project file from
the previous chapter or the starter project for this chapter.
With the appropriate project loaded in Xcode, open GameScene.swift and add this
line to the bottom of the list of properties:
Here, you create an SKLabelNode, passing in the "Chalkduster" font you discovered
in AvailableFonts earlier.
raywenderlich.com 141
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
Here, you do the same sorts of things you've already learned about: set the text to
a placeholder, set the position to the center of the screen, set the font size and then
add the node as a child of the scene. You also set a new property, fontColor, to set
the color of the text.
Build and run, and you'll see the label. But wait! It scrolls off the screen as the
camera moves!
It's because you added the label as a child of the scene; as you move the camera,
it "looks at" different parts of the scene.
What you really want is for the label to stay in the same position, regardless of how
the camera moves. To do this, you need to add the label as a child of the camera
node instead.
livesLabel.position = CGPoint.zero
cameraNode.addChild(livesLabel)
Build and run, and you'll see the label is now in a fixed position near the center of
the screen:
raywenderlich.com 142
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
This looks good, except for Zombie Conga, it would look better if this label were
aligned to the bottom-left of the playable area. For you to understand how to do
this, I'd like to introduce you to the concept of alignment modes.
Alignment modes
So far, you know you can place a label by setting its position, but how can you
control the placement of the text in relation to the position?
The red and blue points in the diagram show, for the different alignment modes,
where each label’s bounding box will be rendered in relation to the label’s position.
raywenderlich.com 143
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
• The default alignment modes of SKLabelNode are Center for horizontal and
Baseline for vertical.
• Baseline uses the actual font’s baseline, which you can think of as the “line” on
which you would draw a font, if you were writing on ruled paper. For example,
the tails of letters such as g and y will hang below the defined position.
To align the lives label to the bottom-left of the screen, you want to set the
alignment modes to Left for horizontal and Bottom for vertical. This way, you can
simply set the position to the bottom-left of the playable area.
It's time to try this out. Delete the line that sets the label's position and replace it
with the following:
livesLabel.horizontalAlignmentMode = .Left
livesLabel.verticalAlignmentMode = .Bottom
livesLabel.position = CGPoint(x: -playableRect.size.width/2 +
CGFloat(20),
y: -playableRect.size.height/2 + CGFloat(20) + overlapAmount()/2)
Here you set the alignment modes as discussed, and then set the position to the
bottom-left of the playable area. Here's a diagram to help you visualize this:
You subtract the width and height of the playable area to get to the bottom-left
raywenderlich.com 144
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
corner, and then you add a 20-point margin to provide a little space between the
label and the edges.
Build and run, and now you'll see the label correctly positioned in the bottom-left of
the playable area.
For example, in Zombie Conga, it would be nice to switch to a font that's less
intrusive in the game, but none of the fonts included by default are going to meet
your needs. Luckily, Apple has made it super simple to use a True Type Font (TTF)
in your project.
First, you need to find the font you want to use. One excellent source of fonts is
http://www.dafont.com. Open your browser of choice and enter the URL. You’ll see
there’s a large selection of categories from which to choose, including one named
Fancy/Cartoon.
Click on that category, and you’ll see a huge list of fonts with example text. Some
people could spend hours looking through these fonts just for fun, so take as much
time as you like to see what’s available.
raywenderlich.com 145
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
Now that you’re back, the font you’re going to use is named Glimstick by Uddi
Uddi. Type that name into the search bar on the dafont.com website. A font preview
will appear:
This fun cartoony font is a perfect fit for the minigame you’re creating. Click the
Download button. Once the download is complete, unzip the package and find the
file named GLIMSTIC.TTF. The resources for this chapter also include a copy of
this font, in case you have trouble downloading it.
Note: It’s important to check the license for any fonts you want to use in your
project. Some fonts require permission or a license before you can use them,
so checking now could save a lot of headache and cost later.
You can see just above the download button that the Glimstick font you’re
using is marked as Free, but to be sure, always check the license information
included in the downloaded zip file.
Now that you have your font, drag GLIMSTIC.TTF into your Zombie Conga project.
Make sure that Copy items if needed and the ZombieConga target are checked,
and click Finish.
raywenderlich.com 146
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
Next, open Info.plist and click on the last entry in the list, and you’ll see plus (+)
and minus (-) buttons appear next to that title.
Click the plus button and a new entry will appear in the table, along with a drop-
down list of options:
raywenderlich.com 147
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
In the drop-down box, type Fonts, making sure to use a capital F. The first option
that comes up will be Fonts provided by application. Press Return to select that
option.
Click the triangle to the left of the new entry to expand it, and double-click inside
the value field.
Inside the textbox that appears, type GLIMSTIC.TTF. This is the name of the font
file you downloaded and the one you’re going to use in the game. Be sure to spell it
correctly or your app won’t be able to load it.
Now, to try out this font! Open GameScene.swift and replace your line that
declares the livesLabel property with the following:
Note: Notice how the font filename (i.e. "GLIMSTIC.TTF") does not necessarily
have to match the actual name of the font (i.e. "Glimstick"). You can find the
actual name of the font by double clicking the .TTF file.
Build and run, and you'll see your new font appear:
Build and run, and now your lives will properly update!
raywenderlich.com 148
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
Challenges
This is your final challenge for Zombie Conga. Your game is 99% complete, so don’t
leave it hanging!
As always, if you get stuck, you can find the solutions in the resources for this
chapter—but give it your best shot first!
• Create a property named catsLabel like you did for the livesLabel.
• For the position, refer to the diagram earlier in the chapter if you get stuck.
raywenderlich.com 149
2D iOS & tvOS Games by Tutorials Chapter 6: Labels
Believe it or not, this knowledge is sufficient to make 90% of Sprite Kit games. The
rest is just icing on the cake! :]
raywenderlich.com 150
7 Chapter 7: Beginning tvOS
By Ray Wenderlich
At this point, Zombie Conga is complete as a Universal app for iPhone and iPad.
"But wait a minute", you may be thinking. "This book is called 2D iOS & tvOS
Games by Tutorials—where in the heck is the tvOS part?!"
In this chapter, you'll port Zombie Conga to the Apple TV. By the time you're done,
your game will be running on the big screen!
raywenderlich.com 151
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
Believe it or not, porting your game is easier than it seems. Sprite Kit works the
same on tvOS as it does on iOS, so getting the game to work on tvOS requires only
a few simple steps.
So prepare to bring zombies into your living room—just have your shotgun ready.
Note: This chapter begins where the previous chapter’s Challenge 1 left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up in the right place.
• On tvOS devices, you don't touch the screen directly—after all, we're too busy
lazing on the couch! :] Instead, you move your finger on the remote's touchpad.
Since you aren't touching the screen itself on tvOS devices, your touch handlers
can't receive exact coordinates of the touch location like they do on iOS.
1. When you start touching the remote's touchpad, tvOS calls touchesBegan() with
coordinates of the center of the scene, regardless of where you began to touch
on the touchpad.
2. As you move your finger along the remote's touchpad, tvOS calls
touchesMoved() with coordinates relative to the previous coordinates, based on
how you move your finger.
If this isn't clear yet, don't worry—the best way to understand how this works is
with an example.
Getting started
Open Xcode and go to File\New\Project.... Select the tvOS\Application\Game
template and click Next.
raywenderlich.com 152
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
Enter tvOSTouchTest for Product Name, choose Swift for Language and
choose SpriteKit for Game Technology. Then, click Next.
raywenderlich.com 153
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
You won't be using the Sprite Kit scene editor for this chapter, so delete
GameScene.sks from your project and choose Move to Trash.
import UIKit
import SpriteKit
This simply creates the GameScene of size 2048x1536 and adds it to the SKView, just
as you did earlier for Zombie Conga.
Next, open GameScene.swift and replace its contents with the following:
import SpriteKit
// 1
let pressLabel = SKLabelNode(fontNamed: "Chalkduster")
// 2
let touchBox = SKSpriteNode(color: UIColor.redColor(), size:
CGSize(width: 100, height: 100))
// 3
pressLabel.text = "Move your finger!"
pressLabel.fontSize = 200
pressLabel.verticalAlignmentMode = .Center
pressLabel.horizontalAlignmentMode = .Center
pressLabel.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(pressLabel)
// 4
addChild(touchBox)
// 5
raywenderlich.com 154
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
Take a look at what you're doing with this code, section by section:
1. Here you initialize a label node, just as you learned to do in the previous
chapter.
2. In addition to creating sprite nodes from images, you can create a sprite node
that's a simple color of a specified size. This is often handy for quick tests, like
the one you're doing here. In this case, you initialize a sprite that's a 100x100
red box.
3. You set the text of the label to "Move your finger!" and center it on the screen.
This is a review from the previous chapter.
4. In your touch handler methods, you simply move the red box to the location the
touch handlers report. This will help you visualize what you're receiving for
these methods.
Build and run on the Apple TV simulator, and you'll see the following:
raywenderlich.com 155
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
Note: If you don't see the red box appear and move around, be sure to hold
down Option as you drag your mouse inside the Apple TV remote.
Play around for a bit to see with your own eyes how touches work. As a reminder,
here's what's going on:
1. When you start touching the remote's touchpad, tvOS calls touchesBegan() with
the coordinates of the center of the scene, regardless of where you begin to
touch on the touchpad.
2. As you move your finger along the remote's touchpad, tvOS calls
touchesMoved() with coordinates relative to the previous coordinates, based on
how you move your finger.
Once you're satisfied, keep reading to learn about one more difference in user input
on tvOS.
raywenderlich.com 156
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
Note: You may have noticed that the input to your touch methods can go
outside of the scene's coordinates. This is because the coordinates you receive
aren't related to your view or scene, and instead only represent relative
movement.
Button presses
A second difference in user input on tvOS is that the remote has a lot more buttons
you might want to use.
// 1
override func pressesBegan(presses: Set<UIPress>, withEvent event:
UIPressesEvent?) {
for press in presses {
// 2
switch press.type {
case .UpArrow:
pressLabel.text = "Up arrow"
case .DownArrow:
pressLabel.text = "Down arrow"
case .LeftArrow:
pressLabel.text = "Left arrow"
case .RightArrow:
pressLabel.text = "Right arrow"
case .Select:
pressLabel.text = "Select"
case .Menu:
pressLabel.text = "Menu"
case .PlayPause:
pressLabel.text = "Play/Pause"
}
}
}
raywenderlich.com 157
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
2. Each press has a type field that indicates which button the user is pressing.
Based on the button, you update the label appropriately.
3. You want to clear the label after the user stops pressing a button, but you do so
after a delay, giving the player time to see the button they pressed before you
remove the label.
Build and run, and tap the play/pause button on the remote to see the label
update:
raywenderlich.com 158
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
If you have an Apple TV, try running this app on the actual TV and try to get each
type of touch to show up on the label. Here's what to do for each:
Note: At the time of writing, pressing the menu button while debugging can
confuse the Apple TV and create a state where you can't move around the
main menu. If you have any trouble, just reset your Apple TV.
All right, now that you understand how user input works in tvOS, it's time to apply
this to Zombie Conga.
Click on your ZombieConga project in the project navigator and make sure the
General tab is selected:
So far, there's only one target listed for your project: ZombieConga, which builds
the project for iOS. To build this project for tvOS, you need to add a tvOS.
Note: If you don't see the list of targets in the left sidebar, click the button
just to the left of the General tab to reveal them.
To add a new target, click the plus (+) button at the bottom-left of the list of
raywenderlich.com 159
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
targets:
Just as you did before, select the tvOS\Application\Game template and click
Next:
Enter ZombieCongaTV for Product Name, select Swift for Language and select
Sprite Kit for Game Technology. Then, click Finish:
raywenderlich.com 160
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
You'll see a new target appear in the list, along with a set of files that belong to that
target in the project navigator:
Your goal is to reuse the files from your ZombieConga target in your
ZombieCongaTV target. To do this, right-click your ZombieConga project located in
the project navigator—it's the blue one at the very top, not the yellow folder—and
select New Group. Name the group Shared and drag the following files from your
yellow ZombieConga group to Shared:
1. GLIMSTIC.TTF
raywenderlich.com 161
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
3. MainMenuScene.swift
4. GameSene.swift
5. GameOverScene.swift
6. Assets.xcassets
7. MyUtils.swift
Now, select all of the files you just added to Shared, including the files in the
Sounds group, but not the group itself. Then, in the File Inspector on the right
sidebar, click the checkbox for the ZombieCongaTV target:
There's just a little cleanup left to do. In your ZombieCongaTV group, delete
GameScene.sks, GameScene.swift and Game.xcassets, since you either don't
need them, or they're already included in the shared files. You can move them to
the trash.
import UIKit
import SpriteKit
raywenderlich.com 162
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
This is the same initialization code you're using in the iOS version of Zombie Conga.
That's it—build and run, and enjoy Zombie Conga on the big screen!
Are you wondering how the game works seamlessly with the tvOS resolution? Recall
from Chapter 1 that your strategy was to make the art at the biggest possible size
and aspect ratio, and downscale it for other devices using Aspect Fill. The Apple TV
renders at 1920x1080, so Aspect Fill will scale a 2048x1152 viewable scene size by
0.93 to fit the 1920x1080 screen size. This is the same as the iPhone 6 Plus. :]
raywenderlich.com 163
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
Note: At the time of writing this chapter, if you leave Zombie Conga by hitting
the home menu on your game then return to the game, sometimes the
background sprites will disappear. This appears to be a bug in Sprite Kit on
tvOS.
To see what I mean, add your old friend, the red touch box, from tvOSTouchTest
into Zombie Conga.
Then, add these lines to the bottom of didMoveToView() to add it to the scene:
touchBox.zPosition = 1000
addChild(touchBox)
touchBox.position = touchLocation
touchBox.position = touchLocation
You'll notice that every time you start a new touch, the touch location always
reverts back to the center of the screen, which can sometimes make the zombie
backtrack or move in unexpected directions.
Since tvOS touches don't map directly to scene coordinates, the best way to fix this
is to set the zombie's velocity based on the recent direction of movement of the
user's touches.
raywenderlich.com 164
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
This makes it so that on tvOS, rather than calling the old sceneTouched() method,
you simply store the touch location—that is, the center of the scene, where it
begins—in priorTouch.
// 2
priorTouch = (priorTouch * 0.75) + (touchLocation * 0.25)
// 3
touchBox.position = zombie.position + (direction*200)
#else
touchBox.position = touchLocation
sceneTouched(touchLocation)
#endif
}
1. This sets the velocity based on the direction between the current touch and
priorTouch, rather than trying to move toward a particular point on the screen.
2. You don't want to set priorTouch to touchLocation directly, because you'd get a
lot of noise from minute finger movements. Instead, use a blend: 75% of the
previous priorTouch and 25% of the new touchLocation.
raywenderlich.com 165
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
Build and run, and you'll see the zombie's movement is much improved!
Now that this is working, turn off the red box by adding this to the end of
didMoveToView():
touchBox.hidden = true
In the resources for this chapter, you'll find a folder named LaunchImage; copy
the image from this folder into this slot.
Still in Assets.xcassets, expand the Top Shelf Image and drag the file from the
resources\Top Shelf Image into this slot.
Next, open App Icon - Large in Assets.xcassets and make sure the Attributes
Inspector is open. In the Layers box, click the + button two more times so there
are five layers in total:
The layers are ordered from frontmost to backmost. Drag the files from resources
\App Icon - Large into each slot from top to bottom:
raywenderlich.com 166
2D iOS & tvOS Games by Tutorials Chapter 7: Beginning tvOS
You can drag your mouse around and see a preview of the 3D image—cool!
Build and run, and press the menu button to see the Apple TV home screen. Now
Zombie Conga's got style!
Congratulations, you've made your first complete iOS and tvOS game! There's no
challenge this time, so you can take a well-deserved break.
When you come back, there are new iOS and tvOS games waiting for you to
make! :]
raywenderlich.com 167
Section II: Physics and Nodes
In this section, you will learn how to use the built-in 2D physics engine included
with Sprite Kit to create movement as realistic as that in Angry Birds or Cut the
Rope. You will also learn how to use special types of nodes that allow you to play
videos or create shapes in your game.
In the process, you will create a physics puzzle game called Cat Nap, where you
take the role of a cat who has had a long day and just wants to go to bed.
raywenderlich.com 168
8 Chapter 8: Scene Editor
By Marin Todorov
In this chapter, you'll begin to build the second minigame in this book: a puzzle
game called Cat Nap. Here’s what it will look like when you’re finished:
In Cat Nap, you take the role of a cat who’s had a long day and just wants to go to
bed.
However, a thoughtless human has cluttered the cat’s bed with scrap materials from
his recent home renovation. This silly human's bad choices are preventing the cat
from falling asleep! Of course, since cats don't care much about—well, anything—he
sits on top of the scrap anyway.
Your job is to destroy the blocks by tapping them so the cat can comfortably fall
into place. Be careful, though: If you cause the cat to fall on the floor or tip onto his
side, he’ll wake up and get really cranky.
The puzzle is to destroy the blocks in the correct order, so the cat falls straight
down. One wrong choice and—queue evil music—you face the Wrath of Kitteh!
raywenderlich.com 169
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
You’ll build this game across the next six chapters, in stages:
1. Chapter 8, Scene Editor: You are here! You’ll begin by creating the first level
of the game, pictured above. By the end, you'll have a better understanding of
Xcode's level designer, better known as the scene editor.
4. Chapter 11, Advanced Physics: You’ll add two more levels to the game as
you learn about interactive bodies, joints between bodies, composed bodies and
more.
5. Chapter 12, Crop, Video and Shape Nodes: You’ll add special new blocks to
Cat Nap while learning about additional types of nodes that allow you to do
amazing things—like play videos, crop images and create dynamic shapes.
6. Chapter 13, Intermediate tvOS: In this last chapter you are going to bring
Cat Nap to the silver screen. You are going to take the fully developed game
and add support for tvOS so the player can relax on their couch and play the
game using only the remote.
It's time to get started—there’s nothing worse (or perhaps funnier) than an
impatient cat!
Getting started
Start Xcode and select File\New\Project… from the main menu. Select the iOS
\Application\Game template and click Next.
Enter CatNap for the Product Name, Swift for the Language, Sprite Kit for the
Game Technology and Universal for the Devices. Click Next, then choose a place
on your hard drive to save your project and click Create.
raywenderlich.com 170
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
You want this app to run in landscape rather than portrait mode. Just like you did in
Chapter 1, "Sprites", select the CatNap project in the project navigator and then
select the CatNap target. Go to the General tab and verify that only the device
orientations for Landscape Left and Landscape Right are checked.
You also need to modify this in one more spot. Open Info.plist and find the
Supported interface orientations (iPad) entry. Delete the entries for Portrait
(bottom home button) and Portrait (top home button) so that only the
landscape options remain.
To get this game started on the right foot, or should we say paw, you need to set
up an app icon. To do this, select Assets.xassets in the project navigator on the
left and then select the AppIcon entry. Then, in the resources for this chapter, drag
all of the files from the Icons\iOS subfolder into the area on the right. You might
need to drag a few individual files until Xcode matches all required icons. You'll see
the following when you’re done:
There’s one final step. Open GameViewController.swift and modify the line that
sets skView.ignoresSiblingOrder from true to false:
skView.ignoresSiblingOrder = false
This makes it so nodes with the same zPosition are drawn in the order in which
they are added to the scene, which will make developing Cat Nap a bit simpler.
Keep in mind, though, that there's a performance cost incurred by changing this
raywenderlich.com 171
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
setting to false. Luckily, for a simple game like this, it's not a problem.
Build and run the project on the iPhone simulator. You’ll see the “Hello, World!”
message nicely positioned in the center of the screen, in landscape mode.
In Xcode, open Assets.xcassets and drag in all of the images from the Resources
\Images folder. Also delete Spaceship from the asset catalog; this cat prefers
both paws on the ground! At this point, your asset catalog should look like this:
Next, drag the Resources\Sounds into your project and make sure that Copy
items if needed, Create groups and the CatNap target are all checked. At this
point, your project navigator should look like this:
raywenderlich.com 172
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Woo-hoo! You’ve finished setting up your project. Now it's time to fire up the scene
editor.
The default Sprite Kit project template contains a scene file already. Look in the
project navigator and you’ll see a file called GameScene.sks. Select that file and
you’ll see a new editor panel that shows a gray background:
Click the minus (–) button in the mid-right corner several times until you see a
yellow rectangle appear—you might need to click it five or six times if you're on a
laptop. This is the boundary of your scene. The default size for a new scene is
raywenderlich.com 173
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
1024x768 points.
Remember from Chapter 1, "Sprites", that the strategy we're taking for games in
this book is to use a single set of images sized for a 2048x1536 scene, and let
Sprite Kit downscale the images for all devices with smaller screen resolutions.
Resizing the scene is straightforward.
So let's resize the scene to our preferred 2048x1536 size. To do this, make sure the
utilities editor on the right-hand side is open; if it’s not, click View\Utilities\Show
Attributes Inspector.
Within the Attributes Inspector for the scene, enter the new dimensions:
Now the scene has established a suitable size for supporting all devices.
raywenderlich.com 174
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Note: If the utilities editor on the right-hand side isn't open, click View
\Utilities\Show Object Library.
The Object Library displays a list of objects that you can drop onto your scene and
configure. When you load the scene file, those objects will appear in their correct
positions based on the properties you set for them in the scene editor. That’s much
better than writing code to position and adjust every game object one by one, isn’t
it?
• Color sprite: This is the object you use to put sprites onscreen and the one
you’ll use most often throughout this chapter and the next.
• Shape node: These are special types of nodes in Sprite Kit that allow you to
easily draw squares, circles and other shapes. You'll learn more about these in
Chapter 12, “Crop, Video and Shape Nodes”.
• Label: You already know how to create labels programmatically, but with the
scene editor, you can create them simply by dragging and dropping them onto
the scene.
• Emitter: These are special types of nodes in Sprite Kit that allow you to create
particle systems, which you can use for special effects like explosions, fire, or
rain. You'll learn more about these in Chapter 16, "Particle Systems".
• Light: You can place a light node in your scene for a spotlight effect and have
your scene objects cast shadows. You'll learn more about these in chapter 23,
"2D Lighting".
The best thing about the scene editor is that it’s not just an editor—it also serves as
raywenderlich.com 175
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
a simulator, allowing you to easily preview scenes without running the app. You’ll
see this later.
With the sprite selected, which happens by default when you create it, you'll see
the available properties listed in the Attributes Inspector.
You may recognize a lot of these properties from before—you used many of them
programmatically in your Zombie Conga project (such as position and name).
In the Attributes Inspector, you can set the sprite’s name, parent node and the
image file you want to use as the texture. You can also set the sprite’s position, size
and anchor point, either by hand or by dragging with the mouse.
Further down in the same panel, you’ll see the controls to adjust the sprite’s scale,
z-axis position and z-axis rotation:
raywenderlich.com 176
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
But wait! There's more! Even further down, listed in a section called Physics
Definition, you’ll find more properties you can set:
Notice that your sprite doesn’t have a physics body, which means it is not taking
part in a physics simulation. You'll be returning to this setting in future chapters,
where you'll learn more about Sprite Kit physics.
• Texture: background
• Position X: 1024
• Position Y: 768
This should start you off nicely with the level’s background image:
raywenderlich.com 177
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
• Name: bed
• Texture: cat_bed
This will position the cat bed a bit off the bottom of the scene.
Now let’s move on to those wooden blocks that get in the cat’s way. There will be
four blocks in total, but you'll add them two by two.
Drop two color sprite objects onto the scene. Edit their properties like so:
Take a moment to appreciate how much easier it is to set objects onscreen via the
scene editor instead of through code. Of course, that doesn't mean you shouldn't
understand what goes on behind the scenes. In fact, knowing how to do both is a
huge plus.
OK! It's time to add the horizontal blocks. Drop two more color sprite objects onto
raywenderlich.com 178
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Your scene continues to develop, and all of the obstacles are present now. At this
point, you’re only missing your main character:
Drop one last color sprite object onto the scene. This will be the cat. Edit as
follows:
Finally, you've completed the basic setup of the first Cat Nap level:
Build and run. Notice that your scene appears on the screen. Also notice the “Hello,
raywenderlich.com 179
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
World” label from the template code in GameScene.swift—don't worry about that
now:
These are the basic skills you need to design levels using the scene editor. Luckily,
it's capable of much more than laying down sprites on a scene. In the next section,
you'll build more complex stuff!
File references
A cool feature (introduced in iOS 9) is that the scene editor allows you to reference
content from other scene (.sks) files.
This means you can put together a bunch of sprites, add some effects like
animations and then save those in a reusable .sks file. Then, you can reference the
same content from multiple scenes, and they'll all dynamically load the same
content from the reusable .sks file.
Now comes the best part: If you need to change the referenced content in all
scenes, you only need to edit the original content and you're good to go.
As you may have guessed, this is perfect for level-based games where you often
have characters or other parts of the scene recurring throughout the game. In Cat
Nap, such a recurring character is everybody's favorite kitten:
raywenderlich.com 180
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
In this section, you're going to extract the sleepy cat into its own .sks file and add
more nodes and animations. Then you'll reference all of these as a bundle from
within GameScene.sks.
First and foremost, since you're going to have more than one .sks file, it's time to
organize them neatly. Control-click the yellow CatNap group and select New
Group. Rename the group Scenes, and move GameScene.sks inside the newly
created folder.
Next, control-click Scenes and from the pop-up menu, click New File.... Choose
the iOS/Resource/SpriteKit Scene file template and then click Next. Call the
new file Cat.sks and save it in the project folder.
Xcode automatically opens the newly created Cat.sks file and presents you with an
empty, gray editor window. In exactly the same way as before, your task is to
resize the scene to your needs and add some sprites.
Set the scene size to 380x440 points (the size of the cat) and since you have that
particular pane open, set the Anchor Point to (0.5, 0.5). Doing so lets you position
the nodes inside the scene relative to the scene center; this is slightly easier
placing them relative to the lower left, as most nodes will be centered either
horizontally or vertically:
Now that the scene is ready for prime time, you need to add all of the cat's
elements. First you'll add the torso. Drag in two color sprite nodes from the Object
Library and set their properties like so:
• Cat Body: Name cat_body, Texture cat_body, Position X 22, Position Y -112
raywenderlich.com 181
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Note you set the cat body as the parent of the cat head. Since each Sprite Kit node
can have as many sub-nodes as needed, sometimes it's handy to have one of your
nodes act as the root node—that is, as a parent to other nodes. This way, if you
need to copy or move all nodes, you only need to work with the root node, and the
rest will move along with it.
Now your cat's body and head are nicely positioned within the scene while leaving
space on the left for the big, fluffy tail that you're adding next.
Speaking of big, fluffy tails, drag in a color sprite from the Object Library and set
its properties like so:
• Tail: Name tail, Texture cat_tail, Parent cat_body, Anchor Point (0, 0),
Position (-206, -70), Z Position -1
Later in this chapter, you'll animate the tail so it rotates gently along its (0, 0)
position. This will make it appear as if the cat is swinging its tail slowly in the air,
giving him that cat swagger.
Now it's time to add the rest of the cat parts. Add two color sprite objects to the
scene and adjust their properties like so:
raywenderlich.com 182
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
This completes the cat, and your scene will look like this:
Now you will remove the static cat image from GameScene.sks and use your
newly designed cat scene.
To do this, open GameScene.sks and delete the static cat sprite. In its place, drop
a reference node from the Object Library:
raywenderlich.com 183
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Set the following property values for the selected reference node:
• Name: cat_shared
• Z-Position: 10
Here, you're loading the content of the file Cat.sks and positioning it where the
static cat image used to be. Additionally, you're setting a higher z-position to make
sure the contents of Cat.sks appear above the level's background image.
With this done, you've successfully created a reusable piece of content that you can
use throughout your game. Nice job!
Note: Due to a bug in Xcode 7 you might not see the cat appear on the scene
when you add the reference. To solve this just close Xcode and start it again -
this time around your reference will show the cat just like on the screenshot
above.
There's one problem with your cat—it doesn't do anything interesting. It's just a
bunch of nodes stuck together!
It's time to correct that by creating what's called an "idle animation". This will help
to make the scene come alive.
raywenderlich.com 184
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
In this section, you're going to learn how to add actions to the nodes in your scene.
Later, you'll learn how to extract those actions into their own .sks files and reuse
them to animate different sprites.
If the arrow on the button points upward, like in the screenshot above, click it to
open the action editor:
The action editor displays all the nodes in the scene and a timeline with rows
corresponding to each node. If you've ever worked with animation or video
software, you might be familiar with this type of user interface.
You're going to use the action editor to animate the cat's tail.
Grab a RotateToAngle action object from the Object Library and drop it onto the
timeline track for the tail node. While dragging the action over the tail track, a new
strip will open and show you a live preview where the new action will appear when
dropped.
Drop the action and position it at the beginning of the timeline—that is, at the 0:00
time mark:
raywenderlich.com 185
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Cool! You've just added a rotate action to the tail sprite. You only need to polish the
action a bit before giving it a try. In the Attributes Inspector, set the following
two values:
• Duration: 2
• Degrees: 5
While you're at it, add one more action just after the first one.
Drag another RotateToAngle action object to the tail node and snap it to the end
of the first one; set its properties as follows:
• Start Time: 2
• Duration: 1.5
• Degrees: 0
This action will swing the cat's tail back to its initial position. The timeline will now
look like so:
The best thing about the scene editor is that you don't need to run your game in
the simulator or on a device in order to test your scenes.
Find the Animate button at the top of the action editor and click it; the scene
editor will play the actions you just added. This allows you to quickly find any issues
you may have with your animations.
raywenderlich.com 186
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
If you want more control over the playback, you can simply grab the timeline
scrubber and move it back and forth:
Notice how the Animate button turns into a Layout button. This indicates that
you're currently animating the scene. If you'd like to work again on the layout, click
on Layout to switch back to that mode.
When you click Layout, the timeline position resets, and you can again move
sprites around and edit their properties.
To the right of the Animate button you will see a Playback Speed control. While you
are playing back your actions you can choose the speed of replay. This makes sense
since when you are loading those animations from code you can tell Sprite Kit the
speed you want to use for the animations.
Click Animate and notice how the tail moves two times faster than before. This
feature is very useful when you are prototyping animations in scene editor - if you
are not really sure about the duration of some of your actions you can easily
experiment by just changing the playback speed until you are satisfied.
raywenderlich.com 187
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
The timeline lists all views in your scene in the order you added them to the scene.
You started with the cat body and added the eyes last and you see the nodes in
that exact order too:
As you can imagine the more complex a scene is the more fields you will have in
this list. Once you have so many nodes that you have to scroll continuously up and
down to find the one you're looking for you will feel the need to navigate the list in
a more convenient way.
You have two ways to filter the timeline node list. First in the top left corner just
under Animate you will see a drop list menu:
The item selected by default is All Nodes but you can choose from two more:
• Nodes with Actions: Filters the node list to show you only the nodes that
already have actions attached. Using this option is useful when you want to
modify an existing action and you want to see only the nodes that possibly the
action runs on.
• Selected Nodes: This option will dynamically show you only the nodes you have
currently selected in scene editor. This is a powerful mode as it shows you only
the timeline for the selected scene items.
The second control allowing you to filter the node list is the search field at the
bottom left corner:
raywenderlich.com 188
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
This field allows you to quickly filter the node list to only those nodes whose names
contain the given search term. This comes very handy in the cases when you want
to work with a given node and you have its name off the top of your head.
Last but not least in the bottom right corner there is a slider that allows you to
scale up or down the timeline so you can see more or less actions without having to
scroll through:
Repeating actions
You're almost done, but there's one more thing you need to do - make the cat's tail
wave continuously. This cat refuses to sit still!
First, select both actions in the action editor's timeline while pressing the
Command key; if you do this properly, you'll see both actions appear highlighted:
While both actions are selected, right-click on one of them and select Create Loop
from the pop-up menu.
In the popover menu, select the infinity symbol ∞ to indicate that you want this
action to repeat continuously (the button will remain selected to show you your
current looping preference):
The timeline shows you the currently selected loop in blue and all of the repeats in
an orange tint so you can easily see, which one is the "original" and its repetitions.
raywenderlich.com 189
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Besides an indefinitely repeating loop you can choose from three more options.
When you clicked Create Loop in first place Xcode created a loop that plays once
and repeats one more time. So if you wanted an action that plays a total of two
times - you would not have had to do anything more.
The popup menu gives you few more options to control the repeat count of the
loop:
For your tail-rotation action, you chose ∞ to make the cat gently swing its tail
throughout the whole game.
Note: It's natural to think that the X button closes the popup. In this case
however it removes the looping from your animation instead. To close the
popup window simply click somewhere outside of it and it will automatically go
away.
That's pretty cool! And even cooler is that you've got a little content hierarchy going
on in your game to maximize resource reuse:
1. Cat.sks contains sprites and actions, and configures the looping and duration of
the actions.
2. GameScene.sks contains a complete level setup, and it also references the cat
character from Cat.sks.
This setup allows you to load the cat with all its body parts and attached to them
actions from any level in your game. In fact - you are going to load the cat in each
level in your game!
raywenderlich.com 190
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Further you are going to create more .sks files containing different cat animations,
which you are going to load and use in different stages of the game.
Note: The original Sprite Kit feature list introduced with iOS 9 includes the
ability to also create actions in their own .sks file and re-use those actions for
different nodes in different scenes. This is pretty cool because it alows you to
use one more level of resource abstraction.
However this feature does not work on 32bit systems - i.e. if your game load
actions from .sks files that runs on iPhone6, iPhone 6s, and newer but crashes
on iPhone5, iPhone 4s and earlier. This bug is present in Xcode 7 and is not
fixed in Xcode 7.1 so this chapter does not cover it.
Now build and run your project to see how far you've come:
This is a pretty long chapter, and you're probably a bit tired. If so, take a five
minute break to fool around. Better yet, why not drag more reference nodes from
the Object Library and load up the scene with more cats?
Good fun! Just make sure you remove all of the extra cats before going further. :]
In this chapter you have learned how to find your way around scene editor, how to
raywenderlich.com 191
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
plan and design game scenes, add and edit nodes and run basic actions on them.
The interface of scene editor is fairly simple but it allows you to achieve quite a lot.
So far you have designed the very first Cat Nap level, which is not that complex but
you will keep applying your new skills throughout the two chapters that follow and
create a number of additional levels, which will get progressively more complex.
The next chapters, in which you get to work on Cat Nap, focus more on creating
sprites and actions from code. It's important for you to know how to design and fine
tune your game's levels both from within Scene Editor and your code so you can
always use the best approach for your current project.
With that being said make sure that you really got a good command of the Scene
Editor interface before moving on. The two challenges that follow are a perfect
opportunity to exercise your newly acquired Scene Editor skills.
Once you are finished working through the challenges and you have your cat fully
animated you will be ready to move on to getting to know the ropes of physics
simulation in SpriteKit.
Challenges
There are two challenges this time, to get you some additional practice with the
scene editor; creating actions and laying out levels.
As always, if you get stuck, you can find the solution sin the resources for this
chapter—but give it your best shot first!
In this challenge, you'll use additional types of actions to complete the cat's idle
animation.
Follow the general steps below to create a new action inside Cat.sks. Add the
actions listed below to the cat's mouth node, setting their properties like so:
• Move Action: Start Time 5, Duration 0.75, Timing Ease Out, Offset (0, 5)
• Move Action: Start Time 5.75, Duration 0.75, Timing Ease In, Offset (0, -5)
For the filename of the sound action, select mrreow.mp3 from the drop-down
menu.
raywenderlich.com 192
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
If you're wondering how you can make the actions overlap on the timeline, here's a
hint for you. The final result looks like this:
Since you would like the actions to repeat including the 5 seconds of waiting time
you need to do a little trick.
Add one more action at the beginning of the timeline for the mouth node:
Then select all four actions and create a loop of them like you did earlier in this
chapter. You should see the four actions grouped in a loop like so:
Note: Once more - this loop could have been easier to create without a cheat
node had Sprite Kit not been crashing on 32bit systems.
While you're at it, create one more action and drop it on the eyes node in the scene
timeline. Drag in an AnimateWithTextures Action and set the Start Time to 6.5
and the Duration to 0.75.
Then, in the fourth tab in the bottom right (the Media Library), drag cat_eyes,
cat_eyes1, and cat_eyes2 onto the Textures list of your newly created action.
Finally, click the loop button on your action to add one more repetition. Keep in
mind that sometimes, depending on the timeline zooming, your actions are too
small to accommodate the button - if you don't see it when hovering with your
mouse over an action, try zooming in on that action by using the slider in the
bottom-right corner of the timeline pane.
raywenderlich.com 193
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Also, tick the Restore checkbox; this way when the animation completes, it will go
back to its initial frame.
Once more, in order to trick Sprite Kit to include the initial 6.5 seconds of waiting
time in your loop, add a cheat action:
Select both actions you added to the cat eyes and create a loop that repeats
forever.
Well done so far! Your complete timeline should now look like this:
raywenderlich.com 194
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Build and run to enjoy the fruits of your labour. Watch as the cat sits quietly waving
its tail, and every now and again, sleepily blinks and purrs. Neat, huh?
Create a new CatCurl.sks file and set the scene size to (380, 440). Add one color
sprite object with the following properties:
• Cat Curl: Name cat_curl, Texture cat_curlup1, Position (190, 220), Size
(380, 440)
In the actions editor, add one action to the cat_curl sprite node as follows:
Make sure Restore is not checked. For Textures, drag in the following files from the
Media Library:
• cat_curlup1.png
• cat_curlup2.png
• cat_curlup3.png
You can scrub the timeline view to preview this animation; later you will load and
run this when the player successfully solves a level in Cat Nap:
There's one more scene left to create: the animation to play when your player fails
to solve a level. The process is similar to creating the winning sequence.
Create a new CatWakeUp.sks file and set the scene size to (380, 440). Add one
color sprite object with the following properties:
• Cat Awake: Name cat_awake, Texture cat_awake, Position (190, 220), Size
(380, 440)
In the actions editor, add one action to the cat_awake sprite node with the
following properties:
raywenderlich.com 195
2D iOS & tvOS Games by Tutorials Chapter 8: Scene Editor
Make sure Restore is not checked. For Textures, drag in the following files from the
Media library:
• cat_awake.png
• cat_sleepy.png
You can scrub the timeline view to preview this animation; later you will load and
run this scene when the cat falls off a wooden block and onto the ground, causing
the player to fail the level:
Phew! That was a long chapter with lots of instructions. If you need to take another
break, no one will blame you. However, the next chapter introduces you to the
world of actions, collisions and crazy physics experiments, so don't wait too long to
turn the page.
raywenderlich.com 196
9 Chapter 9: Beginning Physics
By Marin Todorov
So far, you’ve learned to move sprites by manually positioning them and by running
actions. But what if you want to simulate more complex behavior, like a ball
bouncing against a wobbly pillar, a chain of dominos falling down or a house of
cards collapsing?
You could accomplish the above with plenty of math, but there’s an easier way.
Sprite Kit contains a powerful and user-friendly physics engine that will help you
move your objects realistically—in ways both simple and complex—without breaking
a sweat.
With a physics engine, you can accomplish effects like those you see in many
popular iOS games:
• Angry Birds uses a physics engine to simulate what happens when the bird
collides with the tower of bricks.
• Tiny Wings uses a physics engine to simulate the bird riding the hills and flying
into the air.
• Cut the Rope uses a physics engine to simulate the movement of the ropes and
the effect of gravity on the candy.
raywenderlich.com 197
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Combing a physics engines with touch controls can give your games a wonderfully
realistic dynamism—and as you can see in Angry Birds, sometimes in the name of
destruction!
If you like this kind of lifelike behavior and you want to know how to build your own
physics-based game, you’re in the right chapter.
In this chapter, you'll take a break to learn Sprite Kit physics basics in a playground.
But don't worry - in the next two chapters you'll return to your old friend Cat Nap
and integrate the physics engine there.
However, Box2D has two main drawbacks for iOS developers: It's written in C++,
and it could stand to be more user-friendly, especially for beginners.
Apple doesn’t expose Box2D directly, instead it abstracts it behind its own API in
raywenderlich.com 198
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Sprite Kit. In fact, Box2D is walled so well that Apple could choose to change the
physics engine in a later version of iOS, and you wouldn’t even know it.
To make a long story short, in Sprite Kit, you get access to all the power of a super-
popular engine, but through a friendly, polished, Apple-style API.
Physics bodies
For the physics engine to control the movement of one of your sprites, you have to
create a physics body for the sprite. You can think of a physics body as a rough
boundary for your sprite that the engine will use for collision detection.
The illustration below depicts a typical physics body for a sprite. Note that the
shape of the physics body doesn't need to match the boundaries of the sprite
exactly. Usually, you'll choose a simpler shape to help the collision detection
algorithms run faster.
If you need a more precise shape, you can tell Sprite Kit's physics engine to detect
the shape of your sprite by ignoring all transparent parts of the image. This is a
good strategy if you want a more lifelike collision between the objects in your
game. For the cat, the automatically-detected, transparency-based physics body
would look something like this:
raywenderlich.com 199
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
You may be thinking, “Excellent! I’ll just use that all the time.”
Think twice. Before you rush into anything, understand that it takes much more
processing power to calculate the physics for a complex shape like this one, as
compared to a simpler polygonal shape.
Once you set a physics body for your sprite, it will move similarly to how it would in
real life: It will fall with gravity, be affected by impulses and forces and move in
response to collisions with other objects.
You can adjust the properties of your physics bodies, such as how heavy or bouncy
they are. You can also alter the laws of the entire simulated world—for example,
you can decrease gravity so that a ball, upon falling to the ground, will bounce
higher and travel farther.
Imagine you throw two balls and each bounces for a while—the red one under
normal Earth gravity and the blue one under low gravity, such as on the Moon. It
would look something like this:
There are few things you should know about physics bodies:
• Physics bodies are rigid. In other words, physics bodies can’t be squished or
deformed under pressure and won't change shape as a consequence of the
physics simulation. For example, you can't use a physics body to simulate a
squishy ball that deforms as it rolls along the floor.
raywenderlich.com 200
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
only use this feature when absolutely necessary. If you have many shapes
onscreen colliding with each other, try using an alpha mask only for your main
character or for two to three main characters, and set the rest to rectangles or
circles.
Sprite Kit makes all of these features, and many more, incredibly easy to manage.
In Apple’s typical manner, most of the configuration is fully pre-defined, meaning a
blank Sprite Kit project will already include lifelike physics with absolutely no set up
required.
Getting started
Let's learn about physics in Sprite Kit in the best way possible: by experimenting in
real time inside an Xcode playground.
Launch Xcode and from its initial dialogue, select Get started with a playground.
In the next dialogue, enter SpriteKitPhysicsTest for the Name and select iOS for
the Platform.
raywenderlich.com 201
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Xcode will create a new, empty playground, importing only the UIKit framework, so
you can use all of the data types, classes and structures you're used to working
with in your iOS projects.
This view may seem a little strange if you haven't used playgrounds before. Don't
worry—this next section covers the interface and how to experiment (play!) in a
playground.
raywenderlich.com 202
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Note: If you're already comfortable working in playgrounds, you can skip the
next section and move on to "Creating a Sprite Kit playgound".
Playgrounds allow you to experiment with code in real time. But before you do that,
it's a good idea to get familiar with the interface.
On the left-hand side is the source editor (1) and on the right-hand side is the
results sidebar (2). As you type code, Xcode evaluates and executes every line and
produces the results in the results sidebar, as you'd expect.
For example, if you change “Hello, playground” to “Sprite Kit rules!”, you'll
immediately see the results sidebar update to reflect this change. You can
experiment with anything you like, but right now give the code below a try:
var j = 0
for i in 1..<10 {
j += i*2
}
As soon as you paste or type in the code, you'll see the results sidebar update and
neatly align the output of every line against the corresponding code in the editor.
raywenderlich.com 203
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Note: For clarity, we'll display the code line followed by the corresponding
result.
This is a static value, so to see the output of an expression, as well as prove the
code really gets executed in real time, look at the result of the second line:
Xcode wraps the result in quotes to show you that the data type of that result is a
String. The next example shows you the result of an even more elaborate piece of
code:
The code creates a new array containing Int elements with values from 1 to 5.
When you enter an expression like that, on a line by itself, Xcode will evaluate it
and send the result to the results sidebar. This is incredibly useful for debugging
purposes—rather than use a separate log function as you would in a project, simply
write a variable name or an expression, and you'll immediately see its value to the
right.
var j = 0
for i in 1..<10 {
j += i*2
}
(9 times)
If you consider everything you’ve learned so far, you might expect this. The result is
aligned to the code, so even though the line j += i*2 is executed nine times in the
loop, it can still produce only a single line of text.The line tells you how many times
the loop ran, but that’s far from what would actually be useful to you: to see the
values of the variables while the loop runs.
No fear—a playground is smarter than that! Hover with your mouse cursor over the
text (9 times). You'll see an extra button appear along with a little tip:
raywenderlich.com 204
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Click the + button to show the history of the value over the nine loop iterations.
This history is displayed directly under the line of code that calculates the value of
j. Click on the points representing the loop iterations to see the value of your
tracked expression in a little pop-up.
Good work. This is the basic knowledge you need to use an Xcode playground. Now
comes the interesting part: conducting physics experiments in a playground.
import UIKit
import SpriteKit
import XCPlayground
These import the basic UIKit classes, the Sprite Kit framework and the handy
XCPlayground module, which will help you visualize your Sprite Kit scene right inside
the playground window.
Since you already know how to create a new game scene, you'll do that first.
Make sure Xcode’s assistant editor is open; it usually stays in the right-hand side of
the window. To show the assistant editor, select View/Assistant Editor/Show
Assistant Editor from Xcode’s main menu.
raywenderlich.com 205
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Most of this code is surely familiar, though you've never seen it in this context. Let's
have a look at what you achieve above.
First, you create a new SKView instance and give it a frame size of 480 by 320
pixels. Then, you create an empty default SKScene instance and give it the same
size. This is what the code in your view controllers has been doing for you in the
previous chapters of this book.
Finally, you call XCPShowView from the XCPlayground module and pass in a string
title and the view. As you can see, XCPShowView is quite handy and helps with a few
things:
1. First and foremost, it tells Xcode not to abort executing your playground as soon
as it runs through the source code. In a game prototype, you’d like things to
keep running, right? In this case, the playground will continue to run for the
default duration of 30 seconds every time you change the source code.
3. Finally, it records the view over time so you can rewind, fast forward and skim
through the recorded session. You'll see this momentarily.
raywenderlich.com 206
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Do you see the frame rate label flickering as it renders different rates? This tells you
that the scene is rendering live. Wait until the 30 seconds of execution time are up,
and then drag the little slider below the scene left and right. You're dragging
through the recorded session—how cool is that?
Playing with an empty game scene is not so much fun. Fortunately, that’s easy to
change! You have a nice, blank slate; your next step is to add sprites to the scene.
Add this code to the playground to create a new sprite with the image square.png:
Hover the mouse over the results sidebar where it reads SKSpriteNode and click
on the eye icon to see the sprite you just created:
Oh no! The preview shows a broken image. That's because you didn't add any
assets to your playground, and Sprite Kit is letting you know that it couldn't find an
image named square.png.
From Xcode’s main menu, select View/Navigators/Show Project Navigator.
raywenderlich.com 207
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
The playground contains two empty folders. The first, Sources, contains code you
want to pre-compile and make available to your playground, while the second,
Resources, contains assets you want to use, like images, sounds and so forth.
In the Resources folder for this chapter, you'll find a Shapes folder that includes
all of the artwork you need for your playground. Grab all the files inside Shapes
and drop them into the Resources folder in your playground.
Excellent! Now, switch back to Xcode and click the eye icon next to your sprite node
in the results sidebar. This time, you'll see a blue patterned image. With your assets
in place, you're ready to proceed.
square.name = "shape"
square.position = CGPoint(x: scene.size.width * 0.25, y:
scene.size.height *
0.50)
raywenderlich.com 208
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
This code creates three constants: square, circle and triangle. All of them are
sprite nodes, and you initialize them with the textures square.png, circle.png and
triangle.png, respectively.
At this point, you can see in the results sidebar that the three sprites have been
created successfully, but you still can’t see them onscreen. You need to add them to
your scene, so do that with the following code:
scene.addChild(square)
scene.addChild(circle)
scene.addChild(triangle)
This creates three sprites in the center of the screen: a square, a circle and a
triangle. Check them out:
For the most part, this has been a review of creating sprites and positioning them
manually onscreen, although this time, you've done it using a playground. But now
it’s time to introduce something new—controlling these objects with physics!
Circular bodies
Remember two things from earlier in this chapter:
1. For the physics engine to control the movement of a sprite, you must create a
physics body for the sprite.
2. You can think of a physics body as a rough boundary for your sprite that the
physics engine uses for collision detection.
Let's attach a physics body to the circle. Add this at the bottom of the file:
raywenderlich.com 209
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Since the circle sprite uses an image shaped like a circle, it's best to create a
physics body of roughly the same shape. SKPhysicsBody has a convenience
initializer method, SKPhysicsBody(circleRadius:), that creates a circular body.
Because you need to supply the radius of the circle, you'll be dividing the width of
the circle sprite by 2.
Note: The radius of a circle is the distance from the center of the circle to its
edge.
Believe it or not, thanks to Sprite Kit’s pre-configured physics simulation, you're all
done!
Once you save your file, the playground will automatically re-execute your code and
you'll see the circle drop with gravity:
But wait a minute—the circle keeps falling offscreen and disappears! Not to mention
that by the time the scene is rendered, the circle is almost out of sight—you don’t
see much of the fall happen.
The easiest way to fix this is to turn off gravity at the start of your scene and then
turn it back on a few seconds later. Yes—you heard me right—turn off gravity!
Skim through your Swift code and find the line where you call presentScene() on
your SKView. Just before this line, add the code to turn off gravity:
Your scene has a property named physicsWorld that represents the basic physics
setup of your game. When you alter the gravity vector of your physics world, you
change the constant acceleration that is applied to every physics body in your scene
each frame.
As soon as you enter the code to reset gravity to a zero vector, you'll see that now
the circle stays at its initial position without falling down. So far, so good.
Now you're going to create a little helper function named delay. Since you'll write it
once, and not need to re-compile it each time the playground executes, you may
put it aside in the Sources folder.
raywenderlich.com 210
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
The easiest way to add a source file is to right-click on Sources and choose New
File from the pop-up menu.
Name the newly added file SupportCode.swift and then open it in the source
editor. Once it's opened, add the following code to it:
import UIKit
dispatch_after(popTime,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)) {
completion()
}
}
Don't worry too much about this code. All you need to know is that you're using it
to delay code execution, something you'll be doing throughout this chapter.
Now scroll back down to the end of the code and add the following to re-establish
gravity two seconds after the scene is created:
Note: Keep this piece of code at the bottom of the file—all the code you add
from here on out, you'll add just above it.
Now that you've paused gravity, you'll be able to see the circle shape appear in the
assistant editor and then fall under the pull of gravity, two seconds later.
raywenderlich.com 211
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
But that’s still not exactly what you want! For this demo, you want the circle to stop
when it hits the bottom of the screen and stay there.
Luckily, Sprite Kit makes this easy to do using something called an edge loop
body.
First you set the physics body for the scene itself. Any Sprite Kit node can have a
physics body, and remember, a scene is a node, too!
Next, you create a different type of body—an edge loop rather than a circle. There
is a major difference between these two types of bodies:
• The circle body is a dynamic physics body—that is, it moves. It's solid, has mass
and can collide with any other type of physics body. The physics simulation can
apply various forces to move volume-based bodies.
• The edge loop body is a static physics body—that is, it does not move. As the
raywenderlich.com 212
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
name implies, an edge loop only defines the edges of a shape. It doesn't have
mass, cannot collide with other edge loop bodies and is never moved by the
physics simulation. Other objects can be inside or outside its edges.
The most common use for an edge loop body is to define collision areas to describe
your game’s boundaries, ground, walls, trigger areas or any other type of unmoving
collision space.
Since you want to restrict bodies to movement within the screen's edges, you
create the scene’s physics body to be an edge loop with the scene’s frame CGRect:
As you saw when Xcode ran your playground, the circle now stops when it hits the
bottom of the screen and even bounces a little:
raywenderlich.com 213
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Rectangular bodies
Next, you'll add the physics body for the square sprite. Add the following line to the
end of your code:
You can see that creating a rectangular physics body is very similar to creating a
circular body. The only difference is that instead of passing in the radius of the
circle, you pass in a CGSize representing the width and height of the rectangle.
Now that it has a physics body attached to it, the square will fall down to the
bottom of the scene, too... well, in two seconds, thanks to you having paused
gravity—like a boss!
Custom-shaped bodies
Right now, you have two very simple shapes—a circle and a square. What if your
shape is more complicated? For example, there's no built-in triangle shape.
You can create arbitrarily-shaped bodies by giving Sprite Kit a Core Graphics path
that defines the boundary of the body. The easiest way to understand how this
works is by looking at an example—so let's try it out with the triangle shape.
raywenderlich.com 214
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
1. First, you create a new CGMutablePathRef, which you'll use to plot out the
triangle's points.
2. Next, you move your virtual “pen” to the triangle's first point, which in this case
is the bottom left, by using CGPathMoveToPoint(). Note that the coordinates are
relative to the sprite’s anchor point, which by default is its center.
3. You then draw three lines, one to each of the three corners of the triangle, by
calling CGPathAddLineToPoint(). Note the terms “draw” and “line” do not refer to
things you’ll see onscreen—rather, they represent the notion of virtually defining
the points and line segments that make up a triangle.
Before beginning the code for this section, add one more utility function to make
your code shorter and easier to read. You’ll need a random function that returns a
CGFloat value in a given range, so open Sources/SupportCode.swift and add the
following:
Note: You can force the use of an external parameter name by including the
external and internal parameter names, separated by a space, as shown here
with the min parameter. These names don't need to be the same, though they
raywenderlich.com 215
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
are in this case. Sometimes forcing the parameter name for the first
parameter helps to provide a better understanding of the values being passed
into the function.
With that done, let's pour particles over the objects to observe their true physical
shapes.
Return to your playground and add this function before your call to
delay(seconds:completion:):
func spawnSand() {
sand.position = CGPoint(
x: random(min: 0.0, max: scene.size.width),
y: scene.size.height - sand.size.height)
sand.name = "sand"
scene.addChild(sand)
}
In this function, you make a small circular body, just like you did before, out of the
texture named sand.png and position the sprite in a random location at the top of
the scene. You also give the sprite the name sand for easy access to it later.
Let’s add 100 of these sand particles and see what happens! Modify your call to
delay(seconds:completion:) by replacing what's there now with this:
delay(seconds: 2.0) {
scene.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
scene.runAction(
SKAction.repeatAction(
SKAction.sequence([
SKAction.runBlock(spawnSand),
SKAction.waitForDuration(0.1)
]),
count: 100)
)
}
When the scene starts rendering in the assistant editor, you'll see a "sand storm" as
the 100 sand particles rain down and fill the spaces between the three bodies on
the ground:
raywenderlich.com 216
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Observe how the sand bounces off of the shapes, proving that your shapes indeed
have unique bodies.
After 30 seconds of execution, move the recording slider left and right to see the
comical action of the sand going up and down and bouncing off the shapes.
While it's useful to know how to create bodies out of custom paths, there's a much
easier way to handle complex shapes.
Before you begin, there’s one more shape you need to add to your scene, and it
looks a bit like a rotated capital letter L:
Considering the code you wrote to define a triangular path, you probably already
realize that the shape above will be painful to put together in code.
Let's use the alpha mask of the image to create the physics body for the sprite. Add
this to your code:
let l = SKSpriteNode(imageNamed:"L")
l.name = "shape"
l.position = CGPoint(x: scene.size.width * 0.5, y: scene.size.height *
0.75)
l.physicsBody = SKPhysicsBody(texture: l.texture!, size: l.size)
scene.addChild(l)
The initializer SKPhysicsBody(texture:size:) is the one that lifts the burden from
raywenderlich.com 217
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
your shoulders and automatically detects the shape of your sprite. It takes two
parameters, an SKTexture and a CGSize.
In the example above, you use the texture of the sprite to generate the physics
body for that sprite—but you aren't restricted to using the sprite's texture. If your
sprite’s texture has a very complex shape, you can also use a different image with a
rough outline of your sprite to improve the performance of your game.
You can also control the size of the created body by adjusting the size parameter of
SKPhysicsBody(texture:size:).
Look at the scene now, and you'll see that the L shape automatically got a physics
body that follows its outline. It conveniently falls onto the circle shape for a strong
visual effect:
I'm sure you're already wondering how would you debug a real game scene with
many complex shapes—you can't always have particles raining over your game
objects!
The Sprite Kit physics engine provides a very convenient feature: an API that
enables physics debug output to your live scene. You can see the outlines of your
objects, the joints between them, the physics constraints you create and more.
Find the line in your code that enables the frame counter label, sceneView.showsFPS
= true, and add this line below it:
sceneView.showsPhysics = true
As soon as the scene starts rendering anew, you'll see the shapes of all your bodies
drawn in bright green (it may be hard to see in this screenshot but it's there):
Thanks to this feature, you can do some serious debugging of your physics setup.
raywenderlich.com 218
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
To see how a body’s properties affect the game physics, you'll adjust the properties
for the sand. Right now, the sand falls as though it's very heavy, much like granular
rock. What if the pieces were made of soft, elastic rubber?
sand.physicsBody!.restitution = 1.0
The restitution property describes how much energy the body loses when it
bounces off of another body—a fancy way of saying "bounciness".
Values can range from 0.0, where the body does not bounce at all, to 1.0, where
the body bounces with the same force with which it started the collision. The
default value is 0.2.
Note: Sprite Kit sets all properties of physics bodies to reasonable values by
default. An object’s default weight is based on how big it looks onscreen;
restitution and friction ("slipperiness") default to values matching the
material of most everyday objects, and so forth.
One more thing: While valid restitution values must be from 0 to 1, the compiler
won’t complain if you supply values outside of that range. However, think about
what it would mean for a body to have a restitution value greater than 1, for
example. The body would end a collision with more energy than it had initially.
That’s not realistic behavior and it would quickly break your physics simulation, as
the values would grow too large for the physics engine to calculate accurately. It’s
not something I’d recommend in a real app, but give it a try if you want to have
raywenderlich.com 219
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
some fun.
Next, let's make the particles much more dense, so that they're effectively heavier
than the other shapes. Given how bouncy they are now, it should be an interesting
sight!
sand.physicsBody!.density = 20.0
Density is defined as mass per unit volume—in other words, the higher the density
of an object, the heavier it will be for its size. Density defaults to 1.0, so here you
set the sand to be 20x as dense as usual.
This results in the sand being heavier than any of the other shapes—in comparison,
the other shapes behave as if they're made of styrofoam. After the simulation
settles down, you'll end up with something like this onscreen:
The red particles literally throw their considerable weight around and push the
bigger, but lighter, blue shapes aside. When you control the physics, size doesn’t
necessarily matter!
• friction: This sets an object's “slipperiness”. Values can range from 0.0, where
the body slides smoothly along surfaces like an ice cube, to 1.0, where the body
quickly slows and stops when sliding along surfaces. The default value is 0.2.
• dynamic: Sometimes you want to use physics bodies for collision detection, but
move the node yourself with manual movement or actions. If this is what you
want, simply set dynamic to false, and the physics engine will ignore all forces
and impulses on the physics body and let you move the node yourself.
• allowsRotation: You might have a sprite you want the physics engine to
raywenderlich.com 220
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
simulate, but never rotate. If this is the case, simply set this flag to false.
• linearDamping and angularDamping: These values affect how much the linear
velocity (translation) or angular velocity (rotation) decrease over time. Values
can range from 0.0, where the speed never decreases, to 1.0, where the speed
decreases immediately. The default value is 0.1.
• affectedByGravity: All objects are affected by gravity by default, but you can
turn this off for a body simply by setting this to false.
• resting: The physics engine has an optimization where objects that haven’t
moved in a while are flagged as "resting" so the physics engine doesn't have to
perform calculations on them any more. If you ever need to “wake up” a resting
object manually, simply set this flag to false.
• mass and area: These are automatically calculated for you based on the shape
and density of the physics body. However, if you ever need to manually override
the mass, you can. The area is read-only.
• node: The physics body has a handy pointer back to the SKNode to which it
belongs. This is a read-only property.
Applying an impulse
To wrap up this introduction to physics in Sprite Kit, you’re going to add a special
effect to your test scene. Every now and then, you’ll apply an impulse to the
particles, making them jump.
The effect will look like a seismic shock that throws everything into the air.
Remember, impulses adjust an object’s momentum immediately, like a bullet firing
from a gun.
To try it out, add this new method to your playground before your call to
delay(seconds:completion:):
func shake() {
scene.enumerateChildNodesWithName("sand") { node, _ in
node.physicsBody!.applyImpulse(
CGVector(dx: 0, dy: random(min: 20, max: 40))
)
}
}
This function loops over all of the nodes in your scene with the name sand and
applies an impulse to each of them. You apply an upward impulse by having the x-
raywenderlich.com 221
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
component always equal zero and having a random positive y-component between
20 and 40.
You create the impulse as a CGVector, which is just like a CGPoint but named so that
it's clear it's used as a vector. You then apply the impulse to the anchor point of
each particle. Since the strengths of the impulses are random, the shake effect will
look pretty realistic.
Of course, you need to call the function before you'll see the particles jump. Locate
the call to delay(seconds:completion:) and replace it with this one:
delay(seconds: 2.0) {
scene.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
scene.runAction(
SKAction.repeatAction(
SKAction.sequence([
SKAction.runBlock(spawnSand),
SKAction.waitForDuration(0.1 )
]),
count: 100)
)
delay(seconds: 12, completion: shake)
}
You call shake() after 12 seconds have passed, giving the scene time to settle down
so you can observe the seismic shock.
It’s a bit odd that the shapes don't jump by themselves but are rather “lifted” by
the sand particles. Add this code to your shake() function to make the shapes jump,
too:
scene.enumerateChildNodesWithName("shape") { node, _ in
node.physicsBody!.applyImpulse(
CGVector(dx: random(min:20, max:60),
dy: random(min:20, max:60))
)
}
First, you loop through all the shapes and apply a random vector impulse to each of
them. Then, you call delay(seconds:completion:) and tell it to call shake() again in
three seconds.
raywenderlich.com 222
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
Don’t forget to replay those shakes using the scrubber—it’s pretty funny!
Well done. You've covered the basics of Sprite Kit's physics engine, and you're
almost ready to put these concepts to use in a real game. But first, it’s time to push
yourself to prove all that you've learned so far!
Challenges
This chapter has two challenges that will get you ready to create your first physics
game. You’ll learn about forces and create a dynamic sprite with collision detection.
As always, if you get stuck, you can find the solutions in the resources for this
chapter—but do give it your best shot before peeking!
Challenge 1: Forces
So far, you’ve learned how to make the sand move immediately by applying an
impulse. But what if you wanted to make objects move more gradually, over time?
Your first challenge is to simulate a very windy day that will blow your objects back
and forth across the screen. Below are some guidelines for how to accomplish this.
// 1
NSTimer.scheduledTimerWithTimeInterval(0.05, target: scene, selector:
"windWithTimer:", userInfo: nil, repeats: true)
raywenderlich.com 223
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
extension SKScene {
// 2
func windWithTimer(timer: NSTimer) {
// TODO: apply force to all bodies
}
// 3
func switchWindDirection(timer: NSTimer) {
blowingRight = !blowingRight
windForce = CGVector(dx: blowingRight ? 50 : -50, dy: 0)
}
}
1. You declare two timers. The first fires 20 times per second and calls
windWithTimer(_:) on your scene—this is where you'll apply force to all the
bodies. The second timer fires once every three seconds and calls
switchWindDirection(_:), where you'll toggle blowingRight and adjust the
windForce vector accordingly.
2. Inside windWithTimer(_:), enumerate over all sand particles and shape bodies
and apply the current windForce to each. Look up the method named
applyForce(_:), which works in a similar way to applyImpulse(_:), which you
already know.
Remember the difference between forces and impulses: You apply a force every
frame while the force is active, but you fire an impulse once and only once.
If you get this working, you'll see the objects slide back and forth across the screen
as the wind changes direction:
raywenderlich.com 224
2D iOS & tvOS Games by Tutorials Chapter 9: Beginning Physics
As you learned earlier in this chapter, you can accomplish this by setting the
dynamic flag on a physics body to false. Bodies that you move yourself, but that
still have collision detection, are sometimes called kinematic bodies.
Your second challenge in this chapter is to try this out for yourself by making the
circle sprite move not by the physics engine, but by an SKAction. Here are a few
hints:
• Set the dynamic property of the circle’s physics body to false after creating it.
• Create an SKAction to move the circle horizontally back and forth across the
screen, and make that action repeat forever.
If you get this working, you'll see that everything is affected by the gravity, wind
and impulses, except for the circle. However, the objects still collide with the circle
as usual:
If you made it through both of these challenges, congratulations! You now have a
firm grasp of the most important concepts of Sprite Kit's physics engine, and you're
100% ready to put these concepts to use in Cat Nap. Meow!
raywenderlich.com 225
10 Chapter 10: Intermediate
Physics
By Marin Todorov
In Chapter 8, "Scene Editor", you got acquainted with Sprite Kit's level designer by
building the first level of a game called Cat Nap.
In this chapter, you're going to use your newly acquired scene editor and physics
skills to add physics into Cat Nap, creating your first fully playable level! By the end
of this chapter, you'll finally be able to help the sleepy cat settle into his bed:
Purr-fect!
Note: This chapter begins where the Chapter 8’s Challenge 2 left off. If you
were unable to complete the challenges or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up where Chapter 8's Challenge 2 left off.
raywenderlich.com 226
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Getting started
Open your CatNap project, and make sure GameScene.swift is open.
First, you're going to do some more scene initialization by overriding the scene’s
didMoveToView(_:). Just as you did for Zombie Conga, you need to set the scene’s
playable area so that when you’re finished developing Cat Nap, it will fully support
both iPhone and iPad screen resolutions.
To get rid of the default code added by Xcode, replace the contents of
GameScene.swift with the following:
import SpriteKit
Just as you did for Zombie Conga, you begin with the aspect ratio of the iPhone 5
screen and then define the frame of the playable area based on the current scene
size.
Since Cat Nap is a physics-based game, you set the detected playable frame as the
edge loop for the scene. That’s all there is to it—Sprite Kit will now automatically
confine your game objects within the area you designate for the gameplay.
Now you're ready to put to work those sprites you placed in the scene editor!
This, however, leaves you with limited room for customization—you can't add new
methods or simply override one of the built-in functionalities with your own
implementation.
raywenderlich.com 227
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
In this section, you'll learn how to create and employ your own custom node
classes, which will give you exactly the behavior you need for your game from
every node in your level.
You'll start by adding a simple class for the cat bed node. From Xcode's main menu,
select File/New/File... and for the file template, choose iOS/Source/Swift file.
Name the new file BedNode.swift and save it.
import SpriteKit
You've created an empty class that derives from SKSpriteNode; now you need to link
this class to the bed sprite in the scene editor. To do this, open GameScene.sks in
the scene editor and select the cat bed. In the utilities area, switch to the Custom
Class Inspector, which is the last tab on the right.
This way, when you launch your game, instead of creating a plain SKSpriteNode for
the cat bed, Sprite Kit will make a new instance of BedNode. Now you can customize
your BedNode class to behave in the way you'd like.
Next, to understand how much you can do with custom node classes, you're going
to add an event method that will get called when the node is added to the scene. If
you're familiar with building UIKit apps for iOS, it will be similar to
UIView.didMoveToWindow().
First, you need a new protocol for all the nodes that implement your custom event.
Open GameScene.swift and add the following after the import statement:
protocol CustomNodeEvents {
func didMoveToScene()
}
Now, switch back to BedNode.swift and make the class conform to the new
protocol by adding a didMoveToScene() method stub that prints out a message. The
class will now look like this:
raywenderlich.com 228
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
import SpriteKit
As the final step, you need to call the new method. A good place to do that is at the
bottom of the didMoveToView(_:) of your scene class.
As the first parameter, you can specify either a node name or a search pattern. If
you've worked extensively with XML, you'll notice the similarities:
• /name: Search for nodes named "name" in the root of the hierarchy
• //name: Search for nodes named "name" starting at the root and moving
recursively down the hierarchy
• *: Matches zero or more characters; e.g. "name*" will match name1, name2,
nameABC and name
Now you can decipher the search pattern from the last code block: "//*". When
your search pattern starts with //, the search starts at the top of the node
hierarchy, and when you search for *, which means any name, you loop over all
existing nodes, regardless of their names or their locations in the node hierarchy.
raywenderlich.com 229
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Build and run the game. You'll see your test message show up—your code looped
over all nodes, matched the ones implementing CustomNodeEvents and called
didMoveToScene() on each one:
This proves that your game is using your custom BedNode SKSpriteNode subclass for
the cat bed. w00t!
Now you can put all your node setup code in didMoveToScene() for each respective
node class. That way, you won't clog your scene class with code that's relevant only
to specific nodes. In your scene class, you'll add the code that has to do with the
entire scene or interaction between nodes.
To give custom classes another try, add a custom class for the cat. From Xcode's
main menu, select File/New/File... and for the file template, choose iOS/
Source/Swift file. Name the new file CatNode.swift and save it.
import SpriteKit
Just like before, you make sure that when the method gets called, it prints a
statement.
One last thing before moving on: You don't want to change the class in
GameScene.sks; you want to change the class in Cat.sks. Remember,
GameScene.sks only holds a reference to Cat.sks, so you need to go to Cat.sks
to set the appropriate class for your cat node.
Open Cat.sks and select the cat_body sprite node. In the Custom Class Inspector,
set the Custom Class to CatNode.
Build and run the game again. The output in the console is now:
Next you need to connect the nodes you created in the scene editor to variables, so
you can access the sprites in code.
raywenderlich.com 230
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Open GameScene.swift and add two instance variables to the GameScene class:
catNode and bedNode are—or will be in a moment—the cat and cat bed sprite nodes,
respectively. Notice that you use their custom classes, because the scene editor
takes care to use the correct type when creating the scene nodes.
Open GameScene.sks and select the cat bed. In the Attributes Inspector, notice
that the sprite has the name bed. This is how you'll find that sprite in the scene
from code—by its name. In UIKit development, the name of the sprite is much like
a view's tag property.
childNodeWithName(_:) loops through a node's children and returns the first node
with the required name. In this case, you loop through the scene's children, looking
for the bed sprite, which is based on the name you set in the scene editor.
For the cat sprite, you need a different approach. You don't want to work with the
cat reference; instead, you want to work with the cat body. After all, only the cat's
body will have its own physics body—you don't need to apply physics simulation to
the eyes or the whiskers!
Since the cat_body sprite is not a direct child of the scene, you can't simply provide
the name cat_body to childNodeWithName(_:) and expect to get it back. Instead,
you need to recursively search through all the children of the children of the scene!
raywenderlich.com 231
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Consulting the search pattern table reference from earlier, you end up with the
simple pattern //cat_body. That being the case, add this line to
didMoveToView(_:):
Now you have a reference to each sprite, so you can modify them in code. To test
this, add the following two lines to the end of didMoveToView(_:):
bedNode.setScale(1.5)
catNode.setScale(1.5)
Now that you’ve proved you can modify the sprites in code, comment out those two
lines to revert the cat and bed back to normal:
// bedNode.setScale(1.5)
// catNode.setScale(1.5)
Sorry about that, Giganto-cat, but cats already have a big enough ego! :]
Congratulations - now you know how to connect objects in the scene editor to code.
Now it's time to move onto physics!
Adding physics
Recall from the previous chapter that for the physics engine to kick in, you need to
create physics bodies for your sprites. In this section, you’re going to learn three
different ways to do that:
raywenderlich.com 232
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
From your experiments in the previous chapter, you already know how to create
rectangular bodies in code, so let’s look at how to do it in the scene editor.
Open GameScene.sks and select the four block sprites in your scene. Press and
hold the Command key on your keyboard and click on each block until you've
selected them all:
In the Physics Definition section of the Attributes Inspector, change the selection
for Body Type to Bounding Rectangle. This will open a section with additional
properties, allowing you to control most aspects of a physics body. You read about
raywenderlich.com 233
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
The default property values look about right for your wooden blocks: The bodies will
be dynamic, can rotate when falling and are affected by gravity. The Mass field
reads Multiple Values, because Sprite Kit assigned a different mass to each
wooden block based on its size.
That’s all you need to do to set up the blocks’ physics bodies. Notice now that when
you deselect the blocks, they're faintly outlined in blue-green, indicating they have
physics bodies:
There’s one last thing to do: Select all four wooden blocks again, scroll to the top of
the Attributes Inspector and enter block in the Name field. Now you can easily
enumerate all the blocks in the scene and also easily see which ones are blocks
when you debug the scene.
raywenderlich.com 234
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Note: This kind of node setup is something you could implement in a custom
node class. Don't worry—you'll learn how to set up your bodies both from the
scene editor and from code. But you can only do one at a time. :]
In fact, you'll add a custom class for the block nodes later, when you add user
interaction to them.
You know that clicking the Animate button will run any sprite actions you add to
sprites. But what about physics? Will the same button also fire up the good old
physics engine? Click Animate and watch what happens:
You'll see the blocks fall down off the screen. They won't stop at the edges, because
the animate button does not run the code that creates the edge loop that you
added to GameScene, but this is still a handy way to do some basic testing.
raywenderlich.com 235
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
also gives you an opportunity to apply your skills from the previous chapter to Cat
Nap.
The cat bed itself won’t participate in the physics simulation; instead, it will remain
static on the ground and exempt from collisions with other bodies in the scene. It
will still have a physics body, though, because you need to detect when the cat falls
onto the bed. So you're going to give the bed small, non-interactive body for the
purpose of detecting contacts.
Since you’ve already connected your bedNode instance variable to the bed sprite,
you can create the body in code.
As you learned in the previous chapter, a sprite’s physics body doesn’t necessarily
have to match the sprite’s size or shape. For the cat bed, you want the physics
body to be much smaller than the sprite, because you only want the cat to fall
happily asleep when he hits the exact center of the bed. Cats are known to be
picky, after all!
Since you never want the cat bed to move, you set its dynamic property to false.
This makes the body static, which allows the physics engine to optimize its
calculations, because it can ignore any forces applied to this object.
skView.showsPhysics = true
Build and run the project, and you’ll see your scene come alive:
raywenderlich.com 236
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Look at the little rectangle toward the bottom of the screen—that’s the physics body
of the cat bed! It’s green so that you remember it’s not a dynamic physics body.
But your carefully built obstacle tower appears a little off-center. That happened
because the bed body pushed aside your central wooden block. To fix this, you’ll
need to set the block bodies and the bed body so they don’t collide with each other,
something you’ll learn how to do a bit later.
To do this, you'll load a separate image that describes the shape of the cat's physics
body and use it to create the body object itself. Open CatNode.swift and add this
code to didMoveToScene():
raywenderlich.com 237
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
This shape doesn’t include the cat’s head or tail, and it doesn’t follow the outline of
the paws. Instead, it uses a flat bottom edge, so the cat will remain stable on those
wooden blocks.
Next, you create a body for the cat sprite using an SKPhysicsBody instance and the
appropriate texture, scaling it to the node's own size. You’re already familiar with
how to do this from the previous chapter.
Build and run the project again, and check out the debug drawing of the cat’s body.
Excellent work!
Now that you've set up the first level, why don't you take a break from all this
physics and get the player in the mood for puzzles by turning on some soothing and
delightful music?
Introducing SKTUtils
In the first few chapters of this book, while you were working on Zombie Conga,
you created some handy extensions to allow you to do things like add and subtract
two CGPoints by using the + or – operators.
Rather than make you continuously re-add these extensions in each mini-game,
we’ve combined them and created a library named SKTUtils.
Besides handy geometry and math functions, this library also includes a useful class
that helps you easily play an audio file as your game's background music.
Now you're going to add SKTUtils to your project so you can make use of these
methods throughout the rest of the chapter. Happy birthday!
Locate SKTUtils in the root folder for this book and drag the entire SKTUtils folder
into the project navigator in Xcode. Make sure Copy items if needed, Create
Groups and the CatNap target are all checked, and click Finish.
raywenderlich.com 238
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Take a minute to review the contents of the library. It should look quite familiar,
with a few additions and tweaks:
Now every class in your project has access to these timesaving functions.
Background music
Now that you've added SKTUtils, it will be a cinch to add background music. Open
GameScene.swift and add this code to didMoveToView(_:) to start the music:
SKTAudio.sharedInstance().playBackgroundMusic("backgroundMusic.mp3")
Build and run the project, and enjoy the merry tune!
Note: You still have many more build and runs ahead of you in this chapter. If
at any time you feel like muting the background music, just comment out this
last line.
But in Cat Nap, you want a bit more control than that. For example:
• Categorizing bodies. You want to keep the cat bed from colliding with the
raywenderlich.com 239
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
blocks, and vice versa. To do this, you need a way to categorize bodies and set
up collision flags.
• Finding bodies. You want to enable the player to destroy a block by tapping it.
To do this, you need a way to find a body at a given point.
• Detecting collisions between bodies. You want to detect when the cat hits
the cat bed, so he can get his beauty sleep. To do this, you need a way to detect
collisions.
You’ll investigate these areas over the next three sections. By the time you’re done,
you’ll have implemented the most important parts of this mini-game!
Categorizing bodies
Sprite Kit’s default behavior is for all physics bodies to collide with all other physics
bodies. If two objects are occupying the same point, like the brick and the cat bed,
the physics engine will automatically move one of them aside.
The good news is, you can override this default behavior and specify whether or not
two physics bodies should collide. There are three steps to do this:
1. Define the categories. The first step is to define categories for your physics
bodies, such as block bodies, cat bodies and cat bed bodies.
2. Set the category bit mask. Once you have a set of categories, you need to
specify the categories to which each physics body belongs—a physics body can
belong to more than one category—by setting its category bit mask.
3. Set the collision bit mask. You also need to specify the collision bit mask for
each physics body. This controls which categories of bodies the body will collide
with.
As with most things, the best place to start is at the beginning—in this case, by
defining the categories for Cat Nap. In GameScene.swift, add the category
constants outside the GameScene class, preferably at the top:
struct PhysicsCategory {
static let None: UInt32 = 0
static let Cat: UInt32 = 0b1 // 1
static let Block: UInt32 = 0b10 // 2
static let Bed: UInt32 = 0b100 // 4
}
Now you can comfortably access body categories like PhysicsCategory.Cat and
PhysicsCategory.Bed.
You’ve probably already spotted that each of the categories turns on another bit:
raywenderlich.com 240
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
This is very handy, and very fast for the physics engine to calculate, when you want
to specify that the cat should collide with all block bodies and the bed. You can then
say the collision bitmask for the cat is PhysicsCategory.Block |
PhysicsCategory.Bed—read this as “block OR bed”—which produces the logical OR of
the two values:
Note: If you aren't quite at ease with binary arithmetic, you can read more
about bitwise operations here: http://en.wikipedia.org/wiki/Bitwise_operation
Now you can move on to steps two and three: setting the category and collision bit
masks for each object, starting with the blocks.
Go back to GameScene.sks and select the four wooden blocks, as you did
earlier. Look at the current Category Mask and Collision Mask:
Both are set to the biggest integer value possible, thus making all bodies collide
with all other bodies. If you convert the default value of 4294967295 to binary, you’ll
see that it has all bits turned on, and therefore, it collides with all other objects:
4294967295 = 11111111111111111111111111111111
It’s time to implement custom collisions. Edit the blocks’ properties like so:
Note: Just put the decimal values in the boxes—that is, for the Collision Mask,
enter 3.
raywenderlich.com 241
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Next, set up the bed. You created this body from code, so go back to
BedNode.swift and add the following to the end of didMoveToScene():
physicsBody!.categoryBitMask = PhysicsCategory.Bed
physicsBody!.collisionBitMask = PhysicsCategory.None
With the code above, you set the category of the bed body and then set its collision
mask to PhysicsCategory.None—you don’t want the bed to collide with any other
game objects.
Note: As promised earlier, you're learning how to do things both from the
scene editor and from code—when you're on your own, just pick whichever
suits you. I personally like the code approach a little better, because you can
use the defined enumeration members; in the scene editor, you have to use
hard-coded integer values.
At this point, you’ve set up both the wooden blocks and the cat bed with the proper
categories and collision masks. Build and run the project one more time:
As expected, you see a block right in front of the bed’s body without either body
pushing the other away. Nice!
Finally, set the bitmasks for the cat. Since you created the physics body for your cat
sprite in code, you also have to set the category and collision masks in code,
specifically in CatNode.swift. Open that file and add this to the end of
didMoveToScene():
parent!.physicsBody!.categoryBitMask = PhysicsCategory.Cat
parent!.physicsBody!.collisionBitMask = PhysicsCategory.Block
You put the cat into its own category, PhysicsCategory.Cat, and set it to collide only
with blocks. Note how you add the physics body to the parent node (i.e. the
raywenderlich.com 242
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Now you know how to make a group of bodies pass through some bodies and
collide with others. You’ll find this technique useful for many types of games. For
example, in some games you want players on the same team to pass through each
other, but collide with enemies from the other team. Often, you don't want game
physics to imitate real life!
Handling touches
In this section, you'll implement the first part of the gameplay. When the player
taps a block, you'll destroy it with a pop.
To distinguish nodes you can tap on and those node that are just static decoration
you will add a new protocol. Open GameScene.swift and add under the existing
protocol declaration for CustomNodeEvents:
protocol InteractiveNode {
func interact()
}
When you create a custom node for the level's block nodes you will make that class
adhere to Interactive and will add the method interact() where you will place all
code to react to the player's touches.
Since SKNode inherits from UIResponder, you can handle touches on each node from
the node's own custom class by overriding touchesBegan(_:withEvent:),
touchesEnded(_:withEvent:) or other UIResponder methods.
Since right now you're interested in simple taps on the block nodes, a BlockNode
class with just touchesEnded(_:withEvent:) will suffice.
You're already quite familiar with creating custom node classes, so this should be a
breeze. From Xcode's main menu, select File/New/File... and for the file
template, choose iOS/Source/Swift file. Name the new file BlockNode.swift
and save it.
raywenderlich.com 243
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
import SpriteKit
func interact() {
userInteractionEnabled = false
}
For this type of node, you did all the physics body setup from the scene editor, so
you only need to enable user interactions inside didMoveToScene(). By default,
userInteractionEnabled is off to keep the responder chain as light as possible—but
for your blocks, you definitely want to handle touches, so you set it to true.
Since you will allow the players to destroy a block by simply tapping it once, as
soon as interact() is being called you turn off userInteractionEnabled to ignore
further touches on the same block.
The final step before you test that code is to set this custom class to all block
nodes, in the scene editor. Open GameScene.sks and select the four wooden
blocks just as you did before. In the Custom Class Inspector, enter BlockNode for
the Custom Class:
Build and run the project, and start tapping some blocks. You should see one line in
the console for each of your taps on a block:
raywenderlich.com 244
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Now for the fun part! You want to destroy those blocks and remove them from the
scene. Add this to interact() in BlockNode.swift:
runAction(SKAction.sequence([
SKAction.playSoundFileNamed("pop.mp3", waitForCompletion: false),
SKAction.scaleTo(0.8, duration: 0.1),
SKAction.removeFromParent()
]))
Here you're running a sequence of three actions: The first action plays an amusing
pop sound, the next scales down the sprite and the last removes it from the scene.
This should be enough to make the level's basic physics work.
Build and run the project again. This time, when you tap the blocks, you've got
your game on:
raywenderlich.com 245
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
In Cat Nap, you want to know whether certain pairs of bodies touch:
1. If the cat touches the floor, it means he's on the ground, but out of his bed,
so the player fails the level.
2. If the cat touches the bed, it means he landed successfully on the bed, so the
player wins the level.
Sprite Kit makes it easy for you to receive a callback when two physics bodies make
contact. The first step is to implement the SKPhysicsContactDelegate methods.
raywenderlich.com 246
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
The diagram below shows how you'd call these methods in the case of two bodies
passing through each other:
However, there are times you’ll want to know when objects stop touching. For
example, you may want to use the physics engine to test when a player is within a
trigger area. Perhaps entering the area sounds an alarm, while leaving the area
silences it. In a case such as this, you’ll need to implement didEndContact(_:), as
well.
To try this out, you first need to add a new category constant for the edges of the
screen, since you want to be able to detect when the cat collides with the floor.
Scroll to the top of GameScene.swift and add this new PhysicsCategory value:
physicsWorld.contactDelegate = self
physicsBody!.categoryBitMask = PhysicsCategory.Edge
First, you set GameScene as the contact delegate of the scene’s physics world. Then,
you assign PhysicsCategory.Edge as the body’s category.
Build and run the project to see the results of your actions so far.
raywenderlich.com 247
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Hmm... that’s not right. All the blocks fall through the edge—but just seconds ago,
the project worked fine!
The issue here is that the world edge now has a category, PhysicsCategory.Edge,
but the blocks aren’t set to collide with it. Therefore, they fall through the floor.
Meanwhile, the cat bed’s dynamic property is set to false, so it can’t move at all.
With no blocks on the screen, you have no game! Open GameScene.sks in the
scene editor and select the four wooden blocks, just as you did before. Then,
change the Collision Mask for their bodies from 3 to 11.
Build and run the project now, and you’ll see the familiar scene setup. But try
popping all the blocks out of the cat’s way, and you’ll see the cat fall through the
bottom of the screen and disappear. Goodbye, Kitty!
By now, you probably know what's wrong: The cat doesn’t collide with the scene’s
edge loop, of course!
Go to CatNode.swift and change the line where you set the cat’s collision mask so
that the cat also collides with the scene's boundaries:
parent!.physicsBody!.collisionBitMask = PhysicsCategory.Block |
PhysicsCategory.Edge
This should keep that pesky feline from falling off the screen!
Build and run the project again, and everything will appear (and behave!) as it
should.
raywenderlich.com 248
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
In Cat Nap, you want to receive callbacks when the cat makes contact with either
the edge loop body or the bed body, so switch to CatNode.swift and add this line
to the end of didMoveToScene():
parent!.physicsBody!.contactTestBitMask = PhysicsCategory.Bed |
PhysicsCategory.Edge
That’s all the configuration you need to do. Every time the cat body makes contact
with either the bed body or the edge loop body, you’ll get a message.
Look at the parameter this method receives—it’s of class SKPhysicsContact and tells
you a lot about the contacting bodies:
raywenderlich.com 249
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
There's no way to guarantee a particular object will be in bodyA or bodyB. But there
are various ways you can find out, such as by checking the body’s category or
looking for some property of the body’s node.
This simple game contains only four categories so far, which correspond to the
integer values 1, 2, 4 and 8. That makes it simple to check for contact combinations
—simply use bitwise OR as you did to define the collision and contact bitmasks.
Note: If you feel the ground loosening under your feet when you think about
comparing bitmasks, consider reading this short but informative article: http://
en.wikipedia.org/wiki/Mask_(computing).
• If the two contacting bodies are the cat and the bed, you print out “SUCCESS”.
• If the two contacting bodies are the cat and the edge, you print out “FAIL”.
raywenderlich.com 250
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Build and run the project to verify you've got this working thus far. You'll see a
message in the console when the cat makes contact with either the bed or the floor.
Note: When the cat falls on the ground, you'll see several FAIL messages.
That’s because the cat bounces off the ground just a little by default, so it
ends up making contact with the ground more than once. You’ll fix this soon.
Finishing touches
You’re almost there—you already know when the player should win or lose, so you
just need to do something about it.
• Handle losing
• Handle winning
Next, you need a new custom class; it will inherit from SKLabelNode, the built-in
Sprite Kit label class, but it will implement some custom behavior.
From Xcode's main menu, select File/New/File... and for the file template, choose
iOS/Source/Swift file. Name the new file MessageNode.swift and save it.
raywenderlich.com 251
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
import SpriteKit
self.init(fontNamed: "AvenirNext-Regular")
text = message
fontSize = 256.0
fontColor = SKColor.grayColor()
zPosition = 100
}
}
You add a new convenience init that expects a parameter for the text to show
onscreen. To initialize the label node, you call another built-in convenience init that
sets the label with the AvenirNext font.
Next, you set the label's text, font size, color and z-position; you want the text to
display over all other scene nodes and 100 is an acceptable value.
To make things a bit more interesting, you add another label as a child to the
current one, the second one having a different color and offset by a few points.
Essentially, you're creating a poor man's drop-shadow for the text by combining
dark and light copies of the message.
Now, to make the message more amusing, add some physics to it by appending the
following to the convenience init of MessageNode:
You create a circular physics body for the label and set it to bounce off of the
scene's edge. You also assign it to its own physics category, PhysicsCategory.Label.
When you add the label to the scene, it will bounce around until it rests on the
"ground", like so:
raywenderlich.com 252
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
To make showing an in-game message even easier, add a short utility method to
GameScene.swift:
In this method, you create a new message node and add it at the center of the
scene. Once that's done, the physics engine will take care of the rest.
Now you'll add the methods that run the winning and losing sequences, and you'll
use inGameMessage(_:) from there.
Losing scenario
First of all, you’re going to add a method to restart the current level. To do that,
you’ll simply call presentScene(_:) again on the SKView of your game, and it will
reload the whole scene.
func newGame() {
let scene = GameScene(fileNamed:"GameScene")
scene!.scaleMode = scaleMode
view!.presentScene(scene)
}
• Set the scale mode of the scene to match the scene's current sacale mode;
• Pass the new GameScene instance to presentScene(_:), which removes the current
scene and replaces it with the shiny new scene.
raywenderlich.com 253
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
With all preparations complete, it's time to add the initial version of the lose
method to GameScene.swift:
func lose() {
//1
SKTAudio.sharedInstance().pauseBackgroundMusic()
runAction(SKAction.playSoundFileNamed("lose.mp3", waitForCompletion:
false))
//2
inGameMessage("Try again...")
//3
performSelector("newGame", withObject: nil, afterDelay: 5)
}
1. You play a fun sound effect when the player loses. To make the effect more
prominent, you pause the in-game music by calling pauseBackgroundMusic() on
SKTAudio. Then, you run an action to play the effect on the scene.
2. You also spawn a new in-game message that reads, "Try again...", to keep your
players motivated. :]
3. Finally, you wait for five seconds and then restart the level by calling newGame().
That’s it for now—locate didBeginContact(_:) and add the following line after
print("FAIL"):
lose()
You now have a working fail sequence. Build and run the project, and give it a try:
Oops! Something isn't quite right, and it's a problem you've noticed before.
As the cat bounces off the floor, it produces numerous contact messages, and since
the contact is always between the cat and the scene's edge, you get many calls to
your shiny, new lose() method.
To prevent this from happening, you need a mechanism to stop in-game
raywenderlich.com 254
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
interactions once the player has failed or completed the level. This calls for a state
machine!
For Cat Nap, you're going to build a very simple state machine, but don't let that
sink your motivation level—you're going to learn much more about building solid
game state machines in Chapter 15, "State Machines".
Add a new instance variable to your GameScene class to hold the state of your level:
The level is playable as soon as it loads and appears onscreen. However, you want
the level to become inactive as soon as you call lose(), because the player should
never be able to lose multiple times without trying again. :]
playable = false
Finally, to prevent more successful contacts, insert the following at the top of
didBeginContact(_:):
if !playable {
return
}
This should suffice for now—your level is playable at launch. Then, as soon as the
player fails, it becomes inactive. When the level restarts, it's playable again.
Build and run the program, and test it once more. You've solved the multi-message
problem and the game restarts after a few seconds:
Good work—it was an easy fix that introduced you to the importance of handling
raywenderlich.com 255
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Playing an animation
There's something that feels incomplete about this losing sequence—the cat seems
emotionless about his grand failure to comfortably sneak in a nap.
It's finally time to put the wake-up animation you designed in Chapter 8, "Scene
Editor", to work.
func wakeUp() {
// 1
for child in children {
child.removeFromParent()
}
texture = nil
color = SKColor.clearColor()
// 2
let catAwake = SKSpriteNode(fileNamed:
"CatWakeUp")!.childNodeWithName("cat_awake")!
// 3
catAwake.moveToParent(self)
catAwake.position = CGPoint(x: -30, y: 100)
}
You call this method on the cat node to "wake up" the cat. The method consist of
two sections:
1. In the first section, you loop over all of the cat's child nodes—the cat "parts"—
and remove them from the cat body. Then you set the current texture to nil.
Finally, you set the cat's background to a transparent color, which effectively
resets the cat to an empty node.
2. In the second section, you load CatWakeUp.sks and fetch the scene child
named cat_awake. Review the contents of that .sks file, and you'll see that
cat_awake is the name of the only sprite found there. This is also the sprite on
which the cat_wake action runs.
3. Finally, you change the sprite's parent from the CatWakeUp.sks scene to the
CatNode. You set the node's position to make sure that it will appear exactly over
the existing texture.
raywenderlich.com 256
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Note: I hope you noticed the use of moveToParent:. If a sprite already has a
parent, you can't use addChild(_:) directly to add it elsewhere;
moveToParent(_:) removes it from its current hierarchy and adds it at the new
location you specify.
That's it! Switch back to GameScene.swift and add the following at the bottom of
lose():
catNode.wakeUp()
Build and run the project and enjoy your complete sequence:
Winning scenario
Now that you have a losing sequence, it's only fair to give the player a winning
sequence. Add this new method to your GameScene class:
func win() {
playable = false
SKTAudio.sharedInstance().pauseBackgroundMusic()
runAction(SKAction.playSoundFileNamed("win.mp3", waitForCompletion:
false))
inGameMessage("Nice job!")
This code looks almost identical to what you did in lose(), with few differences, of
course. When you pause the music, you play an uplifting win song and show the
rewarding Nice job! message.
Just like before, you'll add an extra method in your cat node class to load the
winning animation. Open CatNode.swift and add the following:
raywenderlich.com 257
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
child.removeFromParent()
}
texture = nil
color = SKColor.clearColor()
1. You remove the cat's physics body, because you'll animate the cat manually into
the bed;
curlAt(_:) expects a single CGPoint parameter, which is the bed location in the
scene's coordinate system. To find the curl point in the cat coordinate system, you
need to first convert the location. That's easy thanks to the
convertPoint(_:fromNode:) API, which converts positions from one node's
coordinate system to another node's coordinate system.
In the first line, you call convertPoint(_:fromNode:) on the cat body's parent—that
is, the cat reference you load from the .sks file. You need to work with the body's
parent coordinates while the body itself is positioned at those coordinates. Thus,
you need the target curl point within a coordinate system in which you can animate
the body.
In the second line, you add one third of the cat's height to the curl point, which
makes the curl happen toward the bottom of the bed, not in its center.
runAction(SKAction.group([
SKAction.moveTo(localPoint, duration: 0.66),
SKAction.rotateToAngle(0, duration: 0.5)
]))
This action group animates the cat to the center of the bed, and it also straightens
up the cat in case he was falling over.
You've reached the final steps to put everything together. Open GameScene.swift
and in win(), append this line at the bottom:
catNode.curlAt(bedNode.position)
raywenderlich.com 258
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
Then, inside didBeginContact(_:), find the print("SUCCESS") line and add this line
after it:
win()
Build and run the project. You now have a winning sequence in place (pun
intended):
Believe it or not, you’ve completed another mini-game! And this time, your game
also has a complete physics simulation. Give yourself a pat on the back.
Don’t be sad that your game has only one level. You’ll continue to work on Cat Nap
in the next two chapters, adding two more levels as well as some crazy features
before you’re done.
Challenges
Make sure you aren’t rushing through these chapters. You're learning a lot of new
concepts and APIs, so iterating over what you've learned is the key to retaining it.
That’s one reason why the challenges at the end of each chapter are so important.
If you feel confident about everything you’ve covered so far in Cat Nap, move on to
the challenge.
This chapter introduced a lot of new APIs, so in case you get stuck, the solutions
are in the resources folder for this chapter. But have faith in yourself—you can do it!
raywenderlich.com 259
2D iOS & tvOS Games by Tutorials Chapter 10: Intermediate Physics
off the bottom margin of the screen and remove the message on exactly the fourth
bounce. Working through this will teach you more about custom node behaviors,
and it will be a nice iteration over what you've already learned.
Try implementing the solution on your own, but if you need a little help, follow the
directions below.
1. Add a variable in MessageNode to keep track of the number of bounces. Also, add
a didBounce() method that increases the counter and removes the node from its
parent on the fourth bounce.
2. Enable contact detection between the label's physics body and the edge of the
screen. To do that you will need to set the contactTestBitMask of MessageNode.
Locate the node body by accessing its node property or by looking for the message
node; for example:
3. Once you grab the node, you can cast it to a MessageNode and call your custom
method that increases its bounce counter. Finally, don't forget to add a contact
mask to your custom label node, so that it produces contact notifications when
it bounces off the scene's edge.
This exercise will get you on the right path to implementing more complicated
contact handlers. Imagine the possibilities—all the custom actions you could make
happen in a game depending on how many times two bodies touch, or how many
bodies of one category touch the edge, and so forth.
raywenderlich.com 260
11 Chapter 11: Advanced Physics
By Marin Todorov
In the last chapter, you saw how easy it is to create responsive game worlds with
Sprite Kit, especially when using the scene editor. By now, you’re a champion of
creating sprites and physics bodies and configuring them to interact under
simulated physics.
But perhaps you’re already thinking in bigger terms. So far, you can move shapes
by letting gravity, forces and impulses affect them. But what if you want to
constrain the movement of shapes with respect to other shapes—for example,
maybe you want to pin a hat to the top of the cat’s head, and have it rotate back
and forth based on physics? Dr. Seuss would be proud!
In this chapter, you’ll learn how to do things like this by adding two new levels to
Cat Nap—three, if you successfully complete the chapter’s challenge! By the time
you’re done, you’ll have brought your knowledge of Sprite Kit physics to an
advanced level and will be able to apply this newfound force in your own apps.
Note: This chapter begins where the previous chapter’s Challenge 1 left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up in the right place.
Specifically, you want to detect whether the cat is leaning to either side by more
than 25 degrees. If he is, you want to wake up the cat, at which point the player
should fail the level.
raywenderlich.com 261
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
To achieve this, you’ll check the position of the cat every frame, but only after the
physics engine does its job. That means you have to understand a bit more about
the Sprite Kit game loop.
Back in the third chapter of this book, you learned that the Sprite Kit game loop
looks something like this:
Now it’s time to introduce the next piece of the game loop: simulating physics.
Here's your new version of the loop:
raywenderlich.com 262
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
After evaluating the sprite actions, but just before rendering the sprites onscreen,
Sprite Kit performs the physics simulation and moves the sprites and bodies
accordingly, represented by the yellow chunk in your new diagram. At that point,
you have a chance to perform any code you might like by implementing
didSimulatePhysics(), represented by the red chunk.
This is the perfect spot to check if the cat is tilting too much!
Note: The new loop also includes didFinishUpdate(). This is a method you can
override if you want do something after all the other processing has beeen
completed.
To write your code to check for the cat's tilt, you’ll use a function from SKTUtils, the
library of helper methods you added to the project in the previous chapter. In
particular, you’ll use a handy method that converts degrees into radians.
Make sure you have CatNap open where you left it off in the previous chapter's
challenge. Then inside GameScene.swift, implement didSimulatePhysics() as
follows:
• Is the game currently playable? You check if playable is true to see if the
raywenderlich.com 263
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
• Is the cat tilted too much? Specifically, is the absolute value of the cat’s
zRotation property more than the radian equivalent of 25 degrees?
When both of these conditions are true, then you call lose() right away, because
obviously the cat is falling over and should wake up immediately!
One more thing to note is that catNode never rotates; this node is contained by the
cat reference and is therefore pinned to its parent node. That's why you need to
keep an eye on the rotation of the cat as a whole by referencing catNode.parent!.
Build and run, and then fail the level on purpose. The cat wakes up while he’s still in
the air, before he even touches the ground.
This is more realistic behavior, as it’s hard to sleep when falling to the ground!
Introducing Level 2
So far, you’ve been working on a game with a single game scene. Cat Nap,
however, is a level-based puzzle game.
In this section of the chapter, you’re going to give the game the ability to present
different levels onscreen, and you’ll add a second level right away. Level 2 will
feature new interactive physics objects, like springs, ropes and hooks.
Except for this game, you’ll call the springs catapults. See what I did there? =]
raywenderlich.com 264
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Fortunately for the cat, this level will have just one catapult. Here’s how the level
will look when you’re finished:
I know, I know. That catapult underneath the cat and the hook on the ceiling look
rather nefarious, but I promise no animals will be harmed in the making of this
game.
To win the level, the player first needs to tap the catapult. This will launch the cat
upward, where the hook will catch and hold him suspended. With the cat safely out
of the way, the player can destroy the blocks. Once the blocks are destroyed, the
player can tap the hook to release the cat, who will then descend safely to his bed
below.
On the other hand, if the player destroys the blocks first and then taps the catapult,
the cat won’t rise high enough to reach the hook, causing the player to lose the
level.
raywenderlich.com 265
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Loading levels
Lucky for you, you're already loading levels in your game.
You have a single level so far, and the level file is named GameScene.sks. You
load it, show it onscreen, and then you implement the game logic in your GameScene
class.
You need to create another .sks file for each of Cat Nap’s levels. Then, you need to
load and display these new levels, one after the next, as the player solves them.
Next, you need to add a factory method on your GameScene class that takes a level
number and creates a scene by loading the corresponding .sks file from the game
bundle.
//1
var currentLevel: Int = 0
//2
class func level(levelNum: Int) -> GameScene? {
let scene = GameScene(fileNamed: "Level\(levelNum)")!
scene.currentLevel = levelNum
scene.scaleMode = .AspectFill
return scene
}
You'll use the currentLevel property to hold the current level’s number. The class
method level(_:) takes in a number and calls GameScene(fileNamed:). If the level
file loads successfully, you set the current level number on the scene and scale
correctly.
Now you need to make a few changes to your view controller. Open
GameViewController.swift and find the following line in viewDidLoad():
Next, open GameScene.swift and locate newGame(). To improve it with the new
factory method, replace the complete method body with this:
view!.presentScene(GameScene.level(currentLevel))
raywenderlich.com 266
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
As soon as you save the file, Xcode opens it in the scene editor. Zoom out until you
see the yellow border:
raywenderlich.com 267
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
With the scene editor skills you already possess from previous chapters, setting up
this scene will be simple—as well as a great way to review what you've learned.
Next, add five color sprite objects to the scene and set their properties as follows:
• Bed: Texture cat_bed.png, Name bed, Position (1024, 272), Custom Class
BedNode
raywenderlich.com 268
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
• Spring: Texture spring.png, Name spring, Position (1024, 588), Body Type
Bounding Rectangle, Category Mask 32, Collision Mask 11, Custom Class
SpringNode
Whoa, that’s a lot of objects! Much of what you’re doing is recreating the elements
from Level 1: a background image, a cat bed, and some blocks.
For this level, you added a sprite for the spring; it uses a new physics category, 32,
and a new custom class, SpringNode. You don't have those yet, but you can quickly
add them right now.
Open GameScene.swift and find the PhysicsCategory struct at the top of the file.
Add a new constant to use for your springs:
Note: For practice, see if you can figure out why you set the category mask to
43 for the two blocks in the level. If you get stuck, open Calculator on your
Mac, switch to Programmer mode and enter in 43 to see the number in binary.
Xcode will automatically open the file. When it does, replace the contents of the file
with this:
import SpriteKit
func didMoveToScene() {
func interact() {
}
}
Look familiar? Your custom node class implements the CustomNodeEvents protocol
and has an empty didMoveToScene() method, where you'll add some code
momentarily. In addition, this class conforms to InteractiveNode, because you want
to let the player tap on the spring node and interact with it.
There's only one key component missing from your new level—the cat!
raywenderlich.com 269
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Give the reference object the following properties, matching the cat's configuration
in Level 1:
Now the game begins with Level 2, rather than Level 1. That's right: Not only does
this make it quicker to test Level 2—now you can cheat in your own game! =]
Build and run the project, and you’ll see the initial setup for your new level:
You’ll see the cat pass straight through the spring. That happens because your
existing code doesn’t know about your new spring objects. You're about to change
that.
raywenderlich.com 270
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Catapults
Since catapults are a new category of objects for your game, you need to tell your
scene how other objects should interact with them. The code for this will be quite
familiar to you, so I won’t spell out all of the details; feel free to move through this
part quickly.
To make the cat sit on top of the catapult, you need to enable collisions between
the cat and the catapult.
parent!.physicsBody!.collisionBitMask = PhysicsCategory.Block |
PhysicsCategory.Edge | PhysicsCategory.Spring
One small change in code; one big step for sleepy cats!
Next, it's time to make the catapult hurl that kitty when the player taps on the
spring sprite. It’s actually quite easy; if the player taps on the catapult, you need to
apply an impulse to the spring, which will then bounce the cat—if, of course, the cat
is on top of the spring.
The first step is to enable user interaction on the spring node so it will react to taps.
raywenderlich.com 271
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
userInteractionEnabled = true
And just like you did for the block nodes, add the respective UIResponder method to
detect taps:
userInteractionEnabled = false
runAction(SKAction.sequence([
SKAction.waitForDuration(1),
SKAction.removeFromParent()
]))
When the player taps a spring node, you apply an impulse to its body using
applyImpulse(_:atPoint:); this is similar to what you did in Chapter 9, "Beginning
Physics", with the sand particles. Because a spring can "jump" only once, you
disable the user input on that node as soon as it receives a tap. Finally, you remove
the catapult after a delay of one second.
Build and run the game again, and this time, tap on the catapult:
Right now, when catapulted, the kitty flips through the air and lands on its head.
That’s why you need to add the ceiling hook to grab him!
The idea is that the catapult will bounce the kitty right onto the hook, which will
hold him while the player clears the blocks. Once the blocks are gone, you'll release
raywenderlich.com 272
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Joints: An overview
To implement the ceiling hook, you need joints. In Sprite Kit, there are five types
of joints available, each of which lets you constrain the positions of two bodies
relative to each other. This section describes them in turn.
Fixed joint
A fixed joint gives you the ability to attach two physics bodies together.
Imagine you have two objects and you nail them together with a few rusty nails. If
you take one of them and throw it, the other one will fly right along with it.
Sometimes, you want to make an object immoveable. The quickest way to do that
is to fix it to the scene's edge loop, and you're ready to go.
Other times, you want a complex object a player can destroy—and perhaps break
into many pieces. If that's the case, simply fix the pieces together, and when the
player hits the object, remove the joints so the pieces fall apart.
Limit joint
You can use a limit joint to set the maximum distance between two physics bodies.
Although the two bodies can move closer to each other, they can never be farther
apart than the distance you specify.
Think of a limit joint as a soft but strong rope that connects two objects. In the
diagram below, the ball is connected to the square via a limit joint; it can bounce
around, but it can never move farther away than the length of the limit joint:
raywenderlich.com 273
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
These types of joints are useful when you want to connect two objects, but let one
move independently of the other within a certain radius—like a dog on a leash!
Spring joint
A spring joint is much like a limit joint, but the connection behaves more like a
rubber band: it's elastic and springy.
Like a limit joint, a spring joint is useful for simulating rope connections, especially
ropes made of elastic. If you have a bungee-jumping hero, the spring joint will be
of great help!
Pin joint
A pin joint fixes two physics bodies around a certain point, the anchor of the joint.
Both bodies can rotate freely around the anchor point—if they don’t collide, of
course.
Think of the pin joint as a big screw that keeps two objects tightly together, but still
allows them to rotate:
raywenderlich.com 274
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
If you were to build a clock, you’d use a pin joint to fix the hands to the dial; if you
were to build a physics body for an airplane, you’d use a pin joint to attach the
propeller to the plane’s nose.
Sliding joint
A sliding joint fixes two physics bodies on an axis along which they can freely slide;
you can further define the minimum and maximum distances the two bodies can be
from each other while sliding along the axis.
The two connected bodies behave as though they're moving on a rail with limits on
the distance between them:
A sliding joint might come in handy if you're building a roller coaster game and you
needed the two car objects to stay on the track, but to keep some distance from
one another.
Note: It's possible to apply more than one joint to a physics body. For
example, you could use one pin joint to attach an hour hand to a clock face,
and add a second pin joint to connect the minute hand to the clock face.
Joints in use
The easiest way to learn how to use joints is to try them out for yourself. And what
better way to do that than by creating the hook object and attaching it to the
ceiling.
raywenderlich.com 275
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
There's one body fixed to the ceiling (the base), another body for the hook, and a
third body that connects them together (the rope).
• A spring joint to connect the hook to the base. The spring joint will be your
rope.
hookBase is the node to which the hook and its rope will be attached. The base
itself is going to be fixed on the ceiling by a joint to the scene edge body.
You might wonder what purpose the wood block serves. It doesn't even have a
physics body! Take a look at the playable area on an iPhone and an iPad:
raywenderlich.com 276
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
On a 4” iPhone, the scene cuts out somewhere just before the top edge of the hook
base—it looks like the hook base is built into the ceiling. But remember that an
iPad's screen aspect ratio is different, which is why on the iPad, you can see more
of the scene. If it weren’t for the wood piece on top, it would look like the hook
base were just floating in midair.
So the wood block's only role is to make things look nice. On an iPhone, it’s outside
of the screen bounds, so the player won't even see it.
Perhaps you've noticed that the hook has a custom node class; you're going to add
that to the project now.
Create a new file by choosing File/New/File..., and select the file template iOS/
Source/Cocoa Touch Class. Name the new class HookNode, make it a subclass
of SKSpriteNode and click Next and then Create. Xcode will automatically open
the file once it's created.
To clear the error you see in Xcode by default, replace import UIKit with:
import SpriteKit
HookNode is a little different than the other custom nodes you've created so far.
Since the hook is a compound object made of a base, a swinging rope and the hook
itself, you'll use only one custom class for the whole structure.
You're also going to create the rope and the hook in code rather than in the scene
editor.
First, you need a way to access the parts of the hook, so add the following
properties in HookNode.swift:
The first two properties are the nodes you need in order to finish building the hook
object, and hookJoint is something you'll use later when the kitty bounces and is
"hooked" by the rope. Finally, there's a dynamic property named isHooked; this
checks if there's already a stored physics joint in hookJoint.
Next, make your new class conform to the CustomNodeEvents protocol by adding the
declaration to the class line:
raywenderlich.com 277
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
func didMoveToScene() {
guard let scene = scene else {
return
}
}
You check to see if the node has already been added to the scene; if so, you bail
out.
Now in didMoveToScene(), you're going to configure and add hookNode and ropeNode.
Then, you're going to finish constructing the hook. You'll do this in a few steps.
First, add the following, after the guard statement but not within it:
You always specify the anchor point in scene coordinates. When you attach a body
to the scene’s body, you can safely pass any value as the anchor point, so you use
(0, 0). Also note that you must have already added the sprites as children of the
scene before you can create the joint.
Finally, you add the joint to the scene’s physics world. Now, these two bodies are
connected until the end of time—or until you remove the joint.
Note: When you create a fixed joint, the two bodies don’t need to be touching
—each body simply maintains its relative position from the anchor point.
In addition, you could get this same behavior without using a joint at all.
Instead, you could make the physics body static, either by unchecking the
Dynamic field in the scene editor or by setting the dynamic property of the
physicsBody to false. This would be more efficient for the physics engine, but
you’re using a fixed joint here for learning purposes.
Build and run the game, and you’ll see the hook base fixed securely to the top of
the screen:
raywenderlich.com 278
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Note: If you don't see it, try running the game on an iPad or iPad Simulator.
Look at that line going from the bottom-left corner to the top of the scene—this is
the debug physics drawing representing the joint you just created. It indicates
there's a joint connecting the scene itself—with a position of (0, 0)—to the hook.
Since you're on a roll, add the sprite for the rope, too.
This positions the rope just under the hook base, sets its anchor point to be its top
—since it's going to swing like a pendulum—and aligns it so it points down.
Now it's time to add the hook itself, which will use a different type of joint.
hookNode.position = CGPoint(
x: position.x,
y: position.y - ropeNode.size.width )
hookNode.physicsBody =
SKPhysicsBody(circleOfRadius: hookNode.size.width/2)
hookNode.physicsBody!.categoryBitMask = PhysicsCategory.Hook
raywenderlich.com 279
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
hookNode.physicsBody!.contactTestBitMask = PhysicsCategory.Cat
hookNode.physicsBody!.collisionBitMask = PhysicsCategory.None
scene.addChild(hookNode)
This creates a sprite, sets its position and creates a physics body for it. It also sets
the category bitmask to PhysicsCategory.Hook, and sets the contactTestBitMask
such that it detects contacts between the hook and the cat.
The hook doesn’t need to collide with any other objects. Later, you'll implement
some custom behavior for it that default Sprite Kit physics doesn’t provide.
Note: You position the hook sprite just under the ceiling base, because the
distance between the base and the hook is precisely the length of the rope
that will hold them together.
Now, create a spring joint to connect the hook and its ceiling holder by adding the
following to didMoveToScene():
let ropeJoint =
SKPhysicsJointSpring.jointWithBodyA(physicsBody!,
bodyB: hookNode.physicsBody!,
anchorA: position,
anchorB: hookPosition)
scene.physicsWorld.addJoint(ropeJoint)
First, you calculate the position of the hookNode where the joint should attach itself
and store that position in hookPosition.
Then, using a factory method similar to the one you used for the ceiling joint, you
connect the hookNode body and the hook's base body with a spring joint. You also
specify the precise points in the scene’s coordinate system where the rope connects
to the two bodies.
Build and run the game now. If all's well, you’ll see your hook hanging in the air
just under the ceiling:
raywenderlich.com 280
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
The rope joint works fine, but that rope doesn't move.
As you can see below, after Sprite Kit finishes simulating physics, it performs one
last step: applying something named constraints to the scene and notifying your
scene that this has happened.
Constraints: An overview
Constraints are a handy Sprite Kit feature that let you easily ensure certain
relationships are true regarding the position and rotation of sprites within your
game.
raywenderlich.com 281
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
• Create the constraint. This example creates a constraint that limits z rotation
from -45° to 45°, and applies it to the cat node.
• Add the constraint to a node. To add the constraint to a node, simply add it to
the constraints array on the node.
Note: To type π in your source code, press and hold the Alt key on your
keyboard and press P. π is a constant defined in SKTUtils; you can press
Command and click on it in Xcode to jump to its definition. If you prefer, you
can use CGFloat(M_PI) instead.
After Sprite Kit finishes simulating the physics, it runs through each node’s
constraints and updates the position and rotation of the node so that the constraint
is met.
As you can see, even though the physics simulation sometimes determines that the
cat should fall over beyond 45°, during the constraint phase of the game loop,
Sprite Kit updates the cat’s rotation to 45° so the constraint remains true.
//let rotationConstraint =
// SKConstraint.zRotation(
// SKRange(lowerLimit: -π/4, upperLimit: π/4))
//catNode.parent!.constraints = [rotationConstraint]
raywenderlich.com 282
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
• Make a turret in your game point in the direction in which it’s shooting;
After this crash course in constraints, you’re ready to use a real constraint in your
Cat Nap game. Specifically, you'll finish the hook object by adding the final piece:
the rope constraint.
You can also provide an offset range to the constraint if you don’t want to orient the
node with perfect precision. Since you want the end of the rope and the hook to be
tightly connected, you provide a zero range for the constraint.
Finally, you set the orientConstraint as the sole constraint for the ropeNode.
One last step. There’s nothing that moves the hook—which makes it look kind of
odd. Add this line to the end of didMoveToScene():
This applies an impulse to the hook node so that it swings from its base.
Build and run the game again. Check out your moving, customized level object:
raywenderlich.com 283
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Remember, there are two things going on here that make this behavior work:
• You set up a joint connecting the hook to the base. This makes it so the hook is
always a certain distance from the base, so it appears to “swing” from the base.
• You set up a constraint that makes the rope always orient itself toward the hook
so that it appears to follow the hook.
There’s one thing I should mention to keep my conscience clear: Right now, the
rope is represented by just one sprite. That means your rope is more like a rod.
If you’d like to create an object that better resembles a rope, you could create
several shorter sprite segments and connect them to each other, like so:
raywenderlich.com 284
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
In this case, you might want to create physics bodies for each rope segment and
connect them to each other with pin joints.
More constraints
So far, you've seen examples of the zRotation() and orientToNode(_:offset:)
constraints.
There are a number of other types of constraints you can create in Sprite Kit
beyond these. Here’s a reference of what's available:
• positionX(), positionY() and positionX(y:): These let you restrict the position
of a sprite to within a certain range on the x-axis, y-axis or both. For example,
you could use these to restrict the movement of a sprite to be within a certain
rectangle on the screen.
raywenderlich.com 285
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
contacting bodies are the hook and the cat. You also need to do an additional check
to see if they're already hooked together.
To do these checks, you need an outlet to the hook node. Add a new property to
GameScene.swift:
Next, you need to look through the scene hierarchy to find out if there's a hook
node. Don't forget, the other levels don't have a hook, and that's why you need an
Optional value for that property.
That line should be familiar; you simply locate the node by name and assign it to
the property you created earlier.
Now scroll back to didBeginContact() and add the check for a hook-to-cat contact:
You check if the collision bitmask matches the cat and hook categories, and that the
two aren't hooked together already. If both are true, you call hookCat(_:) on the
hook node. Of course, this method doesn't exist yet, so you'll get an error, but
that's an easy problem to fix.
First, you manually alter the physics simulation by forcing a velocity and angular
velocity of zero on the cat’s body. You do this to calm things down when the cat's
about to get hooked; you don’t want to make players wait too long before their
next moves while they wait for the hook to stop swinging.
The important point here is that you can manually alter the physics simulation. How
great is that? Don’t be afraid to do this—making the game fun is your top priority.
raywenderlich.com 286
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
hookJoint = SKPhysicsJointFixed.jointWithBodyA(hookNode.physicsBody!,
bodyB: catNode.parent!.physicsBody!, anchor: pinPoint)
scene!.physicsWorld.addJoint(hookJoint)
hookNode.physicsBody!.contactTestBitMask = PhysicsCategory.None
With that block of code, you calculate the position where the two bodies will be
fixed together. Using this position, you create a new SKPhysicsJointFixed to
connect the hook and cat, and then you add it to the world.
You also need to make didSimulatePhysics() respect the fact that the kitty is
“hooked”, so that the game is OK with rotating to angles more than the margin of
45 degrees.
Now when the cat swings on the hook, he won’t wake up.
When the cat hangs from the ceiling, you can safely destroy the blocks over the cat
bed. But you’re still missing one thing—the cat needs to fall off the hook and into
the bed. To make that happen, you need to remove the joint you just added.
This time, add the method to destroy the joint before you call it. Open
HookNode.swift and add the following:
raywenderlich.com 287
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
func releaseCat() {
hookNode.physicsBody!.categoryBitMask = PhysicsCategory.None
hookNode.physicsBody!.contactTestBitMask = PhysicsCategory.None
hookJoint.bodyA.node!.zRotation = 0
hookJoint.bodyB.node!.zRotation = 0
scene!.physicsWorld.removeJoint(hookJoint)
hookJoint = nil
}
In this method, you simply undo what you did in hookCat(_:). You remove the joint
connecting the hook and the cat. Then, you straighten up the cat and the hook.
Next, you'll add the code to let the cat fall off the hook. In the game, the player will
need to tap the cat while it swings in midair.
Here you encounter a new problem: The player needs to tap on the cat node, but
the hook node needs to react to this action. You don't want to couple the hook and
the cat by making them know about each other in some way. You want to keep
nodes de-coupled, since different levels feature different node combinations.
To keep the nodes independent from each other, you'll use notifications.
Each time the player taps on the cat, it will send a notification, and nodes that are
interested in this event will have the chance to catch the broadcast and react in
whatever way is necessary.
First, you're going to handle taps on the cat node. Open CatNode.swift and add
the following to didMoveToScene():
userInteractionEnabled = true
Just like before, you need to make this class adhere to InteractiveNode.
As soon as you do that, Xcode will complain about missing the required protocol
method, so add a stub for now:
func interact() {
}
Then, outside of the class body and just under import SpriteKit, define a new
notification name:
raywenderlich.com 288
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Each time the player taps on the cat, a notification needs to be sent via
NSNotificationCenter. To make that happen, add the following line of code inside
interact():
NSNotificationCenter.defaultCenter().postNotificationName(
kCatTappedNotification, object: nil)
Note: This time around, you don't disable user interactions inside interact().
Since other nodes might implement custom logic based on taps on the cat,
you can't speculate whether or not further touches on that node would be of
interest. To be safe, you keep accepting touches and broadcasting the same
notification over and over again.
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "catTapped", name: kCatTappedNotification, object: nil)
func catTapped() {
if isHooked {
releaseCat()
}
}
In catTapped(), you simply check if the cat is currently hanging from the hook, and
in that case, you call releaseCat to let it go. That's all!
Build and run the game again, and try to land the cat on the bed. You probably
don’t need this advice, but: tap on the catapult, tap on all the blocks, and then tap
the cat to solve the level.
raywenderlich.com 289
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Creating joints dynamically is a fun and powerful technique. I hope to see you use
it a lot in your own games!
Compound shapes
It’s time to move on to the third level of Cat Nap.
In this level, you’ll tackle another game physics concept, one related to body
complexity. With this in mind, have a look at the completed Level 3 and try to guess
what’s new compared to the previous levels:
You guessed right if you said that in Level 3, one of the blocks has a more
complicated shape than your average wooden block.
Perhaps you also noticed that the shape is broken into two sub-shapes. Maybe you
even wondered why it was done that way instead of being constructed as a polygon
raywenderlich.com 290
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Sometimes in games, for reasons related to game logic, you need an object that’s
more complex than a single image with a physics body. To better understand the
problem, let's go back in time, I mean - back in chapters.
Do you remember your old friend the zombie from the Zombie Conga minigame?
In a physics based game a zombie would have quite a complex shape and you
might be temped to use a single texture for it. But if you make the zombie body-
parts separate nodes with separate physics bodies you could make him wave his
hands, move his legs, etc.
Also - everyone knows that when zombies don't keep a strict diet of fresh brains
they start loosing limbs. You have surely seen your green friends drop an arm or a
jaw on the way while they chase the hero in the latest Hollywood horror movie.
For example if you use separate nodes for the zombie arms you could simply
"detach" them during any point in your game for an added comical effect.
Having said that, in this section you'll build a simple compound body for the next
level of Cat Nap.
Creating the third level of Cat Nap in the scene editor will follow much the same
process as for Level 2.
Select the Scenes group in the project navigator. From Xcode’s menu, select File/
New/File... and then iOS/Resource/SpriteKit Scene. Click Next, save the file
as Level3.sks, and click Create.
You'll be rewarded, as usual, with the sight of an empty game scene. Sorry, no
flashing lights, screaming fans or bells and whistles. But hey, maybe they'll add that
in a future release. =]
OK, first, resize the scene to 2048x1536 points. Then, add the following color sprite
objects to the empty scene:
raywenderlich.com 291
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
• Bed: Texture cat_bed.png, Name bed, Position (1024, 272), Custom Class
BedNode
Finally, drag in a reference from the Object Library. Give the reference object the
following properties:
Build and run the game to see what you have so far:
The level in its current state doesn’t look very good. The cat falls through the stone
L-shaped block as if the block weren't in the scene at all. And no wonder—you didn't
create any bodies for the two blocks used to build the L-shape. In the next section,
you'll learn how to create a complex body that matches the shape of the new stone
raywenderlich.com 292
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
block.
You'll develop a custom class called StoneNode. When you add it to the scene, it will
search for all stone pieces, remove them from the scene and create a new
compound node to hold them all together. As you can see, when it comes to
creating custom node behavior, the sky's the limit!
To clear the error you see in Xcode, replace the default contents with the following:
import SpriteKit
func didMoveToScene() {
func interact() {
}
}
This code looks familiar by now: You create a custom SKSpriteNode subclass and
adapt your CustomNodeEvents protocol.
Next, add the method that will look through the scene nodes and bind together all
the stone pieces. Make it a static method:
You initialize an empty StoneNode object to hold your stone pieces, and give it a
zPosition of -1 to make sure the empty node doesn't "stay" in front of some other
nodes.
Note: Don't worry about the error. That will disappear after you finishing
adding the rest of the code.
raywenderlich.com 293
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Next, find all the stone pieces and remove them from the scene. Then, add each
one to compound, instead:
You filter the scene child nodes, taking only the ones of type StoneNode. Then, you
simply move them from their current places into the hierarchy of the compound
node.
Next, you need to create physics bodies for each of these pieces. You'll just loop
over all the stone nodes now contained in compound node and create a physics body
for each one:
With this code, you store all the bodies in the bodies array, because in the next bit
of code, you'll be supplying them to the initializer of SKPhysicsBody(bodies:) and
creating a compound physics body out of all the pieces.
return compound
SKPhysicsBody(bodies:) takes all of the bodies you provide and binds them
together; you set the result as the body of the compound node. Finally, you set the
collision bitmask of the stone node so that it collides with the cat, the other blocks
and the edge of the screen.
Before returning the ready-for-use compound node, you enable user interactions on
it. That way, the player can't tap on the separate pieces; only the compound node
will accept taps.
Note: Now that you're returning a valid object, the error should be resolved.
raywenderlich.com 294
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
All that's left to do is call the new method from didMoveToScene(), so do that now:
if parent == levelScene {
levelScene!.addChild(StoneNode.makeCompoundNode(inScene: levelScene!))
}
For each node, you check if its parent is levelScene. If it is, that means the node
hasn't been moved to the compound node, in which case you call
makeCompoundNode(inScene:).
Since you're removing the stone nodes from their parents before adding them to
compound within makeCompoundNode(inScene:), they lose their link to the game scene.
That's why, before you start modifying the nodes, you store a pointer to the current
scene object in levelScene and use that variable until the end of the method.
Build and run the game, and behold the coveted L-shaped stone in all its compound
glory!
Destroy one of the wooden blocks on the left to see the two stone pieces now
behave as one solid body:
If you try to solve the level, it won't work; that's because you haven't added
interactivity to the stone blocks yet.
raywenderlich.com 295
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
userInteractionEnabled = false
runAction( SKAction.sequence([
SKAction.playSoundFileNamed("pop.mp3", waitForCompletion: false),
SKAction.removeFromParent()
]))
No matter which of the pieces the player taps, you remove all of the pieces by
removing the node that holds them.
Build and run the game again. This time, you'll be able to solve the level.
Level progression
Until this point, you've worked on one level at a time, so you’ve manually specified
which level to load. However, that won’t work for players—they expect to proceed
to the next level after winning!
This is quite easy to implement. Begin by setting the game to load Level 1. Change
the line that loads the scene in GameViewController.swift so it looks like this:
Then, in GameScene.swift, add the following code at the beginning of win(). Make
sure you add it before all of the other code:
raywenderlich.com 296
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
if (currentLevel < 3) {
currentLevel++
}
Now, every time the player completes a level, the game will move on to the next
one.
if (currentLevel > 1) {
currentLevel--
}
That’ll certainly make the player think twice before tapping a block!
Congratulations - you now have three unique levels for Cat Nap. You've learned how
to use the scene editor and how to create custom classes for your nodes. You've
even learned how to implement custom behavior. From here on out, you're ready to
start working on your own level-based game. The principles you learned in the last
four chapters remain the same in any physics game.
There's one more chapter with Cat Nap to go, where you'll be learning about some
more advanced types of nodes you can use in your game. But before you move on -
try your hand at the challenge below!
Challenges
By now, you’ve come a long way toward mastering physics in Sprite Kit.
And because I’m so confident in your skills, I’ve prepared a challenge for you that
will require a solid understanding of everything you’ve done in the last three
chapters—and will ask even more of you.
raywenderlich.com 297
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
As you can see, this time, besides blocks, there’s a seesaw between the poor cat
and its bed. This cat sure has a hard life.
And yes, it’s a real seesaw; it rotates about its center and is fully interactive. And
you developed it all by yourself! Er, sorry—you will develop it all by yourself. See, I
have confidence in you. :]
I’ll lay down the main points and you can take it from there.
• Bed: Texture cat_bed.png, Name bed, Position (1024, 272), Custom Class
BedNode
• Seesaw: Texture ice.png, Name seesaw, Position (518, 440), Body type
Bounding Rectangle, Category Mask 2, Collision Mask 11
raywenderlich.com 298
2D iOS & tvOS Games by Tutorials Chapter 11: Advanced Physics
Once you’ve placed these objects, the scene should look something like this:
There’s not much left to do from here—I’ll assume you’ve done everything perfectly
so far!
You’ll need to fix the seesaw board to its base on the wall. Do that by creating a pin
joint that will anchor the center of the board to the center of the base and let the
board rotate around that anchor, like so:
You can create a pin joint in two ways. First, create it in code using
SKPhysicsJointPin.jointWithBodyA(bodyB:anchor:) to make sure you understand
how that works. You’ll need to find the node by name and use it to create the joint.
After that, you can remove that code and do the much simpler thing: Check the
Pinned checkbox in the scene editor, in the seesaw’s Physics Definition section.
This will create a pin joint that connects the node to the scene at the node’s anchor
point.
That's it. Try solving the level yourself. I’m not going to give you any other tips
besides the fact that the order in which you destroy the blocks matters.
raywenderlich.com 299
12 Chapter 12: Crop, Video, and
Shape Nodes
By Marin Todorov
In the very first chapter of this book, you learned that all of the visual elements in
your game scene are nodes. You used a number of different nodes by creating
instances of classes that inherit from SKNode.
Whether you created a node from code, or added them using the scene editor, they
all inherited from the SKNode base class.
• SKNode: An empty node that doesn't draw anything on the screen; use it to
group other nodes by adding them as children to this node.
• SKScene: This node represents a single screen or a level in your game; add all
your level nodes directly or indirectly to this node.
• SKLabelNode: From the game score to the player's remaining lives to any in-
game message—it's all done with this node.
• SKSpriteNode: You've been using this node quite often; it displays an image or
a sequence of images onscreen via an action. This is generally the node you use
the most when designing a scene.
In this chapter, you'll continue working on Cat Nap, and in the process, you'll learn
about three advanced types of nodes:
• SKCropNode: This node lets you mask the contents of a node, including its child
nodes. This comes in handy when you want to crop out part of a texture.
• SKVideoNode: This node lets you include videos in your games. As you've
probably experienced, developers often use videos to create richer gameplay.
• SKShapeNode: This node lets you draw shapes onscreen. You can draw shapes
of different complexities, from rectangles and circles to any arbitrary shape.
If these three nodes have anything in common, it's that they all let you add unique
raywenderlich.com 300
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
effects to your games. These nodes might not seem like much at first, but by the
end of this chapter, you'll be able to create amazing and advanced things using
them.
Make no mistake—advanced does not necessarily mean difficult. You'll probably find
this chapter easier than you expect, and when you're done, you just might have the
urge to celebrate with some disco moves.
Without further ado, it's time to add two more levels to Cat Nap.
Note: This chapter begins where the previous chapter’s Challenge 1 left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—you can simply open the starter project from this
chapter to pick up in the right place.
Getting started
Open the project in Xcode. Then, open the Assets.xcassets catalogue.
To work through this chapter, you'll need some extra resources. Look in the starter
\resources folder for this chapter and drag the textures folder into your project's
Assets.xcassets catalogue. Included in this folder are additional textures you'll
need to build this chapter's levels.
raywenderlich.com 301
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Then, drag the media folder into your project—not into the assets catalogue, but
into the project navigator. This folder contains a video that you'll use in this
section to spice up one of the new levels and the audio track to go along with it.
Since you're already a pro with the scene editor, I won't ask you to create the last
two levels of Cat Nap from scratch. Instead, open the levels folder inside
Resources and drag Level5.sks and Level6.sks into your project.
To let the player progress to these new levels, open GameScene.swift and inside
win(), change if (currentLevel < 4) { to this:
if (currentLevel < 6) {
With that, you've completed the project setup and you can lay your hands on
SKCropNode to do some cutouts. Snip snip!
Crop nodes
In previous chapters, you used SKSpriteNode to show textures onscreen. For
example, if you wanted to display a picture of the main character from this book's
first mini-game, Zombie Conga, you could easily do that by creating a new
SKSpriteNode and adding it to your scene like so (don't add this code; this is just an
example):
raywenderlich.com 302
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
But what if you wanted to create a node that's not rectangular in shape? So far,
you've used only rectangular textures; when you needed a different shape, you
simply used images with transparent backgrounds.
Using transparency works most of the time, but if you're creating a more advanced
game, you'll eventually need to take a normal rectangular texture and cut out an
arbitrary piece of it to use in your scene.
To crop the contents of a texture, you can apply a mask and cut out only the
content you want to keep:
Imagine using a cookie cutter: You press the cutter onto the dough, and it cuts out
a piece of the dough in the shape of the cutter. And that's your cookie!
To use SKCropNode to achieve a similar effect, you follow four easy steps:
1. Create a new SKCropNode. This node doesn't display anything by default; it's
only a container node.
2. Add one or more child nodes of any type you like to the crop node: labels,
sprite nodes and so forth.
3. Set the crop mask. The mask is also a node and can be any type, such as a
sprite node or a label. The mask node's contents should have transparent and
opaque areas; opaque areas will preserve their content, while transparent areas
will be masked—that is, hidden or cut out.
4. Add the crop node to the scene. Like any other node, you need to add the
crop node to the scene to see its contents.
raywenderlich.com 303
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
In this example, a crop node has two child nodes: a sprite node and a label. When
you apply a circular mask to the crop, you cut out all of the children nodes' content,
as well.
With that in mind, you're good to start working on a very special part of Level 5: a
dusty old picture of a zombie who likes the conga!
You can play around a bit, but here's the bad news: You can't solve the level as-is.
You need to add some extra cool crop nodes to help the kitty get to his bed.
raywenderlich.com 304
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Note: The black blocks are just like normal blocks, except they're
indestructible; no matter how many times you tap them, they'll remain
unharmed.
The new element in this level is obviously the picture frame on the wall. Open
Level5.sks and select the picture frame node. Switch to the Custom Class
Inspector, and you'll see the node is already configured to be an instance of
PictureNode.
import SpriteKit
func didMoveToScene() {
userInteractionEnabled = true
func interact() {
}
}
This code enables touches on the picture node and gets you ready to add more
custom content. At the moment, PictureNode only displays a hollow picture frame.
Next, you're going to load the zombie picture and crop it to a circle so it will fit in
the frame.
raywenderlich.com 305
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Next, you need to add the crop node. Add these lines:
You create an empty crop node and add pictureNode as a child. Then, you set your
prepared maskNode to be the mask of the crop node. Finally, you add the configured
crop node to the picture frame node—the one that came from the .sks file and
contains all the content.
Build and run the game. You'll see the zombie picture cropped neatly inside the
picture frame:
To see exactly what the crop node does, comment out the line cropNode.maskNode =
maskNode.
This removes the mask so you can see the whole, uncropped content:
Now uncomment the line where you set the mask, and you'll have the zombie
picture cropped once again and neatly tucked inside the picture frame.
Look more carefully at the picture: It features a green outline denoting a physics
body. Switch back to the scene editor and look up the physics body settings of the
picture, and you'll notice two things:
raywenderlich.com 306
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
• The picture has a circular body that matches the shape of the picture frame.
Specifically, the body is a bounding circle.
• The body isn't dynamic, because the Dynamic option is unchecked. This means
the body isn't affected by the physics world—gravity doesn't pull it down, and
when other bodies hit it, it doesn't move.
To finish up the level, you'll add just a bit of interactivity to this node. When the
player taps on the picture frame, you'll set its physics body to be dynamic. Since
the picture isn't pinned to the wall, it will fall down under the pull of gravity and
help the player solve the level.
userInteractionEnabled = false
physicsBody!.dynamic = true
Whenever the player taps the picture, you simply set the dynamic property on its
body to true.
Build and run the game. See if you can get kitty to his bed. Also, check out what
happens when you tap the picture.
raywenderlich.com 307
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Note: If you have difficulty solving the level, here's a hint: destroy all of the
wooden blocks, then tap the picture; when the picture pushes kitty over the
center of his bed, tap the long white block.
Crop nodes are straightforward: They let you cut out any shape you want from any
texture or node. But you don't have to limit yourself to using images for the mask
node. For example, you can use an SKLabelNode as a mask to produce some neat
looking text. Here's what happens when you crop the level's background image,
specifically the room's wallpaper, using a label node for a mask:
But enough about cutting and cropping—it's time to move on to the next stop on
your advanced nodes tour: SKVideoNode.
Video nodes
Videos are everywhere in games—from a lively scene background to cut-off scenes.
A video can give your game a number of advantages. For example:
• You can save GPU power. Imagine you have a game taking place at the bottom
of the ocean; in the background, you have 200 different fishes swimming happily
about. You can create 200 sprite nodes and run 200 repeating actions on them to
create the scene, but that would take a toll on the device's GPU and your game's
frame rate. If you pre-record the scene and play the video as a background in
your game, you can increase your game's performance.
• Finally, you can include content that is simply a movie. :] Is your player in
command of hundreds of tiny 2D space ships? Then put on a hat and a fake
mustache, become Space Admiral Zblorg and use your iPhone to record a video
of the player's pep talk for the next level.
I'm sure you don't need any more convincing to get started with SKVideoNode or the
next level of Cat Nap.
raywenderlich.com 308
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Build and run the game to see this level's starting point:
Since you're familiar with Cat Nap's game mechanics, you'll quickly notice that you
don't have a way to solve this level as it is right now.
There are a bunch of wooden pieces lying around, but the kitty is way off the center
of the screen; no matter the order in which you destroy the wooden blocks, kitty is
just too far from his bed to make it in.
I bet you've also noticed a new element on the screen: a big, shiny disco ball! In
this level, you're going to turn the quiet bedroom into a disco each time the player
taps the ball.
While the music and lights are on, some special gameplay will take effect. When the
player taps the kitty, he'll start to "dance". Each time the kitty dances, he'll end up
a bit to the right of his original position.
If the player repeats this routine of tapping the disco ball, the kitty will eventually
find himself in the center of the screen. From there, getting into his bed will be as
easy as tapping a couple of blocks.
If you look around the level file in the scene editor, you'll see that the custom class
for the ball sprite is already set to DiscoBallNode.
So, what are you waiting for? You know the drill—create that class. :]
Create a new file by choosing File/New/File... and selecting iOS/Source/Cocoa
raywenderlich.com 309
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Touch Class for the file template. Name the new class DiscoBallNode, make it a
subclass of SKSpriteNode and click Create.
Xcode will automatically open the file. Once it does, replace the contents of the file
with the following:
import SpriteKit
import AVFoundation
func didMoveToScene() {
userInteractionEnabled = true
func interact() {
}
}
You lay down the class basics by inheriting from SKSpriteNode and implementing the
CustomNodeEvents protocol, just as before. This time, though, you have two class
variables, as well:
• player: This is the AVPlayer class from the AVFoundation framework. It helps you
load local or remote video files for playback. AVPlayer doesn't render the video
itself; it only loads and manages the video file.
• video: This is the node responsible for displaying onscreen the video file you
load via AVPlayer.
SKVideoNode can automatically load the video file without the help of a separate
raywenderlich.com 310
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
AVPlayer object, but in that case, you can only start and stop the video, and you
lose fine-grained control of playback, such as seeking through and looping the
video.
To instantaneously display the video, you'll load the file and have the video node
ready when the game loads the level. Initially, you'll have the video node hidden,
but when the player taps on the ball, you'll unhide the node and have the video
play instantly.
It's time to get this show on the road, so to speak. Add the following to
didMoveToScene():
The first line gets the URL to the discolights-loop.mov video file included in your
project. You use that URL to create a new AVPlayer instance. Then,
SKVideoNode(AVPLayer:) creates a video node that uses the aforementioned
AVPlayer object.
Note: Did you notice that you didn't have to specify the video format codec
the player needs in order to reproduce your file? AVPlayer is really magical in
the way it deals with videos. It looks up the video file meta information and
decides on its own how to load and decode the video content. Besides being
magical, AVPlayer loads and plays any MPEG4 (.mp4), Apple MPEG4 (.m4v) or
QuickTime (.mov) file.
Video playback
Since you've loaded the video and initialized your video node, all that's left to do is
position and size the node, and, of course, add it to the scene.
video.size = scene!.size
video.position = CGPoint(
x: CGRectGetMidX(scene!.frame),
y: CGRectGetMidY(scene!.frame))
video.zPosition = -1
scene!.addChild(video)
With this code, you make the video as big as the scene and then center it so it fills
the whole screen. Further, you set its zPosition so that the video appears behind
the cat, the bed and the blocks, but in front of the original background.
raywenderlich.com 311
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Note: Due to a bug with the simulator, you have to test on your device until
the end of this chapter. If you use the simulator, you won't see any of the
video content, though you might hear the audio, if your video has audio. From
here on out, build and run on your device.
However, there's a slight problem with the video right now—it plays for few
seconds, then stops as it reaches the end of the file.
You need to make the video loop, which happens to be a rather simple task.
When AVPlayer reaches the end of the video file, it posts a certain notification.
You'll listen for that notification and simply rewind the video tape to its start
position, and continue the playback from there. And by video tape, I mean the
video file. :]
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "didReachEndOfVideo",
name: AVPlayerItemDidPlayToEndTimeNotification, object: nil)
Now, you need to add a didReachEndOfVideo method to react to the playback having
reached the end of the video.
raywenderlich.com 312
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
func didReachEndOfVideo() {
print("rewind!")
player.currentItem!.seekToTime(kCMTimeZero)
}
Using seekToTime(_:), it's possible to move the current position in the video to a
given time offset. The handy kCMTimeZero constant tells the player item to seek to
the beginning of the file.
Additionally, just to make the looping more obvious for this demo, you print a
message in the console whenever you jump from the end of the video to the
beginning of the video.
Build and run the game on your device. Enjoy the glorious disco lights video on
loop.
raywenderlich.com 313
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Note: Isn't that video cool? I made it especially for this chapter's project. I
built a small app using Core Animation and my favorite layer,
CAReplicatorLayer, and then captured the video of the animation from my
screen.
If you want to learn how to create cool animations with Core Animation, check
out my book about animations, iOS Animation by Tutorials:
http://www.raywenderlich.com/store/ios-animations-by-tutorials
The previous screenshot doesn't do much justice to the video, but the output in the
console clearly shows that the video loops and plays continuously:
Now, to make the video blend a little better, set its opacity to 75% by adding the
following line to didMoveToScene():
video.alpha = 0.75
The change in opacity makes for a more subtle effect that fits the scene perfectly.
video.hidden = true
video.pause()
This will hide the video node as well as pause it, since there's no reason to keep
AVPlayer busy while the video isn't visible.
Next, you need to add the code to react to touches on the disco ball.
Since the disco ball will work for just a few seconds at a time, you'll need to
implement a time-out to turn it off again. The easiest way is to add a new instance
property—call it isDiscoTime—and show or hide the video whenever the value of
that property changes.
raywenderlich.com 314
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
}
}
Any time you change the value of isDiscoTime, the didSet handler will show or hide
the video node accordingly.
Now you can override touchesEnded(_:withEvent:) in your disco ball class, and
simply toggle the value of isDiscoTime. Add this to DiscoBallNode:
if !isDiscoTime {
isDiscoTime = true
}
Build and run the game, and tap the disco ball to make sure everything works as
planned:
When you tap the disco ball, the video appears and it's ready to play. Now, you just
need to play the video and add a little extra "something" to the disco ball.
First, add a new property to keep a frame animation for your disco ball:
spinAction is a frame animation action that continuously loops over the discoball1,
raywenderlich.com 315
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
discoball2, and discoball3 images. That should get you a nice and shiny rotating
disco ball animation.
if isDiscoTime {
video.play()
runAction(spinAction)
} else {
video.pause()
removeAllActions()
}
This bit of code plays or pauses the video depending upon whether the player turns
the disco music on or off. Notice the use of the play() and pause() methods from
SKVideoNode; these handy methods control the underlaying AVPlayer and are, in
fact, the only playback methods the node offers.
Now you have the scene and mood all set - you show the disco lights video, the cat
gets dancing but the player keeps listening to the mellow Cat Nap tune. You are
about to fix that pretty quickly :]
In the beginning of this chapter you added two new files to your project - the video
of the disco lights and an audio file called disco-sound.m4a. Why not just have the
audio track contained in the video you might ask? Well - the video is just couple of
seconds long and it loops a number of times when you play it in the game. The
audio track is longer so to keep both files smaller in size you will play it manually
alongside the video.
You will just replace the background music with the disco tune whenever you start
the video and then bring back the default music whenever disco time is over.
SKTAudio.sharedInstance().playBackgroundMusic(
isDiscoTime ? "disco-sound.m4a" : "backgroundMusic.mp3"
)
This simple one-liner switches between the Cat Nap tune and disco depending on
the current level state.
The final code to add to didSet will automatically turn off the disco mode after a
few seconds. At the end of that method, append the following:
if isDiscoTime {
video.runAction(SKAction.waitForDuration(5.0), completion: {
self.isDiscoTime = false
})
}
First, you check if the player is turning on the disco mode; if so, you run a wait
action on the video node for 5 seconds. When this action completes, you simply set
raywenderlich.com 316
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
isDiscoTime to false, which triggers the didSet handler again and pauses and hides
the video.
Build and run to see how it's coming together. With that, your video node crash
course is complete!
Disco kitty
To wrap up this section, you'll add a bit of custom behavior to your cat node. It's
time for some sick dance moves!
You need to handle taps on the cat node, and you need to check if it's currently
disco time. If it is, you'll make the cat "dance" for awhile. To expose the
isDiscoTime property of DiscoBallNode to other classes, you'll need to add a static
property.
Further, inside didSet for the instance property isDiscoTime, and below all of the
other code in the didSet handler, add this line:
DiscoBallNode.isDiscoTime = isDiscoTime
This code will keep the static property isDiscoTime in sync with the instance
raywenderlich.com 317
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Now, each time the player taps on the cat node, you can check the static property
on DiscoBallNode and see if there's a disco happening.
Open CatNode.swift and add a property to keep track of whether or not the cat is
currently dancing:
Wrapping up this method is as simple as adding a few actions to the cat node. First,
replace //add dance action with this:
The first action, danceMove, represents a single dance move: It moves the cat 80
points to the right, then waits half a second and moves the cat back 30 points.
The complete dance action, dance, repeats danceMove three times. When the action
has finished running, the cat will effectively have moved to the right by 150 points.
This is exactly what you want, since the ultimate goal is to move the cat to the
center of the screen and just over the bed.
Finally, you want to run the dance action, and when it completes, toggle the Boolean
flag back to false. To get that working, add this bit of code just after the let dance
line:
parent!.runAction(dance, completion: {
self.isDoingTheDance = false
})
This runs the move action on the compound cat node. And that's it. You're ready to
check out kitty's dance moves!
Build and run the game. Tap the disco ball a few times, and while the lights are on,
tap on the cat to get him moving:
raywenderlich.com 318
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Can you figure out how to solve the level on your own? Give it a try!
Once you're over the center of the bed, you can make your way in:
When you're done with disco moves and fooling around, dance over to the next and
final section of this chapter, where you'll turn this ride around and once more!
Shape nodes
A shape node lets you draw any kind of shape onscreen—from a simple circle or a
rectangle, to any arbitrary shape you can define with a CGPath.
Speech bubbles are a great example of shape nodes used in games, because they
need to have different widths or heights depending on the current phrase the
character is speaking. Based on the amount of text that needs to fit in the bubble,
you can easily create a smaller or a larger rectangle, like so:
raywenderlich.com 319
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
In this section of the chapter, you'll dive into that a little more. You'll learn how to
set the stroke, color and width of the shape; how to make it hollow or filled; and
finally, how to use a texture to make the shape look at home in the scene.
The final goal for this chapter is to add a dynamic hint arrow to show the player
where to begin solving the level. Most games have hints or tutorials of some sort,
and you'd like Cat Nap to be on par with them.
raywenderlich.com 320
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
When you finish working through this section, you'll have a nice bouncing arrow
floating next to the disco ball:
Node: The shape node available in the scene editor's Object Library seems to
be a bit buggy. For example, even if its textures render fine in the editor, when
you run the project, the texture might be missing. For the time being, until
Apple fixes the issue, use your own shape classes.
You'll still use the scene editor, but just to position a placeholder for your new node.
Open Level6.sks and drag a new color sprite into the scene. It appears by default
as a small red square, and that's good enough for a placeholder. You're going to
hide the placeholder, anyway, so the player will never see it.
Before leaving the scene editor, adjust the node like so:
raywenderlich.com 321
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
When Xcode automatically opens the file, replace the contents with the following:
import SpriteKit
func didMoveToScene() {
color = SKColor.clearColor()
}
}
As soon as the node is added to the scene, you set its color to clearColor() and
effectively make it fully transparent:
With the placeholder out of the way, you can create your own shape.
Hey, that's easy! Here you create a shape node, which is a rectangle that fits into
the current node's size and has rounded corners with a radius of 20 points. You
configure the shape node to outline the shape with a 4-points thick red line, and
add it onscreen.
raywenderlich.com 322
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Looking good.
Try adjusting some more properties on your shape node by adding a few things to
didMoveToScene():
shape.glowWidth = 5
shape.fillColor = SKColor.whiteColor()
To make the outline a bit "fuzzier", you add a glow effect by increasing glowWidth,
which defaults to 0.
Finally, an easy win is to set fillColor and have Sprite Kit fill in the shape with a
color automatically, like so:
Before moving on to the real task at hand, watch how easy it is to change the
shape of the node.
This will make your node a different shape, but all the rest of the properties you
use to "beautify" it can remain the same.
I bet you're already asking, "Can't I just set my own custom path instead of using
rectangles and circles?" Why yes, you can. Now that you've covered the basics,
you're going to move on to working with custom shapes.
raywenderlich.com 323
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
Then you can make the node draw anything you want.
You already worked with CGPath back in Chapter 9, "Beginning Physics", where you
created a triangular shape for your triangle sprite's physics body.
Back then, you created this triangle in code: You started at the bottom-left corner,
drew a line to the bottom-right corner, continued drawing to the top, and then back
to where you started to close the shape.
Coming up with the precise coordinates to draw a triangle is a pleasant brain tease,
but how about more complex shapes, like a star, a truck or a space station? Unless
you're a geometry genius and calculate π for breakfast, finding the coordinates by
hand might be a bit too much.
Luckily, there's a neat app named PaintCode that lets you draw with your mouse,
and then translates your drawings into Swift code using CGPath. I used that app to
prepare the code for the arrow shape, so you don't have to draw it yourself:
In PaintCode, the Swift code appears in the lower panel as you draw on the canvas.
raywenderlich.com 324
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
For your Cat Nap arrow shape, you'll add a dynamic property on the HintNode class
that will give you the arrow CGPath. With that said, add the following to HintNode:
return bezierPath.CGPath
}
At the bottom of the method, you return the CGPath property on your bezier path
object.
Now you have to get rid of all the test code you wrote. To do that, simply replace
the complete didMoveToScene() method with this one:
func didMoveToScene() {
color = SKColor.clearColor()
raywenderlich.com 325
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
This code will get you started with a fresh, new arrow shape. This time around, you
use a convenience initializer that takes a CGPath parameter, and you supply it with
the path stored in your arrowPath property:
In this way, you can create any CGPath value and make SKShapeNode draw it
onscreen. You can adjust the points of the path dynamically from your code, or
even generate it on the fly based on the user's input.
There are a few, final touches you need to make, and then you'll be ready.
This simply makes the hint arrow a better fit for your scene; you fill it with a
wooden texture and give it a bit of opacity:
To quickly wrap up the level, add a few actions to the arrow to make it a bit more
pleasant to the eye. Do so by adding these lines to didMoveToScene():
shape.runAction(bounceAction, completion: {
self.removeFromParent()
})
This creates a bouncing animation action and runs it three times on the arrow node.
When the action completes, you remove the node from the scene.
Build and run one last time, and enjoy your new hint arrow. Notice how it appears
as the level begins, and then disappears after three bounces.
raywenderlich.com 326
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
You got to use the scene editor extensively, you learned about physics in Sprite Kit
and how to create your own game worlds, you got to create complex physics-based
mechanisms, and finally, you used fantastic picture frames, videos and custom
shapes!
Now, it's time to move on and take the sleepy cat experience to the next frontier:
the TV! In the next chapter, you'll add tvOS support to Cat Nap so the player to
enjoy the game on the big screen.
And of course, you can always add more levels to Cat Nap on your own. Why not
come back and revisit Cat Nap once you learn about all those exciting APIs from the
chapters in the rest of the book?
But before moving on, there are two quick challenges waiting for you.
Challenges
Challenge 1: Hints all around!
Now that your HintNode is fully functional, you can add it to the other levels in Cat
Nap. Consider how you added the node in the last level, and then add it to Level 1,
as well:
raywenderlich.com 327
2D iOS & tvOS Games by Tutorials Chapter 12: Crop, Video, and Shape Nodes
That should definitely make for more enjoyable gameplay! Can you think of other
game levels where you could add this hint?
Indeed, the documentation for SKShapeNode says that once the texture is set, the
value of fillColor is ignored. But that's not entirely true—you can use fillColor to
tint the texture and produce an infinite number of differently colored textures.
Enable user interaction for the hint arrow, and each time the player taps on it,
change the fillColor of the shape node. Keep an array of SKColor objects: red,
yellow and orange colors. Upon each tap, pick a random color from the list and set
it as the fill color.
I hope this challenge will make you feel like you're back in art class at school. What
color would a blue texture mixed with a yellow fill color produce?
raywenderlich.com 328
13 Chapter 13: Intermediate tvOS
By Marin Todorov
In this chapter, you're going to finish Cat Nap with a bang by putting it on the big
screen in your living room! Or if you don't own an Apple TV, you'll simply use the
Apple TV 1080p simulator. But, don't worry—that still counts. :]
In this chapter, you'll port the levels you have so far to run on the Apple TV, as well
as add some new features. Of course, the main focus will be on porting the
controls, so the player can use the Apple TV remote to play the game.
When you're finished working through this chapter, kitty will be ready to make his
TV debut:
It may not be the red carpet, but close enough. Kitty would just scratch up the
carpet anyway. :]
raywenderlich.com 329
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Note: This chapter begins where the previous chapter’s Challenge 2 left off. If
you were unable to complete the challenges or skipped ahead from an earlier
chapter, don’t worry—you can simply open the starter project from this
chapter to pick up in the right place.
The result, however, will be similar: you'll end up with two project targets, one for
iOS to run on the iPhone and iPad, and another for tvOS to run on the Apple TV.
The two targets will share the code that implements the gameplay, but each will
have its own project settings.
Open Cat Nap in Xcode. Before you add an extra project target for Apple TV, you
need to rename your existing assets catalog to make sure the one for the new
target won't conflict with it. Rename Assets.xcassets to Shared.xcassets. Also, if
you have a file named Game.xcassets, rename it to SharedGame.xcassets.
Then, select the project file in the project navigator to see the list of available
project targets. At the bottom of the list, click the + button to add a new target:
raywenderlich.com 330
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Enter CatNapTV for Product Name and make sure the Language is Swift and
the Game Technology is Sprite Kit. Then, click Finish to create the new target.
Xcode adds a new target for use on tvOS, as well as the needed project files, into a
new folder named CatNapTV. So far, so good.
To begin testing Cat Nap on the Apple TV simulator, change the active project
scheme to CatNapTV/Apple TV 1080p:
raywenderlich.com 331
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Build and run the project to make sure you're testing on the TV simulator; you'll
see the default project template scene:
Still in Assets.xcassets, expand App Icon & Top Shelf Image and then select
the Top Shelf Image item. Drag the file from CatNap_tvOS_TopShelf.png file
into this slot.
raywenderlich.com 332
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Finally, select App Icon - Large and make sure the Attributes Inspector is open. In
Layers, click the + button once; there should be now four layers in total: Layer,
Front, Middle and Back.
The layers in the list are ordered from front to back, so grab the four images from
the App Icon - Large folder in the chapter resources and drop them, one by one,
into the four slots under the big icon preview. Make sure the image names match
the layer names. Once you've done that, you can hover with the mouse pointer
over the preview to see the icon in 3D:
Now, select App Icon - Small from the assets list and repeat the same process for
the smaller version of the game icon; you'll find the image files in the App Icon -
Small folder in the chapter resources.
Note: By iPhone standards, even the small TV icons are large. Just think: The
small icon is 420x200 pixels, and the full screen resolution of the iPhone used
to be 320x480!
raywenderlich.com 333
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Build and run; then from Xcode, stop the program. You'll see the proper assets pop
into your Apple TV simulator:
raywenderlich.com 334
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Open AppDelegate.swift and look inside—the code is the same as what you have
in your iOS target AppDelegate.swift. The same goes for GameScene.swift and
GameViewController.swift.
Since you worked on those files in previous chapters and added everything Cat Nap
needs to run, you can simply reuse the files for your tvOS target.
With that in mind, select the following files in CatNapTV and delete them:
• AppDelegate.swift
• GameScene.sks
• GameScene.swift
• GameViewController.swift
You'll use their counterparts from the iOS target. In fact, you're going to use all of
the files you created in your iOS target, except for the four items you now have
remaining in CatNapTV.
But first, you need to add the iOS files to your tvOS target. Unfortunately, the only
way to add them is to individually select each of the files in each of the folders.
Do that now. Select all of the necessary file groups in the bullet list below, and
when you have each of them selected, make sure to tick the CatNapTV target
checkbox in the Attribute Inspector:
• CatNap/*.swift: all remaining Swift code files; make sure not to include other
files in this folder;
raywenderlich.com 335
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
This should be all the files you've worked on for the iOS version of Cat Nap.
Xcode will attempt to build the project but then bail, reporting three errors—all of
them located in GameViewController.swift:
What Xcode tells you makes sense. On tvOS, UIViewController doesn't support
rotation and therefore the tvOS version doesn't support rotation-related methods.
Imagine if you tried to rotate your expensive 70" TV. :]
Further, the helper method related to hiding the status bar in iOS is missing—
indeed, Apple TV doesn't show a status bar on top of the screen, either.
No worries. You can still keep those methods for the iOS version of Cat Nap.
import UIKit
extension GameViewController {
Move the three methods that throw errors from GameViewController.swift into
GameViewController+iOS.swift:
• shouldAutorotate()
• supportedInterfaceOrientations()
• prefersStatusBarHidden()
raywenderlich.com 336
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
By excluding this file and separating all of the iOS-specific methods, you ensure
that you don't have errors when building for tvOS. In fact, the Xcode errors have
cleared, so build and run the game again:
You can hear the game music and the cat purring in the background, but it looks
like you're missing all the textures. So there's one final step: select CatNap/
Shared.xcassets and add it to the CatNapTV target.
raywenderlich.com 337
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
For tvOS, you'll let players select the active elements in the scene one by one, until
they decide which they want to interact with.
You'll also add an animation to the currently selected node, so that a player can
clearly see which block she's about to destroy:
Once a player has selected her desired block, she'll be able to tap on the remote to
interact with that node. Tapping on a block will destroy it; tapping on a spring will
send kitty into the air.
1. You'll fetch the list of all active items in the current level.
3. You'll observe for taps on the remote control and forward these events to the
currently selected node.
Your first task is to find all the nodes in the current level that allow for user
interaction. The best way to do that is to query all the children in the scene and find
any that conform to InteractiveNode.
First of all, you want all the tvOS-specific code to be separate from your iOS-
specific code. Earlier in this chapter, you enforced that by creating a class extension
file and excluding that file from the tvOS target. Since that works well, you'll do
something similar here.
This time, instead of excluding part of the functionality for Apple TV, you'll do the
opposite—add part of the code, but only add it to the tvOS target.
Open GameScene.swift and toward the top of the file, add a new protocol:
raywenderlich.com 338
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
protocol TVControlsScene {
func setupTVControls()
}
GameScene will adhere to this protocol, but only on the Apple TV version of the
game. To make that happen, you'll add a new file that defines an extension to
GameScene, but you'll only add it to the tvOS project target.
Select your CatNapTV folder, and from Xcode's main menu, select File/New/
File... and choose tvOS/Source/Swift file for the file template. Name the new
file GameScene+TV.swift
While you have GameScene+TV.swift open, make sure that only the CatNapTV
target is checked for the file's Target Membership in the Attributes Inspector:
Now, replace the file contents with the following empty extension:
import SpriteKit
func setupTVControls() {
}
}
The beauty of this configuration is that this extension only exists in the tvOS project
target. That means you can simply check from your main GameScene file if GameScene
adheres to TVControlsScene. This is a great way see if you're running on iOS or on
tvOS. Then, you can set up the game controls accordingly.
Speaking of which, now is the perfect time to do that. Open GameScene.swift and
add the following code to didMoveToView(_:):
You do a little trick with this code: First, you cast the current class instance of
GameScene to a generic SKScene type; then, you check if the generic type is actually
raywenderlich.com 339
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
If you're running on an Apple TV, you simply call setupTVControls(), which is where
you'll put all the code that's only relevant to the user experience on tvOS. Good
work so far!
First, add two variables just above the extension line but outside of the extension
code:
activeNodes will contain the list of all active nodes in the current level, and
currentNodeIndex is simply the index of the currently selected node in that list.
func setupSelectableNodes() {
activeNodes = []
})
}
Here, you begin with an empty list of active nodes, and then you enumerate over
all nodes in the scene. Remember, you used this technique in Chapter 10,
"Intermediate Physics", to loop over all nodes in the scene and call their respective
didMoveToScene() methods.
This code checks if the current node accepts user touches and if it's a SKSpriteNode,
in which case, you simply add it to activeNodes.
To see if setupSelectableNodes() is ready for prime time, add this line at the end of
the method:
print(activeNodes)
Now, you need to call the method to see if it will find all active nodes in the current
scene. Inside setupTVControls(), add the following:
raywenderlich.com 340
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
setupSelectableNodes()
You can see the details for all of the interactive blocks. If you're running the first
level, there are four blocks, the hint arrow and the cat's body.
Before moving on, comment out the print(activeNodes) line to avoid output clutter
in the console:
// print(activeNodes)
Now that you have all the blocks lined up, you can add the TV controls to let the
player make a block selection.
Luckily, just like with all subclasses of UIResponder, you can assign gesture
recognizers to your Sprite Kit view to recognize various gestures on the Apple TV
remote touch surface, like swipes and taps. And gesture recognizers are a fantastic
way to interact with Cat Nap.
As you saw in Chapter 7, "Beginning tvOS", tvOS forwards the user touches from
the Apple TV remote to your Sprite Kit view. The same goes for working with
gesture recognizers—you can create and add recognizers to the game view, and
they'll work for touches coming from the Apple TV remote.
raywenderlich.com 341
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
With the code you just added, you create and attach two swipe gesture
recognizers: one to detect swipes to the left and one to detect swipes to the right.
Both recognizers invoke the same method, didSwipeOnRemote(_:), where you'll
simply change the currently selected node to either the previous one or the next
one.
First, this code checks to see if there are any active nodes. You probably don't
expect to have any levels without any interactive blocks or other items, but this
check is more for the end of a level, when all blocks have been destroyed and the
player swipes by mistake.
Next, you need to update the current index based on the swipe direction. If the
player swipes to the right, increase the index to select the next node; if the player
swipes to the left, decrease the index.
if (swipe.direction == .Right) {
newIndexToSelect++
} else {
newIndexToSelect--
}
This takes care of adjusting the index properly. However, you still need to deal with
instances when the index goes beyond the bounds of the active node list. And you
also need to let the player loop over the list.
raywenderlich.com 342
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
if newIndexToSelect < 0 {
newIndexToSelect = activeNodes.count-1
} else if newIndexToSelect > activeNodes.count-1 {
newIndexToSelect = 0
}
Now, if the index goes below 0 or above the last index in the list, you change
directions and turn the ride around, so to speak.
Finally, you need to select the node with the current index. To do that, add this line:
selectNodeAtIndex(newIndexToSelect)
There, are you happy, Xcode? You now have a method named
selectNodeAtIndex(_:), so settle down. :]
Just like the other method you added, this one begins with a guard statement to
make sure you have active nodes in the scene.
Next, you're going to run a repeating animation to tint the currently selected node.
You'll need two SKAction instances to build the tint animation.
Since you're about to run selectCurrentNode(_:) each time the player changes the
raywenderlich.com 343
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
selection, you'd like to create the actions once and then reuse them each time.
Scroll to the top of the file and add two private constants:
The former defines a fade-out animation, while the latter defines a fade-in
animation. You won't fade the selected node until it's completely transparent,
because that would look a bit awkward, so you fade it out until it's half opaque,
then fade it back in to a 100% opacity.
activeNodes[index].runAction(
SKAction.repeatActionForever(
SKAction.sequence([fadeOut, fadeIn])
)
)
Here, you access the currently selected node in activeNodes by using the index
method parameter, and then you run the animation sequence on that node. This
will make the current node fade in and fade out.
To pre-select the first active node in the level, add this to setupTVControls():
selectNodeAtIndex(0)
Now, build and run. You'll see one of the blocks animate nicely and show the player
which is the currently selected element in the scene:
And since you've already wired the swipe gesture recognizers, try swiping left and
raywenderlich.com 344
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Of course, since you didn't write code to deselect the current node, they all become
selected as you swipe through them—but you'll fix that in a moment.
You may also notice that the top block disappears on your first swipe-you'll fix that
in the chapter's challenge.
raywenderlich.com 345
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
See that big empty area? That's the virtual remote trackpad. To simulate a swipe,
hover the mouse just outside the area of the trackpad, then hold the Option key
and swipe over the trackpad, releasing the Option key when you get to the end of
the trackpad. It takes a bit of practice, but you'll get the hang of it in no time. :]
Deselecting nodes
By now, it's clear that you need to add some code to deselect nodes—otherwise the
level quickly becomes a blinking mess.
First, you check if the currently selected node index is still within the bounds of the
active nodes list. When could that be true? Well, if you destroy the last node in the
list, its index will be bigger than the current last index. In that case, you won't need
to deselect it—the node has already been destroyed!
The second condition in your if statement checks if the currently selected node is
also the new node to select. This condition, which is a bit confusing at first, is true
when the level first begins; the check ensures you don't stop the animation on the
first block that gets selected by default.
If, on the other hand, the previously selected node is still in the scene, you remove
all its running actions, thus stopping the tint animation. Then, you reset the alpha
to 1.0 so the node no longer appears selected.
As a final touch, you need to update the current selection index, so add this line to
the same method:
currentNodeIndex = index
Build and run, and you'll notice something a bit strange when you cycle through the
blocks: There's one swipe that deselects the current block but doesn't select
anything else. This happens because the hint arrow is present at the time you first
run setupSelectableNodes(), but when it disappears after a bit, you still have a
reference to it in activeNodes.
To quickly solve this issue, open HintNode.swift and remove InteractiveNode from
the class declaration. This way, setupSelectableNodes() will skip over this node
when it queries for active nodes in the scene.
That should fix cycling through the nodes, so you're ready to add some destructive
action to the game!
raywenderlich.com 346
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
However, on Apple TV, as you learned in Chapter 7, you don't get precise
coordinates of the touches, and you can't rely on the player being able to tap
precisely on a node on the scene.
But for Cat Nap, simply knowing that the player tapped on the remote is more than
enough, since you added code to handle node selection on swipes. Therefore,
whenever players tap on the remote, you already know which node they want to
interact with. It seems like a simple tap gesture recognizer should suffice to
complete the gameplay and have the game running on Apple TV in no time!
First, add a tap gesture recognizer to your Sprite Kit view. Open GameScene
+TV.swift and add the following to setupTVControls():
You've already added the recognizers for swiping left and right, and in the same
manner, you add a simple tap recognizer to the scene's view.
Next, you need to cast the current node to InteractiveNode and call interact() on
it. For this, append the following to didTapOnRemote(_:):
raywenderlich.com 347
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
You use an if let construct to cast the current node, and then you call interact()
on the selected node, which invokes the action for that node: blocks get destroyed,
or in case of disco time, the cat dances, and so forth. In the end, you call
setupSelectableNodes() because, as a result of calling interact(), there may be
fewer nodes to cycle through.
With that, your swipe and tap recognizers are ready to go.
Build and run, and this time, you can play the game through.
The game looks absolutely gorgeous on the big screen! Using the Apple TV
simulator is a bit difficult until you get used to it, but playing the game with the
actual Apple TV remote is tons of fun.
Get ready to jump on the couch and fire up Cat Nap on your TV!
Challenges
You've already covered many tvOS concepts in this chapter, and the basic Cat Nap
gameplay works pretty well on Apple TV. But there are a number of small things
you can do to polish the game.
These challenges are optional. If you're eager to start working on the next game in
the book, skip to the next chapter. If you can't get enough of our kitty and would
like to improve Cat Nap's code further, read on!
raywenderlich.com 348
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
To keep the player going, you could automatically select the next node in the list of
active nodes. You can implement this easily by checking if the number of active
nodes has changed after calling interact() on the current one. If the number of
active nodes has changed, you can simply call selectNodeAtIndex(_:) to select the
next node.
if originalCount != activeNodes.count {
selectNodeAtIndex(currentNodeIndex)
}
This code will adjust the selection each time the player destroys a block. You're
welcome, player. :]
raywenderlich.com 349
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
appears and disappears when he's supposed to be sleeping calmly in bed—it's just
the animation loop continuing to run.
To fix this, you can simply remove all running actions on kitty whenever he falls into
bed or onto the ground.
First, open CatNode.swift and scroll to curlAt(_:). Once there, add the following
at the top:
removeAllActions()
alpha = 1.0
That should remove the selected node animation just before you load the curl up
frame sequence.
Now in the same file, scroll to wakeUp() and add this at the top:
removeAllActions()
alpha = 1.0
This will remove the selection animation from the kitty before he falls over and onto
the ground.
raywenderlich.com 350
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Open Cat Nap Level 1 and do the swipe motion below on the remote. Make sure to
stop your swipe at the end of the red line:
After you try it few times, you'll see something interesting happen: One of the
blocks disappears even if you didn't tap to destroy it. It may not even be selected
at the time.
This happens because if you do a swipe like the one above, you end your touch on
the trackpad around the center of the screen. The remote forwards the touches to
the responder chain, and that event ends up calling touchesEnded(_:withEvent:) on
the block in the center of the screen.
It looks like you have to disable touchesEnded(_:withEvent:) on tvOS but still keep
userInteractionEnabled on.
raywenderlich.com 351
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
#if os(iOS)
override func touchesEnded(touches: Set<UITouch>, withEvent event:
UIEvent?) {
super.touchesEnded(touches, withEvent: event)
print("destroy block")
interact()
}
#endif
This macro instructs Xcode to include this part of the source code only when
building for iOS. When you build your tvOS app, it will be as if
touchesEnded(_:withEvent:) never existed in your BlockNode class. This will solve
the black magic node interaction.
To prevent the issue from happening throughout the game, go over CatNode,
SpringNode, StoneNode, PictureNode and DiscoBallNode, and wrap
touchesEnded(_:withEvent:) for each of those in an #if macro.
This inevitably results in discrepancies in the user experience between the different
versions of the game. It happens when you port your Sprite Kit iOS game to OS X,
and it happens when you port to Apple TV, as well.
One example of such a discrepancy in the user experience is in the last level, Level
6. When you play the game on your iPhone or iPad, you can tap the disco ball. Of
course, when you do that, while you're in disco mode, you can also tap the cat to
make him dance around.
On the other hand, when you play the game on your Apple TV, you have to cycle
through all the blocks, and there are plenty in this level, to get to the disco ball.
Then, after you make the long trek there, you have to tap the disco ball and cycle
again through a number of blocks until you get to the cat.
raywenderlich.com 352
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
Since you have to repeat that procedure a few times to solve the level, the players
enjoying Cat Nap on an Apple TV might lose their motivation. Imagine they solve
the level by doing all the necessary tapping and dancing (but not tap dancing!), and
then move on to the next level, only to make a mistake and be sent back to the
disco ball!
To keep players committed to this level on the Apple TV, it would be a good idea to
adjust the level so that players can solve the puzzle faster.
For example, you can adjust the distance kitty moves with each dance routine. To
do that, adjust the code like so:
#if os(iOS)
let move = SKAction.sequence([
SKAction.moveByX(80, y: 0, duration: 0.5),
SKAction.waitForDuration(0.5),
SKAction.moveByX(-30, y: 0, duration: 0.5)
])
#endif
raywenderlich.com 353
2D iOS & tvOS Games by Tutorials Chapter 13: Intermediate tvOS
#if os(tvOS)
let move = SKAction.sequence([
SKAction.moveByX(200, y: 0, duration: 0.5),
SKAction.waitForDuration(0.5),
SKAction.moveByX(-50, y: 0, duration: 0.5)
])
#endif
After you implement this code, try the level on your Apple TV or in the Apple TV
simulator, and you'll see that now, the level is a lot more enjoyable.
You brought the complete Cat Nap game to Apple TV, and if you throw in a start
menu, some extra levels and maybe some Game Center integration, the game is
ready for prime time. Or should we say couch time! I hope you enjoyed working on
this little game, and I hope you learned a lot about Sprite Kit along the way.
raywenderlich.com 354
Section III: Juice
In this section, you’ll also learn how to take a good game and make it great by
adding a ton of special effects and excitement – a.k.a. “juice.”
In the process, you will create a game called Drop Charge, where you're a space
hero with a mission to blow up an alien space ship - and escape with your life
before it explodes. To do this, you must jump from platform to platform, collecting
special boosts along the way. Just be careful not to fall into the red hot lava!
raywenderlich.com 355
14 Chapter 14: Making Drop
Charge
By Michael Briscoe
In this section of the book, you'll take what you've learned so far and use that
knowledge to create an endless, platform jumper game named Drop Charge. You'll
also learn how to take a game from good to great by adding "juice"—those special
details that collectively make your game shine brightly among the pack.
You'll do all of this and more in multiple stages across the next four chapters:
1. Chapter 14, Making Drop Charge: You'll put together the basic gameplay
using the scene editor and code, flexing the Sprite Kit muscles you've developed
working through previous chapters.
2. Chapter 15, State Machines: You'll learn what state machines are and how to
use them.
3. Chapter 16, Particle Systems: You'll learn how to use particle systems to
create amazing special effects.
4. Chapter 17, Juice Up Your Game: You'll trick out your game with music,
sound, animation, more particles and other special effects, experiencing for
yourself the benefits of mastering the details.
raywenderlich.com 356
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
In Drop Charge, you're a space hero with a mission to blow up an alien space ship -
and escape with your life before it explodes. To do this, you must jump from
platform to platform, collecting special boosts along the way. Just be careful not to
fall into the red hot lava!
Note: This chapter is optional; it is a review of what you have learned in the
chapters so far. You should read this chapter if you'd like to get some
additional practice while making a cool new game, but if you feel confident
that you understand the material covered already, feel free to skip to the next
chapter.
raywenderlich.com 357
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Getting started
As you've done in previous sections, start Xcode and create a new project with the
iOS \ Application \ Game template. Enter DropCharge for the Product Name
and verify that the Language is set to Swift, the Game Technology to SpriteKit
and the Devices to Universal.
Also, open Info.plist and locate the Supported interface orientations (iPad)
entry. Delete the entries for Portrait (top home button), Landscape (left home
button) and Landscape (right home button).
raywenderlich.com 358
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Finally, import the artwork you need for this game by dragging the files and folders
from starter\resources\images into the left sidebar of Assets.xcassets.
Optional: This would be a good point to set up your launch screen, if you'd
like one. You can find the launch art in starter\resources\images. Refer to
Chapter 1 if you need help.
The first step is to configure your scene for the appropriate mode, which in this
case is portrait mode.
raywenderlich.com 359
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Remember, this is the standard scene size you are using for all games in this book,
just in portrait mode instead of landscape mode.
From the Object Library, drag an empty node into the scene. Name this node
World and set its position to (768, 1024). All other nodes will be children of this
node. This is a handy trick to use, because it will allow you to easily move all
objects in the scene at once by moving the World node - which will be useful later
on on when you implement a screen shake effect.
Next, drag another empty node into the scene. Name this node Background, and
set its Parent to World and its Position to (0, 0). You will add all backgrounds as
descendants of this node, so that it will be easy to move all backgrounds at once by
moving the Background node.
Add one last empty node. This time, name it Overlay. Set its Parent to
Background and its Position to (0, 0). This node will hold all of your background
textures. The reason you are adding them to the Overlay node rather than directly
to the Background node is that you will eventually be creating multiple copies of the
Overlay node, in order to implement continuous scrolling backgrounds.
raywenderlich.com 360
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Name this SKSpriteNode bg1, set its Parent to Overlay, its Position to (0, -1024)
and its Anchor Point to (0.5, 0.0).
Note: While you're laying out the scene, the artwork will appear quite large.
Remember, to better visualize your scene, you can zoom out by clicking the
minus (-) button at the bottom-right of the scene editor.
Add the remaining two bg nodes to the Overlay node with the following settings:
Note: Setting the anchor point to the center-bottom of the SKSpriteNode aids
in positioning the stacked background, because you can simply add the
texture's height to the y-position for each additional texture.
raywenderlich.com 361
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
To make the background a little more interesting you'll add some decorations. Add
the midground sprites to the Overlay node using the following settings:
How's that for a payoff? Build and run to see your progress.
raywenderlich.com 362
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
You've finished the background. That can only mean it's time to work on the
foreground!
From the Media Library, add four sprites to the Foreground node using these
settings:
• Title is the game's title, which you'll display until the player taps the screen to
start.
raywenderlich.com 363
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
• Ready is what you'll display once the game has loaded and is ready to play.
• Bomb is what the space marine will drop to explode the alien spaceship and get
the action started!
Next, you're going to add your last foreground node: the lava.
Lava is the enemy! It continuously rises as your marine strives for escape. The
player must avoid falling in the lava, or risk a smouldering end.
From the Object Library, drag a Color Sprite into the scene. In Chapter 16, you'll
attach an awesome particle system to this sprite, but you'll keep it simple for now.
Name the sprite Lava, set its Parent to Foreground, its Position to (0, -1024), its
Size to W: 1536 and H: 2048, its Anchor Point to (0.5, 1) and its Z Position to 4.
Also, change the Color to orange and set its Opacity to 75%.
raywenderlich.com 364
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
The lava is calmly resting just off-screen, waiting to be agitated by a bomb blast.
You're almost ready to start coding, but you don't want to leave your hero hanging
in midair with a lit bomb behind him. You need some platforms—and as extra
incentive to get him jumping, some shiny coins.
Creating platforms
To keep your players coming back, the game should be different every time they
play. Later in this chapter, you'll add code that will randomly place platforms and
coins in the level as your player moves up the screen. You could create your
platform configurations entirely in code, but it's a lot easier—and more fun—to do it
in the scene editor!
Control-click on your DropCharge folder, select New Group, and rename the
group to Scene Files. You'll be creating lots of these, so it will be nice to keep
them organized.
Control-click your new Scene Files group and select New File. Select the iOS
\Resource\SpriteKit Scene template and click Next. Save your scene as
Platform5Across.sks.
Set the scene's size to W: 900 and H: 200. Now drag a Color Sprite from the
Object Library, name it Overlay, set its Position to (450, 100) and set its Size to
W: 900 and H: 200. This sprite is merely a container, so you don't need to see it;
therefore, set its Opacity to 0%.
raywenderlich.com 365
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Note: You're using an SKSpriteNode here because you want to get the overlay
node's exact dimensions, keeping the spacing between platforms consistent.
For this node, you want five platforms evenly spaced across the scene. Start by
dragging platform01 from the Media Library. Set its Name to p1, its Parent to
Overlay and its Position to (-360, 0).
This time, you're also going to add a physics definition to the sprite, because you
want the player to interact with the platform rather than just fall through it.
Scroll the Attributes Inspector down until you see the Physics Definition heading.
Choose Bounding Rectangle for the Body Type and make sure to uncheck
Dynamic, Allows Rotation, Pinned and Affected By Gravity. Set the Category Mask
to 2 (the bit flag for a platform), the Collision Mask to 0, the Field Mask to 0 and
the Contact Mask to 1 (the bit flag for the player).
Note: Remember that you use the category mask to identify objects in
didBeginContact(_:).
Now that you have the first platform set up, click on p1 and duplicate
(Command-D) it four times with the following changes:
raywenderlich.com 366
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
When you're done, your platform scene will look like this:
Creating coins
Now you're going to create some coins to give the marine a boost, and then you'll
write some code.
This coin node will consist of five coins in an arrow pattern. As you did before,
create a new file in your Scene Files group with the iOS\Resource\SpriteKit
Scene template. This time, name it CoinArrow.sks and set the scene size to W:
900 and H: 600.
Drag a Color Sprite from the Object Library and name it Overlay. Set its Position
to (450, 300), its Size to W: 900 and H: 600, and its Opacity to 0%.
Now from the Media Library, drag powerup05_1 into the scene. Set its Name to
c1, its Parent to Overlay and its Position to (-360, -200).
Under Physics Definition, choose Bounding Circle for the Body Type. Also,
uncheck Dynamic, Allows Rotation, Pinned and Affected By Gravity. Set the
Category Mask to 8 (the bit flag for a coin), the Collision Mask to 0, the Field Mask
to 0 and the Contact Mask to 1 (the bit flag for the player).
With that done, click on c1 and duplicate (Command-D) it four times with the
following changes:
raywenderlich.com 367
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
You're done with the scene editor—for now. It's time to get coding!
3. Adding physics and collision detection so the marine can jump on platforms and
raywenderlich.com 368
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
collect coins;
4. Implementing code to read the device accelerometer data to steer the marine;
5. Creating methods to handle the camera node so that it follows the player sprite;
Most of these things you've done before, so crack your knuckles and let's get
started!
Adding platforms
Your hero is still floating in midair! Put some platforms under his feet to give the
guy a rest—he'll need it when he finds out his ship's about to explode.
Begin by dragging SKTUtils into the project navigator. Verify Copy items if
needed, Create groups, and the DropCharge target are selected, and click
Finish. You'll be using these utilities here to generate random numbers.
import SpriteKit
// MARK: - Properties
var bgNode = SKNode()
var fgNode = SKNode()
var background: SKNode!
var backHeight: CGFloat = 0.0
var player: SKSpriteNode!
func setupNodes() {
let worldNode = childNodeWithName("World")!
bgNode = worldNode.childNodeWithName("Background")!
raywenderlich.com 369
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
background = bgNode.childNodeWithName("Overlay")!.copy()
as! SKNode
backHeight = background.calculateAccumulatedFrame().height
fgNode = worldNode.childNodeWithName("Foreground")!
player = fgNode.childNodeWithName("Player") as! SKSpriteNode
fgNode.childNodeWithName("Bomb")?.runAction(SKAction.hide())
}
These properties will hold the nodes and platform overlays you set up in the scene
editor. There are also properties here to hold the background height, and keep track
of the last platform's position, height and current position, making it possible for
the game logic to properly position future platforms in a random manner once
gameplay begins.
setupNodes() loads the children nodes from the World node into their respective
properties, calculates the height of the background, and it also hides the Bomb
node.
Note that when you retrieve the Overlay node (containing the background images)
from the scene file, you use copy() to make a new copy of the node.
Now implement the methods that load and create the platforms:
func createBackgroundNode() {
let backNode = background.copy() as! SKNode
backNode.position = CGPoint(x: 0.0, y: levelY)
bgNode.addChild(backNode)
levelY += backHeight
}
raywenderlich.com 370
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
2. createOverlayNode() positions the new overlay node right above where any
previous overlay node was placed. It also has a parameter to flip the node along
its x-axis (wihch you will use later to add more variability into the level).
platform5Across = loadOverlayNode("Platform5Across")
coinArrow = loadOverlayNode("CoinArrow")
func setupLevel() {
// Place initial platform
let initialPlatform = platform5Across.copy() as! SKSpriteNode
var itemPosition = player.position
itemPosition.y = player.position.y -
((player.size.height * 0.5) +
(initialPlatform.size.height * 0.20))
initialPlatform.position = itemPosition
fgNode.addChild(initialPlatform)
lastItemPosition = itemPosition
lastItemHeight = initialPlatform.size.height / 2.0
}
This method places the platform right below the player, and updates
lastItemPosition and lastItemHeight appropriately.
setupLevel()
raywenderlich.com 371
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Nice work! Your hero is standing his ground, looking like a boss—for now.
More platforms!
But you want more than one set of platforms in your level—and what about those
coins? Add the following method after createOverlayNode(_:flipX:):
func addRandomOverlayNode() {
let overlaySprite: SKSpriteNode!
let platformPercentage = 60
if Int.random(min: 1, max: 100) <= platformPercentage {
overlaySprite = platform5Across
} else {
overlaySprite = coinArrow
}
createOverlayNode(overlaySprite, flipX: false)
}
This bit of code generates a random number and adds a platform 60% of the time;
otherwise, it adds the arrow of coins. Later, as a challenge, you'll build on this
method to add other platform and coin configurations.
raywenderlich.com 372
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Build and run. You'll see something like this (your screen might look slightly
different):
This Boolean keeps track of whether or not the user is playing the game. Next, add
the following methods:
// MARK: - Events
func bombDrop() {
let scaleUp = SKAction.scaleTo(1.25, duration: 0.25)
let scaleDown = SKAction.scaleTo(1.0, duration: 0.25)
let sequence = SKAction.sequence([scaleUp, scaleDown])
let repeatSeq = SKAction.repeatActionForever(sequence)
raywenderlich.com 373
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
fgNode.childNodeWithName("Bomb")!.runAction(SKAction.unhide())
fgNode.childNodeWithName("Bomb")!.runAction(repeatSeq)
runAction(SKAction.sequence([
SKAction.waitForDuration(2.0),
SKAction.runBlock(startGame)
]))
}
func startGame() {
fgNode.childNodeWithName("Title")!.removeFromParent()
fgNode.childNodeWithName("Bomb")!.removeFromParent()
isPlaying = true
}
You're already familiar with touch events from Chapter 2, "Manual Movement". Here
you only need touchesBegan(_:withEvent:) to monitor for a screen tap. Once you
detect a touch, this method checks to make sure gameplay hasn't already started,
and if it hasn't, calls bombDrop() to animate the bomb and start the game.
Build and run and you'll see the bomb pulse breifly then disappear, along with the
title.
Getting physical
If there was a bomb behind you, wouldn't you get moving real fast? Your hero
needs to be a lot more physical. Let's fix that by configuring GameScene.swift to
handle some basic physics.
In this section, you'll practice the skills you developed in Chapters 9, 10, and 11 -
the three chapters on Sprite Kit physics. If at any point you get confused about
raywenderlich.com 374
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
func setupPlayer() {
player.physicsBody = SKPhysicsBody(circleOfRadius:
player.size.width * 0.3)
player.physicsBody!.dynamic = false
player.physicsBody!.allowsRotation = false
player.physicsBody!.categoryBitMask = 0
player.physicsBody!.collisionBitMask = 0
}
This configures the physics properties for your player node, aka your hero. Now
add this line to didMoveToView(_:) to call setupPlayer():
setupPlayer()
Since player now has a physicsBody let's turn it on when the game starts. Add the
following line to startGame():
player.physicsBody!.dynamic = true
Oops! It looks like your marine is a bit clumsy—falling off the platform like that.
Help him out by giving him a boost with the following methods:
raywenderlich.com 375
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
func setPlayerVelocity(amount:CGFloat) {
let gain: CGFloat = 1.5
player.physicsBody!.velocity.dy =
max(player.physicsBody!.velocity.dy, amount * gain)
}
func jumpPlayer() {
setPlayerVelocity(650)
}
func boostPlayer() {
setPlayerVelocity(1200)
}
func superBoostPlayer() {
setPlayerVelocity(1700)
}
These methods set the vertical velocity of the player nodes physics body, giving
him thrust. When the marine lands on a platform or collides with a coin,
jumpPlayer() will fire. You'll use boostPlayer() when the player collects a special
coin.
Add a call to superBoostPlayer() at the start of the game when the bomb explodes,
by adding this line to startGame():
superBoostPlayer()
He goes up—and he comes down! Your marine needs to know about those coins
raywenderlich.com 376
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Collision detection
First, add the following struct definition between the import statements and the
class declaration:
struct PhysicsCategory {
static let None: UInt32 = 0
static let Player: UInt32 = 0b1 // 1
static let PlatformNormal: UInt32 = 0b10 // 2
static let PlatformBreakable: UInt32 = 0b100 // 4
static let CoinNormal: UInt32 = 0b1000 // 8
static let CoinSpecial: UInt32 = 0b10000 // 16
static let Edges: UInt32 = 0b100000 // 32
}
Here you define the various physics categories for collision detection. This is why
earlier in the scene editor you set the platform's category to 2, the coin's category
to 8, and both of them to register a contact with 1 (the player).
physicsWorld.contactDelegate = self
This declares your class as a delegate for SKPhysicsContactDelegate, and sets the
delegate to the scene.
raywenderlich.com 377
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
This method handles collision detection and how it affects the player in various
circumstances. Now when the marine makes contact with the platforms and coins,
he'll get that boost he needs to get moving. There's just one more thing to do first.
player.physicsBody!.categoryBitMask = 0
To the following:
player.physicsBody!.categoryBitMask = PhysicsCategory.Player
This is to make sure didBeginContact(_:) can differentiate between the player node
and other game objects.
Yay! Now your hero is grabbing coins and bouncing happily off of the platforms.
Note: Note there is a chance that your hero could get stuck offscreen based
on the configuration of your random coins and platforms. If this happens, build
and run again until you verify that you see the bouncing working.
raywenderlich.com 378
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
from device hardware, including both accelerometer and gyro-based data. In the
games you created in earlier chapters of this book, you controlled your player
sprites with touch events. In Drop Charge, you'll control the marine by tilting your
device right or left.
Core Motion is beyond the scope of this book, but it's easy to set up and will be
quite useful in controlling your marine's movement.
import CoreMotion
Here you're declaring an instance of the CMMotionManager() that you'll poll for
motion data, and adding a variable to track the accelerometer.
func setupCoreMotion() {
motionManager.accelerometerUpdateInterval = 0.2
let queue = NSOperationQueue()
motionManager.startAccelerometerUpdatesToQueue(queue, withHandler:
{
accelerometerData, error in
guard let accelerometerData = accelerometerData else {
return
}
let acceleration = accelerometerData.acceleration
self.xAcceleration = (CGFloat(acceleration.x) * 0.75) +
(self.xAcceleration * 0.25)
})
}
This method sets up the motionManager to periodically check the accelerometer and
update the xAcceleration variable based on how much the user is tilting the device
right or left. You'll use the xAcceleration property in the updatePlayer() method
that you'll add shortly.
setupCoreMotion()
func updatePlayer() {
// Set velocity based on core motion
player.physicsBody?.velocity.dx = xAcceleration * 1000.0
raywenderlich.com 379
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
This updates the velocity of the player's physics body based on the xAcceleration
property you set based on the accelerometer. Note it multiplies the value by 1000 -
this felt right through trial and error. This also contains some code to wrap the
player around the edges of the screen.
You'll call updatePlayer() continuously to steer your marine; to do that you'll need
to override the scenes update(_:) method:
Build and run - but this time be sure to use an actual device, as Core Motion isn't
supported in the iOS Simulator.
raywenderlich.com 380
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Note: For more information about Core Motion, visit Apple's iOS Developer
Library: http://apple.co/1F4DjCH. I highly recommend you acquaint yourself
with this framework because it's often handy to add accelerometer-based input
into your games.
Camera tracking
Now that you've got the marine jumping, you may have noticed that he frequently
moves out of view. To fix that, you'll add an SKCameraNode and some methods, to
track the hero as he jumps his way to safety. This is a review of the material from
Chapter 5, "Camera".
Begin by adding this to GameScene.swift, where the rest of the properties are:
addChild(cameraNode)
camera = cameraNode
These lines add the SKCameraNode to the scene, and sets its camera property.
// MARK: - Camera
func updateCamera() {
// 1
let cameraTarget = convertPoint(player.position,
fromNode: fgNode)
raywenderlich.com 381
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
// 2
var targetPosition = CGPoint(x: getCameraPosition().x,
y: cameraTarget.y - (scene!.view!.bounds.height * 0.40))
// 3
let diff = targetPosition - getCameraPosition()
// 4
let lerpValue = CGFloat(0.05)
let lerpDiff = diff * lerpValue
let newPosition = getCameraPosition() + lerpDiff
// 5
setCameraPosition(CGPoint(x: size.width/2, y: newPosition.y))
}
1. The player's position is relative to it's parent (fgNode), so you use this method
to convert the position to scene coordinates.
2. Set the target camera position to the player's Y position less 40% of the scene's
height, since we want to see what's coming ahead of the player.
3. Calculate the difference between the target camera position, and its current
position.
4. Rather than updating the camera straight to the target, move the camera 5%
toward the target. This will make the camera appear to take a while to catch up
to the player when the player moves quickly, for a cool effect. This technique is
also known as a linear interpolation, or "lerp" for short.
This code will center the camera. To make sure that the camera is tracking your
hero at all times, add this line to the top of update(_:):
updateCamera()
Build and run to see how the camera tracks the hero, always keeping him onscreen.
raywenderlich.com 382
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Note: You may notice if you jump far enough, the background disappears!
Don't worry, you'll fix that soon.
You use this property to hold your "Lava" node that you set up in the scene editor.
Now add the following method:
raywenderlich.com 383
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
1. This calculates the lower left position of the viewable part of the screen. Since
the (y) position is changing constantly, you are subtracting the height of the
scene from the camera's current (y) position.
2. The Lava's parent (fgNode) position is relative to lowerLeft, so you convert the
position to scene coordinates.
3. Here you are defining a base velocity for the lava, then multiplying the velocity
by the current time step, and adding it to lava's position.
4. The max method returns the highest (y) position between newPosition and a
position slightly below the visible area of the screen. This keeps the lava in sync
with the camera position; otherwise it would fall behind as the hero climbs his
way up.
func updateCollisionLava() {
if player.position.y < lava.position.y + 90 {
boostPlayer()
}
}
This pseudo collision detector doesn't actually detect contact between the lava and
player nodes, but rather compares their proximity to each other. If the marine falls
too close to the lava, he'll jump up as if his feet are afire!
Now you need to modify the camera to accommodate the lava. Scroll back to
updateCamera() and add these lines just after var targetPosition...:
These modifications get the current lava position and compares it to the camera
target; choosing the larger of the two values. This keeps the lava from appearing
higher than the middle of the screen.
raywenderlich.com 384
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Before you build and run let's properly set up your update(_:) method. Start by
adding the following properties:
1. currentTime is a relatively large number and can be unwieldy for some methods
that use a time step, such as your updateLava(_:) method. So deltaTime is
calculated to a much more manageable fraction.
2. Next you check to see if the scene is paused, and if so, exit the method.
3. Then check to see if the game is playing, and if it is, call your update methods,
including your new lava methods.
Now go ahead and build and run, and play your game for a bit. Everything is
looking great! You've got some hot lava, the marine is collecting coins and jumping
from platform, to platform, then—hey! Houston we've got a problem!
raywenderlich.com 385
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
func updateLevel() {
let cameraPos = getCameraPosition()
if cameraPos.y > levelY - (size.height * 0.55) {
createBackgroundNode()
while lastItemPosition.y < levelY {
addRandomOverlayNode()
}
}
}
This tracks the camera's position and adds a new background node, as well as a
random platform or coin overlay node when needed. Now call it by adding this line
to update(_:):
updateLevel()
raywenderlich.com 386
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
Now that you've completed most of the basic gameplay for Drop Charge, build and
run to see everything you've accomplished.
Congratulations - you now have the core gameplay for Drop Charge complete, and
have reviewed everything you've learned about Sprite Kit so far!
Now that you have a solid understanding of the basics of Sprite Kit, it's time to
move onto some new techniques, such as state machines, particle systems, and
adding Juice. By the time you've done the next three chapters, you'll take this basic
gameplay and make it shine!
Challenges
You only created two object overlays for Drop Charge: a standard five-across
platform and an arrow pattern of coins. It would be a lot more interesting if your
game had a variety of platform and coin patterns.
You're going to practice what you've learned on your own by creating additional
object overlays. This mission is divided into four challenges. As you begin to work
through them, keep in mind that each object has its own physics category to
differentiate itself in didBeginContact(_:).
And remember, if you get stuck with any of these challenges, you can find the
solutions in the resources for this chapter. But give them your best shot first!
raywenderlich.com 387
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
raywenderlich.com 388
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
raywenderlich.com 389
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
raywenderlich.com 390
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
• CoinCross.sks: a cross pattern of nine coins. Make five coins horizontal and five
vertical—no need to duplicate the center coin.
raywenderlich.com 391
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
• CoinSCross.sks: a cross pattern of nine coins just like the standard coin cross,
but include three special coins in the horizontal bar.
raywenderlich.com 392
2D iOS & tvOS Games by Tutorials Chapter 14: Making Drop Charge
• Optional: Can you think of other interesting platform/coin patterns for your
game? Using what you learned, come up with your own pattern!
Hint: You can save yourself a bit of work by duplicating and renaming existing
.sks files from the finder and using them as the basis for your new ones.
• Update didBeginContact(_:) to add a new case for .CoinSpecial that boosts the
player (rather than jumping the player).
If you get this working, congratulations; you have truly mastered the material so
far in this book!
In the next chapter, you'll learn about state machines—what they are and how
useful they can be in keeping your code organized. You'll also expand Drop Charge's
gameplay by adding code to accommodate the game ending—in other words, what
happens when the player succumbs to the lava!
raywenderlich.com 393
15 Chapter 15: State Machines
By Michael Briscoe
Drop Charge is starting to come together! In the previous chapter, you built the
game's UI and level objects using the scene editor. You also added the code for
most of the basic gameplay. In this chapter, you'll further enhance the game while
learning about state machines.
Most gameplay logic is governed by the current state of the game. For example, if
the game is in the "main menu" state, the player shouldn't move, but if the game is
in the "action" state, it should.
In a simple app, you could manage the state by using Boolean variables inside the
update loop. So far, this is the approach you've taken in Drop Charge with the
isPlaying variable.
But as a game grows, update(_:) can develop convoluted logic and become difficult
to maintain. By using a state machine, you can better organize your code as your
game becomes more complex.
Simply put, a state machine manages a group of states, with a single current
state and a set of rules for transitioning between states. As the state of the game
changes, the state machine will run methods upon exiting the previous state and
entering the next. These methods cans be used to control gameplay from within
each state. After a successful state change, the machine will then execute the
current state's update loop.
raywenderlich.com 394
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
Many games have multiple state machines. You could use a state machine to
control the hero's animation, the behavior of an enemy sprite and even the game's
user interface. To better understand this concept, you'll create a state machine to
manage the UI for Drop Charge, as pictured here:
To add a state machine to your game, first think about the various states that you
would want to manage. In Drop Charge you have four—Waiting for Tap; Waiting for
Bomb; Playing; and Game Over.
For each state, you'll subclass GKState. Within your custom class, you define state-
specific behavior by overriding these four methods:
raywenderlich.com 395
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
• canEnterState(_:): Use this method to check whether you can transition into a
specified state.
• enterState(_:): Call this to transition into the specified state. This is what you'll
use to switch from state to state.
Now that you've got some state machine knowledge under your belt, it's time to
put it into practice.
Note: This chapter begins where the previous chapter’s Challenge 4 left off. If
you were unable to complete the challenges or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up where the previous chapter left off.
raywenderlich.com 396
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
Getting started
Let's take a closer look at the four states you're going to define for Drop Charge:
1. Waiting for Tap: The game is waiting for user interaction. Once the player taps
the screen, you transition to the Waiting for Bomb state.
2. Waiting for Bomb: You play a short animation of the bomb pulsing and then
transition to the Playing state.
3. Playing: Drop Charge enters the main gameplay loop. If the player falls into
the lava three times, you transition to the Game Over state.
4. Game Over: In this state, you stop the gameplay and display the "Game Over"
sprite. When the user taps the screen, you transition back to the Waiting for Tap
state.
The Game Over state means that Drop Charge will no longer be the game that
never ends. Not even a fancy space suit can protect the hero from the lava
anymore!
Throughout the rest of this chapter, you'll implement each state in turn. Pay
attention, because when you're done, you'll be challenged to create your own state
machine!
In Xcode, open either your DropCharge project or the starter project found in
projects\starter\DropCharge.
Remember, to create a state machine, you first need to define your states by
subclassing GKState.
Select File\New\File... from the main menu. Choose the iOS\Source\Cocoa
Touch Class template and click Next. Enter WaitingForTap for the Class,
raywenderlich.com 397
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
GKState for the Subclass and Swift for the Language, click Next and then Create.
In the new file, replace the import UIKit line with this:
import SpriteKit
import GameplayKit
To use the GameplayKit framework, you first need to import it. You'll also be using
SpriteKit, so you import that, as well.
init(scene: SKScene) {
self.scene = scene as! GameScene
super.init()
}
You add a property to hold a reference to GameScene. This property gets initialized
when you create an instance of this state, so that WaitingForTap can interact with
the state machine and the scene.
Once this state is active, GKState will run this method. This is a good place to
display the "READY" sprite you created in the last chapter, indicating that the game
has loaded.
Now open GameScene.swift and add this import statement to the top of the file:
import GameplayKit
By defining this variable, you've effectively created the state machine for Drop
Charge. Notice that you're initializing GKStateMachine with an array of GKState
subclasses. Well, just the one subclass for now—you'll add more later.
gameState.enterState(WaitingForTap)
raywenderlich.com 398
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
Once the game is loaded and the WaitingForTap game state is active, the "READY"
prompt appears.
Repeat the process for creating a new GKState subclass, this time naming it
WaitingForBomb. Replace its contents with the following code:
import SpriteKit
import GameplayKit
init(scene: SKScene) {
self.scene = scene as! GameScene
super.init()
}
raywenderlich.com 399
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
SKAction.sequence(
[SKAction.waitForDuration(0.2), scale]))
// Bounce bomb
let scaleUp = SKAction.scaleTo(1.25, duration: 0.25)
let scaleDown = SKAction.scaleTo(1.0, duration: 0.25)
let sequence = SKAction.sequence([scaleUp, scaleDown])
let repeatSeq = SKAction.repeatActionForever(sequence)
scene.fgNode.childNodeWithName("Bomb")!.runAction(
SKAction.unhide())
scene.fgNode.childNodeWithName("Bomb")!.runAction(
repeatSeq)
}
}
}
Open GameScene.swift and replace your gameState variable declaration with this:
default:
break
}
}
This bit of code switches game states, depending on the current state, when the
user taps the screen.
While you're here, delete bombDrop() and startGame(), since you're going to move
these actions into your state machine.
One more bit of business and you're done with this state. Open
WaitingForTap.swift and implement the following method:
raywenderlich.com 400
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
This method tells your state machine that WaitingForTap can only transition to
certain valid states—in this case, Waiting For Bomb. This "rule" prevents the game
from accidentally transitioning to the wrong state. It wouldn't be fair to send your
player to the GameOver state on the first tap. ;]
Build and run. Now, when you touch the screen, you'll see a bomb pulsing behind
your hero—forever! If the suspense is too much for you, move quickly to the next
section.
raywenderlich.com 401
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
State 3: Playing
This state is where all the action takes place. Drop Charge will enter the gameplay
loop and check to see if the hero has fallen into the lava. If the hero collides with
the lava three times, the state machine will transition to the GameOver state.
Like before, create a new GKState subclass. This time, name it Playing and
replace its contents with the following code:
import SpriteKit
import GameplayKit
init(scene: SKScene) {
self.scene = scene as! GameScene
super.init()
}
}
There's nothing new going on here, but you'll be adding to this class in a moment.
In the familiar first method, you define the next valid state. You execute
willExitWithNextState(_:) when the game transitions to Playing. That's a good
time to do a little cleanup, so you remove the bomb sprite from the scene. In the
next chapter, you'll do something more exciting with the bomb by adding an
explosion.
Open GameScene.swift and update the gameState variable to add your new state:
Now scroll down to touchesBegan(_:withEvent:) and add the following just below
gameState.enterState(WaitingForBomb):
raywenderlich.com 402
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
This action makes sure the game switches to the Playing state after a couple of
seconds of the bomb animation.
Build and run, tap the screen, and watch as the game enters the Playing state. Let
the fun begin!
Before you added game states, you made those calls in the scene's update(_:).
Now you're going to pass them on to the state machine.
raywenderlich.com 403
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
This calls the state machine's updateWithDeltaTime(_:), which in turn calls the
same method on the current state. Now you're seeing the real power of state
machines!
To make the switch to your state machine complete, scroll to the top and remove
the isPlaying variable from your properties.
Build and run. The game plays much as it did before you started this chapter, but
this time with much more manageable code, thanks to the state machine.
raywenderlich.com 404
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
Create one more GKState subclass and name it—you've guessed it—GameOver.
Replace its contents with the following:
import SpriteKit
import GameplayKit
init(scene: SKScene) {
self.scene = scene as! GameScene
super.init()
}
When Drop Charge enters the GameOver state, you turn off the game's physics and
create an action to animate the hero offscreen. Then you create the "Game Over"
sprite and add it to the scene.
raywenderlich.com 405
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
As you may remember, this method tells the Playing state that it can only transition
to certain valid states—in this case, only Game Over.
Open GameScene.swift and update your gameState variable to add the new state
to your state machine:
var lives = 3
To decrement the hero's lives after a collision with the lava, change
updateCollisionLava() as follows:
func updateCollisionLava() {
if player.position.y < lava.position.y + 90 {
boostPlayer()
lives--
if lives <= 0 {
gameState.enterState(GameOver)
}
}
}
When your hero touches the lava, you decrement his lives by 1. If the number of
lives is less than or equal to 0, then you transition to the GameOver state.
Finishing touches
One last thing and your state machine will be complete!
case is GameOver:
let newScene = GameScene(fileNamed:"GameScene")
raywenderlich.com 406
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
newScene!.scaleMode = .AspectFill
let reveal = SKTransition.flipHorizontalWithDuration(0.5)
self.view?.presentScene(newScene!, transition: reveal)
default:
break
}
}
Now when the player taps the screen, touchesBegan(_:withEvent:) will check which
state the game is in using a switch statement:
• If gameState is WaitingForTap then enter the WaitingForBomb state, run the bomb
animation for two seconds, then enter the Playing state.
• If gameState is GameOver then reload GameScene which effectively resets the state
machine to WaitingForTap.
Drop Charge now feels much like a complete game. Through your state machine,
you've added an opening bomb animation, losing criteria and a game over
sequence, so users can experience your game from start to finish. More
importantly, your code is now much better organized, which will make it easier to
manage as you begin to add your "juice"!
raywenderlich.com 407
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
Challenges
There's only one challenge this time, but it's one of the most challenging you've had
in the book so far and requires mastery of this chapter's material.
As always, if you get stuck you can find the solution in the resources for this
chapter--but give it your best shot first!
What about the space marine? The space marine can be alive, in the process of
dying (running the bounce up and down actions when you hit the lava), or dead. It
would also be nice to keep track of whether the space marine is currently jumping
or falling, so you can play the appropriate animations in later chapters.
In this chalenge, you'll practice what you've learned by creating a state machine to
manage your hero's behavior. Here's a flow chart of what the player state machine
should do:
• Idle will be the player's initial state. Move the code to set up the physics for the
player sprite into this state . Remove setupPlayer() from GameScene.swift.
• Jump will control the jump animation. Just add the init() and
isValidNextState(_:) methods for now; you'll add more to this state later. Also
add a didEnterWithPreviousState(_:) method with printing out the name of the
state for debugging purposes.
• Fall will control the fall animation. Just add the init() and isValidNextState(_:)
methods for now; you'll add more to this state later. Also add a
didEnterWithPreviousState(_:) method with printing out the name of the state
raywenderlich.com 408
2D iOS & tvOS Games by Tutorials Chapter 15: State Machines
• Lava will control what happens when the player touches the lava. Set this state
within updateCollisionLava() in GameScene.swift. Move the code that boosts
the player and decrements his lives into this state.
• Dead will control what happens when the player has lost all his lives. Also set
this state within updateCollisionLava(), just before setting gameState to
GameOver. Move the code that animates the player offscreen into this state.
• You'll need to add code to updatePlayer() to check the player's y-velocity and set
the state to Fall or Jump.
Build and run, and the game should work as usual, but now nicely refactored (along
with some print statements when you jump or fall).
Over the next two chapters you will flesh out the playerState with additional code
as you add more "juice".
In the next chapter, you'll learn about particle systems, and how they can be
used to create stunning visual effects like explosions and smoke—and lava!
raywenderlich.com 409
16 Chapter 16: Particle Systems
By Michael Briscoe
Particle systems such as this spectacular explosion are an easy way to create a
variety of special effects in your games. Here are just a few of the things you can
simulate with a particle system:
And this is only the beginning. It's impossible to imagine all you could do with a
particle system, and it pays to be creative. For instance, say you want to simulate
subatomic particles emanating from a rip in the space-time continuum. That's easy!
What makes particle systems so great? Without a particle system, to achieve a
special effect like the explosion above, you'd have to resort to traditional frame-by-
frame animation techniques. This would require several images taking up significant
texture space and memory—not to mention be tedious to create—and the results
raywenderlich.com 410
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
might not look very realistic. In a particle system, the effects are created with a
small image texture and a configuration file, greatly reducing memory
requirements. This allows for real-time editing and rendering, resulting in greater
realism.
In this chapter, you'll continue preparing Drop Charge for its juice-up by using
particle systems to create three dazzling special effects. You'll learn how to
implement particle systems programmatically, as well as by using the Xcode editor.
Note: This chapter begins where the previous chapter’s challenge left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project for this chapter to pick
up where the previous chapter left off.
During each frame, the particle system looks at each individual particle it owns and
advances it according to the system's configuration. For example, the configuration
might say, "Move each particle between 2-10 pixels toward the bottom of the
screen." You can see the effects of this configuration in the following diagram:
raywenderlich.com 411
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
When it's initialized, a particle system will typically create a cache of particles
known as the particle pool. When it's time for a new particle to be born, the
particle system will obtain an available particle from its particle pool, set the initial
values of the new particle and then add it to the rendering queue.
When the particle has reached the end of its life, the system will remove it from the
render queue and return it to the particle pool, to be used at a later time.
This section will give you a quick overview of how to use particle systems in Sprite
Kit. You'll begin by reading through this information without doing anything in
Xcode. This will likely get you excited to try it out for yourself. Later in the chapter,
you'll do just that.
raywenderlich.com 412
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
For now, don't worry about what these properties mean—you'll learn about them
shortly. To see the effects of this code, open and run the Rain project included in
the starter\examples folder.
You can also use Xcode's built-in editor to visually create and configure a particle
system:
raywenderlich.com 413
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
To do this, you simply create a new file with the iOS\Resource\SpriteKit Particle
File template, resulting in an .sks file that you can edit with the built-in particle
emittor editor.
Then, in code, you create an SKEmitterNode with the file, like this:
This visual editor is super convenient, because it lets you visualize the particle
system in real time as you tweak its properties, making it quick and easy to get the
exact effects you want.
There's much more to discover, but it's time to begin to learn by doing. You'll create
your first particle system for Drop Charge by implementing an SKEmitterNode
programmatically. Later, you'll create additional effects using the visual editor.
Open your DropCharge project in Xcode. If you've skipped ahead, you can open
the project located in starter.
raywenderlich.com 414
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Sprite Kit renders every particle displayed onscreen using a single texture attached
to the particle system. This texture can be anything you wish, giving you great
freedom to customize the look of your particles.
From the project navigator, open the Assets.xcassets file. Drag the spark.png file
from starter\resources into the left sidebar. This is a white circular image that
you will tint orange, and create a ton of copies of to look like an explosion.
Now that you have a texture, you can create the particle system.
emitter.zPosition = 2
emitter.particleTexture = particleTexture
emitter.particleBirthRate = 4000 * intensity
emitter.numParticlesToEmit = Int(400 * intensity)
emitter.particleLifetime = 2.0
emitter.emissionAngle = CGFloat(90.0).degreesToRadians()
emitter.emissionAngleRange = CGFloat(360.0).degreesToRadians()
emitter.particleSpeed = 600 * intensity
emitter.particleSpeedRange = 1000 * intensity
emitter.particleAlpha = 1.0
emitter.particleAlphaRange = 0.25
emitter.particleScale = 1.2
emitter.particleScaleRange = 2.0
emitter.particleScaleSpeed = -1.5
emitter.particleColor = SKColor.orangeColor()
emitter.particleColorBlendFactor = 1
emitter.particleBlendMode = SKBlendMode.Add
emitter.runAction(SKAction.removeFromParentAfterDelay(2.0))
return emitter
}
raywenderlich.com 415
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
you to reuse this method to add additional explosions later in the game. You first
create a new SKEmitterNode and an SKTexture. Then, after setting the emitter's
zPosition, you set these properties:
• particleTexture is the texture to use for each particle, and probably the most
important property to set. The default value is nil, and if you don't set a texture,
the emitter uses a colored rectangle to draw the particle.
• particleBirthRate is the rate at which the emitter generates the particles per
second. It defaults to 0.0. If you leave the birth rate at 0.0, the emitter won't
generate any particles. In your method above, you set the particleBirthRate to
a high value of 4000 so that the emitter generates its particles very quickly,
creating an explosive effect.
• emissionAngle is the angle from which the particles are emitted. The default
value is 0.0, which is straight down. In your method, you set the particles to
initially emit straight up. Controlling the emission angle can be useful when you
want to create effects like fountains or geysers, where the water should move up
the screen before slowing and falling back down the screen, as if affected by
gravity.
• particleSpeed is the initial speed for a new particle in points per second. The
default value is 0.0. In your method, you scale the speed to create explosions of
varying intensity.
• particleAlpha is the average starting alpha value for each particle, and thus
determines the transparency of your particles. The default value is 1.0, which is
fully opaque.
• particleAlphaRange randomizes the particle alpha, plus or minus half the range
value. The default value is 0.0, which makes some of the particles slightly
transparent.
raywenderlich.com 416
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
• particleScale is the scale at which the emitter renders each particle. The default
value is 1.0, which means the emitter renders the texture for each particle at the
texture's full size. Values greater than 1.0 scale up the particles while values less
than 1.0 scale them down.
• particleScaleRange randomizes the particle scale plus or minus half the range
value. The default value is 0.0, which varies the size of your particles for a more
convincing explosion.
• particleColor is the color the emitter blends with the particle texture using the
particleColorBlendFactor (see below). The default value is
SKColor.clearColor(). In your method, you set this to orange so that the
particles look more like fire and sparks.
• particleBlendMode is the blending mode used to blend particles with the other
colors onscreen. The default value is SKBlendModeAlpha, which means that the
particles and other objects are blended by multiplying the particles' alpha value.
In your method, you set the blend mode to SKBlendModeAdd, which adds the
particles' and objects' colors together—making the particles seem to give off
light.
After setting the emitter's properties, you attach an action to remove the emitter
after a two-second delay. The method then returns the configured emitter node.
As you can see, there are a lot of properties you can set on an SKEmitterNode—and
there are even more you'll learn about very soon.
Now that you have a method that can create and configure a particle system, add
one to your scene to see what it looks like.
scene.fgNode.childNodeWithName("Bomb")!.removeFromParent()
raywenderlich.com 417
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
bomb.removeFromParent()
This bit of code grabs a reference to the bomb sprite and creates an explosion
particle system. Then, it places the explosion at the bomb's position, adds the
particle system to the scene and removes the bomb sprite.
Now that's an explosive start! I'm sure you don't need any more convincing that
particle systems can really juice up your game!
To demonstrate this, open and run the Starfield project located in starter
\examples. Notice that it takes a few seconds for the screen to fill with stars.
Still in the Starfield project, open GameScene.swift and add the following line to
didMoveToView(_:), just after setting the emitter position but before adding the
emitter to the scene:
emitter.advanceSimulationTime(15)
raywenderlich.com 418
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
This property sets the start time of the particle system ahead by 15 seconds,
effectively filling the sky with stars.
Now you see a screen full of stars, as if you were traveling on a starship at warp
speed! Any time you want to advance a particle system to a point beyond its initial
state, you'll find this method handy.
raywenderlich.com 419
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
• particleScaleSpeed: The rate at which to modify the particle scale over one
second.
• particleAlphaSpeed: The rate at which to modify the particle alpha value over
one second.
• targetNode: This lets you render particles as if they belong to another node. It's
an important property that you can use to create some unique effects. You'll
learn more about this property later.
Range properties
There is another set of properties on SKEmitterNode designed to allow you to add
random variance to a related property. You've seen examples of this already, when
you used emissionAngleRange, particleSpeedRange, particleAlphaRange and
particleScaleRange to add random values to the explosion emitter. Without these
properties, your explosion would look more like an electric candle flame.
These range properties are very important when you're trying to simulate real world
objects and phenomena. By adding a bit of randomness to the way your system
generates particles, you're introducing turbulence and adding realism to your
effects.
raywenderlich.com 420
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Keyframe properties
There are four properties on SKEmitterNode that provide a very cool technique
referred to as key framing. Instead of varying the property between a single value
and a random range, the idea with key frames is to change a property to several
specific values over time.
To use a keyframe property, you first initialize it and then add one or more
keyframes. Each keyframe has two properties:
• value: This is the value the property takes when this keyframe occurs.
particleColorSequence expects an SKColor instance for the value, such as
SKColor.yellowColor(). Other properties may expect different types of values.
• time: This is the time the keyframe occurs within the lifetime of the particle,
with a value ranging from 0 (the moment the particle is created) to 1 (the
moment the particle is destroyed). For example, if the lifetime of a particle is 10
seconds and you specify 0.25 for time, the keyframe would occur at 2.5 seconds.
Give key framing a shot by using the particleColorSequence to make the explosion
more lifelike.
Switch back to your DropCharge project and open GameScene.swift. Add the
following code to explosion(_:), before the return statement:
raywenderlich.com 421
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
The explosion starts out brighter than before and then fades away to glowing
embers. It's a subtle effect, but remember, adding juice is all about the details—
especially the subtle ones!
Sequence properties
There are two important properties to mention for SKKeyframeSequence: the
interpolationMode and the repeatMode.
The interpolationMode property specifies how to calculate the values for times
between each keyframe. The available interpolationMode values are:
• Linear: This mode calculates the interpolation values linearly. This is the default
raywenderlich.com 422
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
mode.
• Spline: This mode calculates the interpolation values using a spline, which gives
the effect of easing at the start and end of a keyframe sequence. If you were to
scale a particle with this mode, then the scaling would begin slowly, pick up
speed and then slow down until coming to the end of the sequence, providing a
smooth transition.
• Step: This mode does not interpolate the time values between keyframes. It
simply calculates the value as that of the most recent keyframe.
The repeatMode property specifies how to calculate values if they're outside of the
keyframes defined in the sequence. It's possible to define keyframes from 0.0 all
the way to 1.0, but you don't have to; you could have a keyframe that runs from
0.25 to 0.75. In that case, the repeatMode property defines what values to use from
0.0 to 0.25 and from 0.75 to 1.0. The available SKRepeatMode values are:
• Clamp: This mode clamps the value to the range of time values found in the
sequence. If, for example, the last keyframe in a sequence had a time value of
0.5, any time from 0.5 to 1.0 would return the last keyframe value. This is the
default mode.
• Loop: This mode causes the sequence to loop. If, for example, the last keyframe
in a sequence had a time value of 0.5, any time from 0.5 to 1.0 would return the
same value as from 0.0 to 0.5.
OK! It's time to create a new group to hold all of your particle systems. With
GameScene.swift highlighted, select File\New\Group from the main menu.
Name this group Particles and click Enter to confirm.
raywenderlich.com 423
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
With the Particles group highlighted, select File\New\File... from the main
menu. Select iOS\Resource\SpriteKit Particle File.
Click Next. On the next screen, you'll see a drop-down that contains a number of
different particle templates.
raywenderlich.com 424
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
These templates give you a handy starting point from which to create your own
particles. The items in the list are common particle configurations that you can
adapt to your own needs. The following images show what each template looks
like:
Generally you should choose whichever is closest to the effect you're trying to make
and then tweak it from there. Although you could use any of these templates to get
started on the lava, fire is the closest thing to lava, so select the Fire template from
raywenderlich.com 425
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
the list.
Click Next once more and enter Lava for the file name.
Note: When you add your first particle file to a project, Xcode automatically
adds a second file, spark.png, alongside it. This image file is the default
texture for all particle templates built into Xcode, apart from one: the Bokeh
template has its own default image called bokeh.png. You don't have to use
this texture for your particle system. If you want to use your own texture,
simply add it to your project and select it in the Xcode particle emitter editor
(see below).
Feel free to take a moment to play around with the settings in the inspector to see
how the particle system behaves in response.
Notice the Particle Texture property? It's set to use the default texture file,
raywenderlich.com 426
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
spark.png. This is where you would set the texture to one of your own, if you
desired.
Making lava
The best way to begin making lava is by changing the color of the template. The
default color is close to what you want, but you'll get a softer lava effect if the color
ramps up from black to a reddish-orange glow.
Locate the Color Ramp property in the editor and click on the color stop at the left
of the color selection panel. This will display a standard OS X color picker. Change
the color in the picker and watch the color of the particles change.
Select the Color Sliders tab in the color picker and make sure that the drop-down
shows RGB Sliders. Now set the first color stop to black by setting the red, green
and blue sliders all to 0. The color ramp in the editor is actually editing the
emitter's particleColorSequence. You're going to add two more stops (keyframes)
to the sequence.
Click at about the 25% mark of the color ramp to create a new color stop. Enter 99
for the red value, 50 for the green and 0 for the blue. Next, click at the end of the
color ramp to create another color stop. Enter 219 for the red value, 66 for the
green and 0 for the blue.
With the color sequence set, you can start to edit the shape, speed and position of
the emitter. At the top of the particle editor pane, find Particles Birthrate and
change the value to 50 with a Maximum of 0. Then set Lifetime Start to 1.5 with
a Range of 0.
raywenderlich.com 427
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
This causes the emitter to generate 50 particles per second, with each particle
living for 1.5 seconds. To optimize performance, you want to keep the number of
particles as low as possible to get the effect you want. So 50 particles per second *
1.5 seconds of lifetime per particle means only about 75 particles will be onscreen
at one time.
For now, set Position Range X to 128 and Y to 0. You'll change this in code later.
Change Angle to 90 with a Range of 360. This causes the particles to emit from
every direction.
Next, set the Speed Start and Range to 0. Lava is thick and slow, so you don't
need any speed here.
Now, set Alpha Start to 1 with a Range of 0.2 and a Speed of -0.2. The particles
will start with an opacity between 80–100% and then fade out slowly.
Finally, give your lava some mass by setting Scale Start to 9 with a Range and
Speed of 0. Your particle system is beginning to look a lot more like hot lava!
raywenderlich.com 428
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Now that you've configured your lava particle system, it's time to see it in action.
Switch to GameScene.swift and implement the following method:
func setupLava() {
lava = fgNode.childNodeWithName("Lava") as! SKSpriteNode
let emitter = SKEmitterNode(fileNamed: "Lava.sks")!
emitter.particlePositionRange = CGVector(dx: size.width * 1.125, dy:
0.0)
emitter.advanceSimulationTime(3.0)
emitter.zPosition = 4
lava.addChild(emitter)
}
First, you grab a reference to the lava SKSpriteNode that you created in the scene
editor; you're going to attach the emitter to this.
Then you create an SKEmitterNode, providing the filename of the SKS file you
created.
The next line sets the particlePositionRange property so that particles are
generated along the entire width of the screen, with a little overlap.
Next, you advance the simulation time so your lava is extra hot! You set the
zPosition to 4 so the lava covers everything else.
Replace that line with a call to set up your lava particle system:
setupLava()
This places the marine a little higher above the lava in it's collision. Without this
modification, the marine sinks a little too far into the lava before his boost.
raywenderlich.com 429
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Note: Be sure to test on an actual device rather than the simulator, as particle
systems do not perform well on the simulator.
raywenderlich.com 430
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
select Spark from the list of templates and click Next. Name the file
SmokeTrail.sks and click Create.
You now have a new particle file you can edit. Notice the spark template emits
particles continuously. For this effect, you only want the smoke to last for a second
or two. To accomplish this, start by setting Particles Birthrate to 200 and
Maximum to 200. Then set Lifetime Start to 1 and Range to 0.5.
Set both the Position Range X and the Position Range Y to 24. Set Angle to 0
and Angle Range to 360. These settings give your smoke a little turbulence.
Now to slow things down a bit, set Speed Start to 50 and Range to 100. Smoke
tends to float, so remove the gravity effect by setting both Acceleration X and Y
to 0.
You want the smoke particles to start fairly large, then shrink and fade out. To do
this, set Alpha Start to 1, Range to 0 and Speed to -0.25. Then set Scale Start
to 1.6, Range to 1 and Speed to -2.
Next, you'll color your particles gray so that together, they look more like smoke.
Begin by setting Color Blend Factor to 1 and Range to 1. This will apply random
values of color to the existing texture color. Now click on the left-most color stop on
the Color Ramp to bring up the color picker, and set Red, Green and Blue all to
100.
Click at about the 75% mark to create a new color stop, and set Red, Green and
Blue to 177.
raywenderlich.com 431
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Finally, create a third color stop at the right-most edge of the color ramp and set
Red, Green and Blue to 255. Lastly, you don't want the smoke to glow, so set
Blend Mode to Alpha.
You have your smoke particles, so it's time to set them loose. Switch back to
GameScene.swift. You'll be creating another trail effect for your hero later, so add
these helper methods:
The first method helps by creating an SKEmitterNode with the specified SKS file and
attaching it to the player sprite. It then returns the SKEmitterNode, so that you can
clean it up later by passing it to removeTrail(_:).
Now within the Player States group, open Lava.swift and add the following code
to the top of didEnterWithPreviousState(_:):
You create a smoke trail particle system, add it to the player, and after three
seconds, remove it. Since you only want the smoke trail effect when the player
enters the Lava state, you only need to add the code to this file.
Build and run, and let the hero fall into the lava.
raywenderlich.com 432
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Hey, what's going on? Where's the smoke? The particle system is there, but it's
behind the player sprite, and there are no forces influencing the trajectory of the
particles.
Something similar is going on with your particle system. You need to tell the
emitter that there's a world outside of the sprite to which it's attached.
SKEmitterNode has a property called targetNode that allows you to set the node that
renders the emitter's particles. The initial properties of new particles are based on
the emitter, but in future frames, the particles are treated as children of the target
node.
trail.targetNode = fgNode
Now build and run. Each time your hero touches the lava, a trail of smoke will
appear!
raywenderlich.com 433
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Congratulations! Besides adding hefty doses of fire and smoke to Drop Charge,
you've developed your understanding of particle systems and their usefulness.
You've learned how to create particle systems and configure their properties, both
programmatically and within the Xcode editor, and how to load them and deploy
them in your game.
But probably what's made the biggest impression is the dramatic effect these
systems can have on your player's experience—and this is just the beginning. In
the next chapter get ready to make this game even sweeter - through the power of
Juice! :]
Challenges
Now it's time to practice creating particle systems on your own. With each
challenge, you'll create a particle system for your game using Xcode's particle
emitter editor.
If you get lost or feel stuck, have a look at the particle system SKS files in the
challenge\particles folder. But most importantly, have fun and feel free to
experiment!
raywenderlich.com 434
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
raywenderlich.com 435
2D iOS & tvOS Games by Tutorials Chapter 16: Particle Systems
Note: Don't worry about adding the code to spawn these particle effects yet;
you'll handle that in the next chapter.
raywenderlich.com 436
17 Chapter 17: Juice Up Your
Game
By Michael Briscoe
Pop quiz: What's the difference between a good game and a great game? Why does
one game delight its players while another is greeted with indifference? Why do
some games have raving fans and stellar reviews? And what is the magical potion
named "polish" that you're supposed to sprinkle on your games to make them
awesome?
Great games are filled to the brim with droves of details that are often so subtle
you might not even consciously notice them while you're playing. Polishing a game
means paying attention to these details. Don't stop developing once your game
reaches a playable state, and don't rush it to the App Store. Push your game
further. Spice it up! Add polish!
"Juice" is a special type of polish that is easy and fun to add, and serves to bring
joy and exuberance to your game. When a game is juicy, it feels alive—every
interaction between the player and the game world results in a stimulating
response.
For example, when two objects collide, you shouldn't just see it happen on the
screen—that collision should look so convincing that you can almost feel it in your
body. Playing a juicy game is a visceral experience.
raywenderlich.com 437
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
The great thing about juice is that you don't need to have a large art budget or hire
expensive consultants with impressive resumes. Instead, you can use simple
animation effects—like scaling, rotation and movement, particle effects, music and
sound effects. Most of these things are already in your toolkit; others, like music
and sound effects, you can find online or create with free or inexpensive software.
This is wonderful news for programmers like you and me!
On their own, none of these effects are terribly exciting, but when combined, every
interaction within the game world results in a cascade of visual and audible
feedback that keeps players coming back for more. That is what it means to make
your game juicy.
In this chapter, you'll take Drop Charge and juice it up by adding a myriad of details
to it. Although it's a good game now, adding juice will make it totally awesome!
raywenderlich.com 438
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Note: This chapter begins where the previous chapter’s challenge left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project for this chapter to pick
up where the previous chapter left off.
Before you begin randomly adding effects, you need to know where to apply them.
If you add them arbitrarily or without consideration, your game runs the risk of
confusing and distracting players.
But there's good news! You simply need to follow this three-step algorithm to plan
your game's effects:
1. List the actors: First, make a list of all of the objects that play a role in your
game, often called the actors. For example, the space hero and a platform
object are two of your actors.
2. List the interactions: Second, make a list of the interactions that exist
between the actors. For example, the hero collides with a coin to collect it. An
object can also perform interactions with itself, like moving or changing state.
For example, the hero could be moving upwards, or falling.
Simple enough, right? Good. Now it's time to carry out steps 1 and 2 with Drop
Charge. Then later, you'll repeatedly apply step 3.
• The hero: Your space hero, frantically jumping to escape his exploding ship.
• Breakable platforms: Fragile platforms that break when the hero lands on
them, providing very little respite from his escape.
raywenderlich.com 439
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
• Normal coins: These help the hero by providing a boost to his upward
movement.
• Special coins: These provide a greater boost to the hero, propelling him closer
to safety.
• Lava: The hero must avoid the rising lava at all costs or risk a serious hot foot!
• Background: This includes the scrolling images of the ship's machinery and
vents. The images serve no real purpose in the game, other than to make it look
more interesting.
• The screen itself: The game world is the container for all of the other actors.
• The player: Yes, the player is an actor in the game, too. In fact, the player has
the most important role! In this game, the hero is the player's avatar.
• The hero interacts with platforms, as when he rests or jumps up the screen.
• The hero interacts with coins, as when he collects and destroys coins for a
speed boost.
• The hero interacts with the lava, as when he gets a boost and a hot foot, or
maybe even dies.
• The hero interacts with the game world, as when he performs an action like
jumping, falling or changing direction.
• The player interacts with the screen, as when she moves the device or taps
the screen.
• A game rule interacts with the game world, as when the "game over"
conditions are satisfied.
raywenderlich.com 440
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
All of these interactions are opportunities for juice—like ripe fruit hanging from a
tree, just waiting for you to pluck and squeeze. But what effects should you use?
1. Music and sound effects: Snappy music and sound effects can set the mood
of your game and enhance your visual effects.
3. Particle systems: You've already seen how adding particle effects, like
explosions and smoke, can add high-octane juice.
4. Screen effects: Shaking and/or flashing the entire game world more deeply
engages the player and conveys a sense of urgency.
5. Sprite effects: You can change a node's size, rotation, color and transparency
to create interesting effects and enhance visual cues.
All of these effects can be temporary or permanent, immediate or animated,
performed by themselves or—and this is where the magic happens—in combination
raywenderlich.com 441
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
with other effects. When you add a bunch of these effects together, you can make
the entire screen jump and bounce. That's when things get juicy!
You can add most of these effects with a simple SKAction. That's the wonderful
thing about these effects—they're incredibly simple to program, so adding them to
your game is a quick win. Be warned, however: Once you start adding special
effects, it's hard to stop!
Getting started
Open your DropCharge project. If you're skipping ahead, you can find the project
in starter\DropCharge.
Once you have your project open, drag the starter\resources\sounds folder to
the Xcode project navigator to import them. Make sure Copy items if needed,
Create Groups, and the DropCharge target is checked. Once you're done, you
should see them in your project navigator:
raywenderlich.com 442
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Drop Charge seems a little tepid without sound. To fix that, you'll start by adding
music.
With iOS 9, Apple gave Sprite Kit a new node, the SKAudioNode, which makes it easy
to play sound files from within your game. What used to take several lines of code
can now be done in two!
The first part of this method checks to see if you've already added the
backgroundMusic node to the scene, and if so, removes it. Next, you initialize an
SKAudioNode with the file name passed into the method, setting its autoplayLooped
property. Then, you add the node to your scene.
Because you set the autoplayLooped to true, your music file will automatically play
once it's added to the scene; it will repeat until you remove it. Now you need to call
this method with a sound file.
playBackgroundMusic("SpaceGame.caf")
Build and run Drop Charge. If all went well, you'll hear a jazzy little tune welcoming
you to the game.
scene.playBackgroundMusic("bgMusic.mp3")
This code executes when the game enters the Playing state, replacing the calmer
raywenderlich.com 443
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
music with a more frantic beat. Build and run to hear the results. Don't forget,
you'll need to start the gameplay to hear the music change.
Not bad, but you can juice it up even move by adding some background noise. Go
back to GameScene.swift and add this variable to the properties:
if bgMusicAlarm != nil {
bgMusicAlarm.removeFromParent()
} else {
bgMusicAlarm = SKAudioNode(fileNamed: "alarm.wav")
bgMusicAlarm.autoplayLooped = true
addChild(bgMusicAlarm)
}
When the game enters the Playing state and makes the call to
playBackgroundMusic(_:), backgroundMusic is no longer nil. From there, you check
to see if bgMusicAlarm is nil. If it is, you initialize it and add bgMusicAlarm to the
scene.
scene.playBackgroundMusic("SpaceGame.caf")
This changes the background music to the original music, and since the
bgMusicAlarm variable isn't nil, playBackgroundMusic(_:) will remove it from the
scene, silencing the alarm sound.
Build and run and notice a cool alarm sound effect as you play the game, and that
the game reverts back to the peaceful music on game over.
raywenderlich.com 444
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
let soundExplosions = [
SKAction.playSoundFileNamed("explosion1.wav",
waitForCompletion: false),
SKAction.playSoundFileNamed("explosion2.wav",
waitForCompletion: false),
SKAction.playSoundFileNamed("explosion3.wav",
waitForCompletion: false),
SKAction.playSoundFileNamed("explosion4.wav",
waitForCompletion: false)
]
You define a series of SKAction constants, each of which will load and play a sound
file. Because you define these actions before you need them, they are preloaded
into memory, which prevents the game from stalling when you play the sounds for
the first time. You also create an array of explosion sound effects that you'll use to
play a random boom.
scene.runAction(scene.soundBombDrop)
scene.runAction(SKAction.repeatAction(scene.soundTickTock, count: 2))
When the game enters the WaitingForBomb state, it will play bombDrop.wav,
followed by tickTock.wav, before transitioning to the Playing state.
But no bomb would be complete with an Earth-shattering kaboom, right? Add this
line to willExitWithNextState(_:), after the line that removes the bomb from the
scene:
scene.runAction(scene.soundExplosions[3])
Build and run to hear the bomb tick, with a huge explosion!
raywenderlich.com 445
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
It's time to add the sound effects for when the player collides with the coins and
platforms. Open GameScene.swift and scroll to didBeginContact(_:). Replace the
switch statement with the following:
switch other.categoryBitMask {
case PhysicsCategory.CoinNormal:
if let coin = other.node as? SKSpriteNode {
coin.removeFromParent()
jumpPlayer()
runAction(soundCoin)
}
case PhysicsCategory.CoinSpecial:
if let coin = other.node as? SKSpriteNode {
coin.removeFromParent()
boostPlayer()
runAction(soundBoost)
}
case PhysicsCategory.PlatformNormal:
if let _ = other.node as? SKSpriteNode {
if player.physicsBody!.velocity.dy < 0 {
jumpPlayer()
runAction(soundJump)
}
}
case PhysicsCategory.PlatformBreakable:
if let platform = other.node as? SKSpriteNode {
if player.physicsBody!.velocity.dy < 0 {
platform.removeFromParent()
jumpPlayer()
runAction(soundBrick)
}
}
default:
break
}
raywenderlich.com 446
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Give it a go! Build and run, and listen to the sweet sound (effects) when you hit
platforms and coins.
scene.runAction(scene.soundHitLava)
scene.runAction(scene.soundGameOver)
Both of the lines you've added simply play their respective sound files after the
marine collides with the lava or enters the dead state.
Build and run. Play your game for a bit, and then play it again—but this time, with
the sound muted. Notice how the interactions between your game actors are
enhanced by the simple addition of sound. But you're just getting started with the
juice!
Frame animation
Now you're going to get a bit more visual by adding texture animation.
At the moment, the game objects in Drop Charge seem a little static, and the coins
blend in with the background. You can fix that by animating the textures of
SKSpriteNode.
raywenderlich.com 447
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
As you've done before, create a new file in your Scene Files group with the iOS
\Resource\SpriteKit Scene template. Name this scene Coin.sks.
Now from the Media Library, drag powerup05_1 into the scene. Set its Name to
Overlay, and its Position to (0, 0).
Under Physics Definition, choose Bounding Circle for the Body Type. Also,
uncheck Dynamic, Allows Rotation, Pinned and Affected By Gravity. Set the
Category Mask to 8 (the bit flag for a coin), the Collision Mask to 0, the Field Mask
to 0 and the Contact Mask to 1 (the bit flag for the player).
Make sure the Utilities Area is visible on the right and that the Attributes Inspector
is selected. From the Object Library, drag an AnimateWithTextures action
toward the scene; the action editor will expand if it's not already visible. Drop the
action on Overlay within the action editor.
Within the Utilities Area, switch to the Media Library and drag and drop
powerup05_1 through powerup05_6 into the Textures attribute. Set the
raywenderlich.com 448
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Duration to 0.5.
You can preview your animation action by clicking on the Animate button at the
top of the action editor. The toolbar will turn blue, and the Animate button will
change to Layout, as your animation begins to play.
The coin animation only cycles once, but you can do better than that by making the
coin rotate repeatedly. Luckily, you can specify how the action loops along its
timeline.
raywenderlich.com 449
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
you move the cursor to the edges of the action, it will appear as a bar with arrows
pointing to the left and right, indicating you can scale the action duration over time
by dragging its edges.
Get a feel for how this works by dragging the action to a new location. Next, try
scaling its duration over several seconds. Click on Animate to preview your action
to see how it's changed. When you're finished, make sure to return your action to
the beginning of the timeline and its duration to 0.5.
Click on the circular arrow icon on the bottom-left of the action to bring up the
Looping pop-up. You could use this pop-up to set a finite number of loops by
clicking on the plus (+) and minus (-) buttons, but you want this action to loop over
and over, so click on the loop forever icon on the left—it looks like a sideways 8.
Notice how the action representation in the timeline has changed to reflect its
looping status.
Now that you have your coin reference scene, you'll also need one for the special
coin. As you did before, create a new scene and name it CoinSpecial.sks. This
time drag the blue coin, powerup01_1, into the scene. Set its Name to Overlay,
and its Position to (0, 0).
Under Physics Definition, choose Bounding Circle for the Body Type. Also,
uncheck Dynamic, Allows Rotation, Pinned and Affected By Gravity. Set the
Category Mask to 16 (the bit flag for a special coin), the Collision Mask to 0, the
Field Mask to 0 and the Contact Mask to 1.
You could use some new iOS 9 features, such as action references or reference
nodes; but at the time of this writing these features were a bit buggy. For now,
you'll have to swap out the old static coins for the new animated ones—in code.
raywenderlich.com 450
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Start by opening GameScene.swift and add the following variables to the class
properties:
These variables will hold references to the two coin scenes you just created. Now
add this code to setupNodes() just after breakDiagonal...:
coinRef = loadOverlayNode("Coin")
coinSpecialRef = loadOverlayNode("CoinSpecial")
Just like the platform nodes, you're loading the overlay node into your reference
properties. Next, implement this new method just below loadOverlayNode(_:):
1. First it loads a scene file, then sets contentTemplateNode to the "Overlay" node.
4. Position and add the new animated coin node, then remove the old static one.
Now that you have the loadCoinOverlayNode(_:) method, let's put it to use.
raywenderlich.com 451
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
coin5Across = loadOverlayNode("Coin5Across")
coinDiagonal = loadOverlayNode("CoinDiagonal")
coinCross = loadOverlayNode("CoinCross")
coinArrow = loadOverlayNode("CoinArrow")
coinS5Across = loadOverlayNode("CoinS5Across")
coinSDiagonal = loadOverlayNode("CoinSDiagonal")
coinSCross = loadOverlayNode("CoinSCross")
coinSArrow = loadOverlayNode("CoinSArrow")
coin5Across = loadCoinOverlayNode("Coin5Across")
coinDiagonal = loadCoinOverlayNode("CoinDiagonal")
coinCross = loadCoinOverlayNode("CoinCross")
coinArrow = loadCoinOverlayNode("CoinArrow")
coinS5Across = loadCoinOverlayNode("CoinS5Across")
coinSDiagonal = loadCoinOverlayNode("CoinSDiagonal")
coinSCross = loadCoinOverlayNode("CoinSCross")
coinSArrow = loadCoinOverlayNode("CoinSArrow")
Build and run to see your animation action and coin references, at work in the
game.
Whether or not you're a space marine, those coins now look a lot more enticing.
raywenderlich.com 452
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
But what if you want the animation to occur when triggered by specific actions,
such as when your brave space hero is jumping? That's when it makes more sense
to create those animation actions in code.
In this section, you're going to add some animation to the space marine himself. To
do this, open GameScene.swift and add the following variables to the class
properties:
These are empty SKAction variables that you'll initialize in just a moment. But
before you do, add these two helper methods:
The first method loads several textures into an array. Then, it creates an
animateWithTextures action using the textures and the timePerFrame for its
parameters.
• It checks to see if there's a current animation and if there is, makes sure it's not
the same as the input animation.
• If the animation passes these tests, then the method removes any running
raywenderlich.com 453
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
animation actions.
• Finally, the method sets the curAnim to the input anim, so you can properly track
it.
Scroll to didMoveToView(_:) and add these lines to initialize your animation actions:
animJump = setupAnimWithPrefix("player01_jump_",
start: 1, end: 4, timePerFrame: 0.1)
animFall = setupAnimWithPrefix("player01_fall_",
start: 1, end: 3, timePerFrame: 0.1)
animSteerLeft = setupAnimWithPrefix("player01_steerleft_",
start: 1, end: 2, timePerFrame: 0.1)
animSteerRight = setupAnimWithPrefix("player01_steerright_",
start: 1, end: 2, timePerFrame: 0.1)
Now that you've created your player animations, it's time to put them to use. Open
Jump.swift and add this method:
Because you want the Jump state to track the hero during his jump cycle, you use
updateWithDeltaTime(_:). This checks the player sprite's horizontal velocity and
plays the appropriate animation action for the Jump state.
Open Fall.swift. There's only one animation for falling, so for this state, replace
didEnterWithPreviousState(_:) with the following:
There's only one thing left to do before testing this. Switch back to
GameScene.swift and add this line to update(_:):
playerState.updateWithDeltaTime(deltaTime)
This ensures the Jump state executes its updateWithDeltaTime(_:), so it can change
the hero's jump animation.
Build and run to see Drop Charge in all its animated glory!
raywenderlich.com 454
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Things are really starting to get juicy, but you're going to take it up another notch
by adding particle systems.
Particle effects
It's time to put those particle systems you created in the last chapter to use,
starting with a motion trails effect for the hero. This will enhance the illusion that
he's moving quickly, eager to escape the ship.
For this effect, you want to temporarily turn off the trails when the hero has his hot
foot. To do that, you need a reference to the player trail's SKEmitterNode.
scene.playerTrail = scene.addTrail("PlayerTrail")
Here, you use the addTrail(_:) method you created for adding the smoke trail,
passing in the the PlayerTrail filename.
raywenderlich.com 455
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
When the hero hits the lava, you currently can't see the smoke trail because the
motion trail covers it.
To fix that, open Lava.swift and add the following line to the top of
didEnterWithPreviousState(_:):
scene.playerTrail.particleBirthRate = 0
The particleBirthRate emitter property from the last chapter comes in handy here,
stopping the flow of particles just before you add the smoke trail.
In the first part of this method, you check to make sure the last player state wasn't
Lava. If it was, you skip the rest. If the previous state wasn't Lava, then you restore
the playerTrail birthrate to 200, provided it's not 0.
Build and run. Now you'll see the motion and smoke trails playing nicely together.
raywenderlich.com 456
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Random explosions
Let's step up the chaos factor and add more juice by creating random explosions in
the background to enhance the sense of the ship disintegrating.
You'll use these properties to track when to add your random explosions.
func createRandomExplosion() {
// 1
let cameraPos = getCameraPosition()
let screenSize = self.view!.bounds.size
1. First, you get the camera position and generate a random position within the
viewable part of the game world.
2. Next, you get a random number to play a random sound effect from the
soundExplosions array.
3. Finally, you create an explosion with a random intensity. Then you set its
position, removing it after two seconds, and add it to the background node of
the game world.
Before you can see your explosions, you need to do a couple more things.
raywenderlich.com 457
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
createRandomExplosion()
}
}
This method checks periodically to see when to set off an explosion by comparing
the last explosion time with a randomly chosen time in the future. When that time
is reached, the method fires createRandomExplosion().
scene.updateExplosions(seconds)
Power-up particles
There's another actor interaction that's begging for an effect: when the hero
collides with the coins. Right now, the coins simply disappear from the screen and
give him a boost. Wouldn't it be cool to add particles to the mix?
raywenderlich.com 458
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
This method takes a filename and a sprite node, does all the work of creating and
positioning your particle system and then removes the sprite.
Now, scroll to didBeginContact(_:) and make the following changes to the switch
statement:
coin.removeFromParent()
Do the same within case PhysicsCategory.CoinSpecial: but replacing with this line:
While you're there, add the particle system for contact with the breakable
platforms.
platform.removeFromParent()
raywenderlich.com 459
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
scene.runAction(scene.soundExplosions[3])
scene.screenShakeByAmt(200)
This code creates a large explosion and positions it over the gameOver sprite,
complete with screen shake and sound effects.
Build and run to see your juicy power-ups, platforms, and particle mayhem!
Screen effects
You've juiced things up with sound, animation and particles, so it's time to turn to
screen effects, which can really bring things to life by affecting the entire game
world. For Drop Charge, you'll implement a screen shake to simulate transient
spatial movement similar to an earthquake—or in your case, a shipquake!
raywenderlich.com 460
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
1. It grabs a reference to the world node, resets its position and removes any
previous "shake" actions. This code makes sure that only one screen shake
action is running at a time—preventing pandemonium!
2. It creates a CGPoint based on the input amount. You only want the screen to
shake vertically, so you set x: to 0.
4. Using the "shake" key, the method executes the action on the world node so
that subsequent calls to the method can remove it.
Now you need to put this helper method to use. Juicing up the bomb explosion is a
good way to start!
scene.screenShakeByAmt(100)
Build and run. You'll see just how much shaking the screen can invigorate the
game.
raywenderlich.com 461
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
scene.screenShakeByAmt(50)
The next time the player hits the lava, she'll really know it!
screenShakeByAmt(40)
Now the screen will shake a little when the hero collects a special coin.
if randomNum == 3 {
screenShakeByAmt(10)
}
Because it's possible to overuse the screen shake effect, you only shake the screen
a small amount, and only with the largest explosion. You don't want to make the
player queasy!
raywenderlich.com 462
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Sprite effects
There are a lot of sprite properties and actions you can use to achieve a variety of
effects. For example, often games will change a sprite's scale to convey a sense of
elasticity, like that of a bouncing ball, or change its color to indicate a transition to a
new mode.
In the first section of this chapter, you added an alarm sound to create a sense of
urgency. Now you'll juice that up by adding an oscillating red-light effect.
raywenderlich.com 463
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Open GameScene.swift and add the following variable to the class properties:
You'll use this method to determine if a node is visible within the game world. This
will help with performance and memory management, as you don't want unused
objects taking up valuable resources.
1. This part of the code calculates the oscillation of color to apply to each
background sprite.
2. Here, you loop to iterate through all of the background nodes in the game
world.
raywenderlich.com 464
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
3. If the node isn't visible, you remove it to free up resources; otherwise, you set
its color to red and blend it according to the amount you calculated in step 1.
Since this effect is generated over time, you need to add it to your game's update
loop.
scene.updateRedAlert(seconds)
Bouncing platforms
Finally, you're going to add a method to make the platforms bounce when the
player lands on them. As if things weren't sketchy enough for our poor space
marine!
if breakable == true {
emitParticles("BrokenPlatform", sprite: sprite)
}
}
case PhysicsCategory.PlatformNormal:
if let platform = other.node as? SKSpriteNode {
if player.physicsBody!.velocity.dy < 0 {
platformAction(platform, breakable: false)
jumpPlayer()
runAction(soundJump)
}
}
case PhysicsCategory.PlatformBreakable:
if let platform = other.node as? SKSpriteNode {
if player.physicsBody!.velocity.dy < 0 {
platformAction(platform, breakable: true)
jumpPlayer()
runAction(soundBrick)
}
raywenderlich.com 465
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
When the player sprite makes contact with either the normal or breakable
platforms, platformAction(_:breakable:) is now called to bounce the platform.
Finishing touches
Before you build and run, you need to do some cleanup and a performance tweak.
First, scroll to updateLevel(). Once there, add the following lines of code:
Like the code you used in the red-light effect, this code iterates through all of the
platforms and coins in the scene and removes any obsolete objects. This avoids
runaway memory usage - like a pile of offscreen cat ladies in Zombie Conga.
func setPlayerVelocity(amount:CGFloat) {
player.physicsBody!.velocity.dy =
max(player.physicsBody!.velocity.dy, amount * gameGain)
}
This moves the hard-coded gain property to the gameGain property you created
earlier - this will make it easier to tweak later on in one of the challenges.
Build and run your game, and behold the abundance of juice!
raywenderlich.com 466
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
Challenges
Putting what you've learned into practice, here are a couple of challenges for you.
As always, you can find solutions to these challenges in the challenge folder—but
believe in yourself and try your very best first. Good luck!
• Run the action on the player sprite within the Jump, Fall and Lava player states.
raywenderlich.com 467
2D iOS & tvOS Games by Tutorials Chapter 17: Juice Up Your Game
#if os(iOS)
import CoreMotion
#endif
• Adjust the gameGain constant to a lower number to keep the hero onscreen.
For some ideas of more ways to add Juice to your game, check out Ray's AltConf
talk on the matter here: http://bit.ly/1FViSbC
Also, check out what other developers are doing with their games. Make a list of the
things you like about them. The best thing about adding juice is that you're only
limited by your imagination!
raywenderlich.com 468
Section IV: GameplayKit
In this section, you'll learn how to use iOS 9's new GameplayKit to improve your
game's architecture and reusability, along with adding pathfinding and basic game
AI.
In the proces, you'll create a fun tower defense game called Dino Defense where
you construct a perfect defense to save your village from an onslaught of angry
dinosaurs!
raywenderlich.com 469
18 Chapter 18: Entity-Component
System
By Toby Stephens
In the next few chapters, you're going to learn about a cool new framework
introduced in iOS 9 called GameplayKit. In the process, you're going to develop a
tower defense game named Dino Defense.
In tower defense games, the player must place towers on a map to stop invading
enemies from reaching her home base. In Dino Defense, hoards of dinosaurs are
attacking the player's village, and it's up to the player to defend the townsfolk from
the onslaught.
• Chapter 18, Entity-Component System: You are here! You'll learn all about
modeling your game's objects using the new GKEntity and GKComponent objects
provided with GameplayKit, and you'll use what you've learned to implement
your first dinosaur and tower.
• Chapter 20, Agents, Goals and Behaviors: Finally, you'll add a second
raywenderlich.com 470
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
dinosaur to your game that will use a GKAgent with GKGoal and GKBehavior objects
to move across the scene as a more organic alternative to pathfinding.
Getting started
The purpose of the chapters in this section of the book is to take a deep dive into
some of GameplayKit's features. To keep focused on GameplayKit, this chapter
includes a starter project that already contains a lot of the Sprite Kit code required
to get you to a point where you can implement the entities and components.
The starter project takes care of loading the game scene, the win and lose scenes
and an initial ready scene. It prepares the background music for you, and manages
a HUD for displaying the player's gold, remaining lives and the approaching wave of
dinosaurs. This should all be review from previous chapters in this book.
Build and run. You'll briefly see a loading screen with the Dino Defense logo before
being presented with the ready scene.
There are two things in particular that are important to understand about this
project: game layers, and texture atlases.
Game layers
The starter project contains a single scene in which the entire game plays,
implemented in GameScene.sks and GameScene.swift.
The scene is split into a number of top-level nodes that comprise logical layers of
the game, one on top of the other. You can see those layers in this diagram:
raywenderlich.com 471
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
• Background: This layer is a sprite node with the texture for the background of
your game. It's also the layer on which you'll be placing nodes for the possible
tower locations.
• Shadows: This is a special layer for all the shadows of your game sprites. It's
important that each shadow is always under the sprites—even the sprites that
don't possess that particular shadow.
• Sprites: Sitting above the shadows layer is the sprites layer. This is where you'll
add your dinosaurs, towers and obstacles—the key game objects.
• HUD: This is a node for displaying game information to the player, such as the
gold available to build towers.
• Overlay: You'll use the top-level layer to display in-game menus, such as the
win and lose screens.
raywenderlich.com 472
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Texture atlases
Up until this point in the book, you have stored the images for your game in Sprite
Kit's asset catalog. When you do this, Sprite Kit automatically organizes your
images into a set of texture atlases.
A texture atlas is basically one big image that contains all your other images as
sub-images - this is done to improve your game's performance. For example, here's
what your texture atlas might look like for Cat Nap:
An alternative to using Sprite Kit's asset catalog is to save your images inside
folders with an .atlas extension. If you do this, Sprite Kit will create a texture atlas
for each folder name.
This is the approach that Dino Defnense takes. If you take a look at the
ArtAndSounds folder, you will see several folders with an .atlas extension:
The advantage of manually specifying the texture atlases this way is that you can
have images with the same filename in different texture atlases. For example, the
dinosaurs in this game all have images for walk animations that go from
raywenderlich.com 473
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Sprite Kit provides a class called SKTextureAtlas that allows you to load a texture
atlas and retrieve a texture from it (along with other operations like enumerationg
all textures). Here's an example of using it:
You will be using this later in the chapter to retrieve textures to use for the sprites
in this game.
At this point, feel free to take a peek through the rest of the project to get a feel for
what's inside. Once you're feeling good, keep reading to start your tour of
GameplayKit!
Introducing GameplayKit
When writing a complex game, it's important to plan a good architecture for the
game's many elements. If you've developed a game before, I'm sure you've sat
down with a pen and paper and thought about each of the components in the game
—mapping those components to an object hierarchy.
For example, with the tower defense game that you're about to write, you'll have
enemies (dinosaurs), towers (wood and rock) and a number of other game objects
(projectiles for the towers, obstacles in the scene). Thinking about code reusability,
you might plan your object model to look something like this:
The GameObject might contain code to render itself, determine the objects' physics
and other generic functions that apply to each game object. The Tower class might
provide extra information for towers, and the Wood and Rock tower classes might
raywenderlich.com 474
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Assume for a moment that the Tower class contains code for aiming at and
attacking enemies. Now, imagine that your game expands and a new dinosaur—a
particularly aggressive Raptor class—is allowed to attack the towers, rather than
the typical passive behavior.
If you wanted to reuse the attacking code for rate of attack, damage and so forth,
then you would have to move that attacking code up the class hierarchy to the
GameObject class for the new Raptor subclass to use it. You would then have to
explicitly tell your game that T-Rex and Triceratops dinosaurs can't attack towers.
Now imagine this process of moving code up to the GameObject layer continues as
you develop your game. Eventually, your GameObject class will become a bloated
class full of switches. A new way of modeling these concepts is clearly required—
enter GameplayKit.
Entity-Component system
One of the key features of GameplayKit is its Entity-Component system. This
architecture enables you to design your object model based on what the game
objects do as opposed to what they are.
1. First, you create a component class for each thing you want your objects to do
(such as appear on the screen, display a shadow, or shoot).
2. Then, you create an entity classs for each type of object in your game, and add
any components you would like to the entity.
Let's see how this will work in Dino Defense. Your game will have two tower types:
• Wood Towers fire arrows at the dinosaurs. They fire rapidly, but don't do much
damage.
• Stone Towers fire rocks at the dinosaurs, doing more damage and slowing the
dinosaurs down.
raywenderlich.com 475
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Since all of the actual code for executing each of the pieces of functionality is in the
components, the entities are relatively small implementations, and you can reuse
the component code by simply adding the component to the entity.
Each of the components has a specific piece of functionality that it's responsible for
providing:
• The sprite component renders the entity to the scene. This is a Sprite Kit
game, so the sprite component will use an SKSpriteNode to place the entity in the
scene.
• The firing component contains all of the functionality associated with the tower
attacks.
• Finally, the health component renders an entity's health above it in the form of
a health bar. It redraws the health bar when the entity takes damage.
raywenderlich.com 476
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
There are two classes you need to override to use GameplayKit's entity-component
system:
• GKComponent: You override this class for each component you want to add to
your game (such as a sprite component). Often a component will perform
periodic processing in its updateWithDeltaTime(_:) method.
• GKEntity: You override this class for each entity you want to add to your game
(such as a dinosaur component). You then add various components to the entity
(like a sprite component, animation component, shadow component, and health
component).
Here's a diagram of what this will look like for the first two entities you'll add to the
game:
Basically, you will create several subclasses of GKComponent for various behavior you
want your entities to have, and then creating a GKEntity subclass for each entity,
which is basically a collection of components.
It's a different way of thinking, but it makes your code much cleaner and more
flexible. Let's give it a try!
raywenderlich.com 477
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
In the project navigator, create a new group named Components, and add a new
Swift source file to the group named SpriteComponent.swift.
import SpriteKit
import GameplayKit
The SpriteComponent is a very simple class that does nothing more than gives an
entity a representation in a Sprite Kit scene via an SKSpriteNode. To use
SpriteComponent, you simply have to initialize it with your GKEntity subclass, give it
a texture to use, and then add its node to your scene. Let's do that next.
raywenderlich.com 478
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
import UIKit
import GameplayKit
import SpriteKit
This first adds some imports you need. To implement the dinosaur GKEntity, you
need to import GameplayKit. You'll also be using Sprite Kit and some UIKit
elements so you'll need those frameworks as well.
DinosaurType is a simple enumeration of the three dinosaur types that you'll have in
your completed game. Later, you'll add to this enum so it also provides type-specific
dinosaur information, like health and movement speed.
// 1
let dinosaurType: DinosaurType
var spriteComponent: SpriteComponent!
// 2
init(dinosaurType: DinosaurType) {
self.dinosaurType = dinosaurType
super.init()
// 3
let size: CGSize
switch dinosaurType {
case .TRex, .TRexBoss:
size = CGSizeMake(203, 110)
case .Triceratops:
size = CGSizeMake(142, 74)
}
// 4
let textureAtlas = SKTextureAtlas(named: dinosaurType.rawValue)
let defaultTexture = textureAtlas.textureNamed("Walk__01.png")
// 5
raywenderlich.com 479
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
1. These are two properties to store the type of the dinosaur, and keep a reference
to the SpriteComponent you will add to this entity.
2. Here you declare the initializer and store the type of the dinosaur.
3. Here you determine the size of the dinosaur, based on the type of the dinosaur.
4. This starter project comes with a different texture atlas for each type of
dinosaur in the game. Here you find the appropriate texture atlas based on the
type of the dinosaur, and pick out the default sprite to use.
Now that you've created your component and entity, you can add them to the scene
and see this in action!
addEntity(dinosaur)
}
Your implementation of this function will change as you continue through the
chapters of this section of the book. Right now, this function creates start and end
points for your dinosaur and moves the dinosaur from one to the other using an
SKAction. In the next chapter, you'll remove this and use pathfinding to let the
dinosaur make its way across the scene.
raywenderlich.com 480
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Right now Xcode is warning you that the addEntity() method does not exist. You'll
fix this next, but first add this property to the top of the class to keep track of all of
the entities in the game:
// 2
if let spriteNode = entity.componentForClass(
SpriteComponent.self)?.node {
addDinosaur(.TRex)
Congratulations, you have created your first entity and component! Let's review
what you did:
raywenderlich.com 481
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
3. You created an instance of the DinosaurEntity, and added the node of it's sprite
component to the scene (along with running an action to make it move).
You might not be too impressed yet, as this looks like more code than you'd
typically have to write to make a sprite move across the screen, but you'll begin to
see the benefits as you continue to make components and reuse code in your
game.
Let's continue working on the dinosaur by adding another component - one for his
shadow.
import Foundation
import GameplayKit
import SpriteKit
This is a simple class that uses SKShapeNode to draw a semi-transparent black oval
shape to represent a shadow, as you learned how to do in Chapter 12, "Crop,
Video, and Shape Nodes."
Now that you've created your component, you need to add it to your entity. To do
this, open DinosaurEntity.swift and add this property:
raywenderlich.com 482
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
This creates a handy reference to the ShadowComponent you're about to add to the
entity. Next, add this code to the end of init(dinosaurType:):
This creates an instance of ShadowComponent, positions it below the sprite, and adds
it to the entity.
There's one final step - you need to add the shape node to the scene so it appears.
To do this, open GameScene.swift and add the following inside addEntity(:) right
after the "TODO" comment:
// 1
if let shadowNode = entity.componentForClass(
ShadowComponent.self)?.node {
// 2
addNode(shadowNode, toGameLayer: .Shadows)
// 3
let xRange = SKRange(constantValue: shadowNode.position.x)
let yRange = SKRange(constantValue: shadowNode.position.y)
let constraint = SKConstraint.positionX(xRange, y: yRange)
constraint.referenceNode = spriteNode
shadowNode.constraints = [constraint]
}
3. This applies a constraint to make the shadow follow the movement of the
dinosaur sprite, as you learned in Chapter 11, "Advanced Physics".
Build and run, and you should now see a shadow underneath your T-Rex:
raywenderlich.com 483
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Start by creating another Swift source file in the Components group and name it
AnimationComponent.swift.
import SpriteKit
import GameplayKit
This enumeration provides a list of the possible animation states in which your
game objects can be. The String raw value is used in the file name when loading
the animations. For example, Walk frames are named Walk__01, Walk__02 and so
forth.
struct Animation {
let animationState: AnimationState
let textures: [SKTexture]
let repeatTexturesForever: Bool
}
This simple struct represents everything you need to know about a particular
animation. Each animation will have an AnimationState from the enumeration you
just defined, an array of textures for the frames of the animation, and a Boolean
dictating whether or not the animation repeats continuously.
// 2
var animations: [AnimationState: Animation]
// 3
raywenderlich.com 484
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
// 4
init(node: SKSpriteNode, textureSize: CGSize,
animations: [AnimationState: Animation]) {
self.node = node
self.animations = animations
}
}
4. This is a custom initializer that stores the SKSpriteNode on which to run the
animation, and sets the available animations.
Remember from earlier in this chapter that you'll load the frames of each animation
from a texture atlas, using the SKTextureAtlas class. To do this, add the following to
the AnimationComponent class:
return Animation(
animationState: animationState,
textures: textures,
repeatTexturesForever: repeatTexturesForever
)
}
This method takes an image identifier for the animation, such as Walk, and loads
the frames of the animation from the provided texture atlas. It then returns an
Animation struct for the AnimationState.
Now that the AnimationComponent class has a class method to load the animations
for a given atlas and AnimationState, and an initializer that takes the available
animations, you need to provide an action that will actually run the animation on
the node.
raywenderlich.com 485
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
// 1
let actionKey = "Animation"
// 2
let timePerFrame = NSTimeInterval(1.0 / 30.0)
// 3
if currentAnimation != nil &&
currentAnimation!.animationState == animationState { return }
// 4
guard let animation = animations[animationState] else {
print("Unknown animation for state \(animationState.rawValue)")
return
}
// 5
node.removeActionForKey(actionKey)
// 6
let texturesAction: SKAction
if animation.repeatTexturesForever {
texturesAction = SKAction.repeatActionForever(
SKAction.animateWithTextures(
animation.textures, timePerFrame: timePerFrame))
}
else {
texturesAction = SKAction.animateWithTextures(
animation.textures, timePerFrame: timePerFrame)
}
// 7
node.runAction(texturesAction, withKey: actionKey)
// 8
currentAnimation = animation
}
There's a lot going on here, but it's actually pretty simple stuff. Take a look:
1. To be able to remove the existing animation, if there is one, you need a key for
the animation running on the node.
2. The animation frames in this game have been created to run at 30 frames per
second.
3. Here you check that, if there's an existing animation, it's a different state than
the one that's being requested. If this method is called with the same animation
as the one that is currently running, then there's nothing to do.
raywenderlich.com 486
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
6. This is where you create the animation SKAction. It will either be a repeating
animation such as Walk, or a single animation such as Dead.
GKComponentSystem
GKComponentSystem is a class that contains all of the components of a particular type
in your game. For example, you can make a GKComponentSystem that contains all of
the animation components in your game.
This is handy because then you can call the update methods on all instances of a
component at the same time. It's often handy in games to know that processing for
a single "system" (like animation) happens at a known time in your game loop.
raywenderlich.com 487
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Let's see how this works by creating a GKComponentSystem to track your animation
components and call their update methods at the same time. Open
GameScene.swift and add the following property:
Next in addEntity(_:), just after you add the entity to your entities set, add the
following:
This loops through the array of component systems you just created (currently,
only the animation component system) and adds the entity to that system. If the
entity has any aniation components, the component system will keep track of it.
Fianlly, to do the actual update, add the following to the end of update(_:):
Now, with every frame, you update each of the component systems. This means
that when you request an animation from the AnimationComponent, the animation
raywenderlich.com 488
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
You're almost done - you just need to add your new animation component to your
dinosuar entity!
Remember, when you initialize the AnimationComponent, you need to provide the
animations—an array of your Animation structs. To load the animations for the
dinosaur, add the following method:
animations[.Walk] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: "Walk",
forAnimationState: .Walk)
animations[.Hit] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: "Hurt",
forAnimationState: .Hit,
repeatTexturesForever: false)
animations[.Dead] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: "Dead",
forAnimationState: .Dead,
repeatTexturesForever: false)
return animations
}
Now that you have a function for loading the animations, you can add the
AnimationComponent to the entity.
Here, you create the AnimationComponent, providing the sprite node to animate and
also the array of animations. This way, the animation node takes care of the
animation, and the sprite component node takes care of the position of the entity in
the scene.
Theres one last step. Add this line to the bottom of addDinosaur(_:) to set the
raywenderlich.com 489
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
initial animation:
dinosaur.animationComponent.requestedAnimationState = .Walk
Reusing components
Now you're going to see the true power of the Entity-Component model by reusing
the components you created in a tower entity.
Create a new Swift source file in your Entities group named TowerEntity.swift,
and replace the contents of the file with the following:
import UIKit
import SpriteKit
import GameplayKit
Just as with the DinosaurEntity, you create an enum to provide a tower type. The
TowerType will contain more information about each of the towers as you continue
to build your game.
init(towerType: TowerType) {
raywenderlich.com 490
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
super.init()
This is exactly the same thing you did for DinosaurEntity, and like DinosaurEntity,
you'll need to provide a function for loading the animations.
animations[.Idle] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: "Idle",
forAnimationState: .Idle)
animations[.Attacking] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: "Attacking",
forAnimationState: .Attacking)
return animations
}
This function is almost identical to the one in DinosaurEntity, except that the
animations are for the Idle and Attacking states.
And that's it! You created an SKEntity subclass for your towers and added all the
required components. You can see the power behind the Entity-Component model
raywenderlich.com 491
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
here. You didn't have to create a new renderer or code a shadow generator or
animation functions for the TowerEntity—they're all available to you, for free, as the
same code you created for the DinosaurEntity.
To add a tower to your scene, open GameScene.swift and add the following
function:
In the next chapter, you'll position the towers at points determined by the player,
but for the moment, you've added the tower to a hard-coded position in the scene.
Just like with the dinosaur, you've set the position of the SpriteComponent node and
requested the .Idle animation, before adding the entity to the scene.
Finally, to make the call to add the tower to the scene, simply add the following line
to startFirstWave():
addTower(.Wood)
You now have a shiny new wood tower in your scene, and for a relatively small
amount of extra code. The entity is reusing all the functionality provided by your
components. Pretty neat, huh? :]
Now that you've seen how awesome components are, why not create another two.
raywenderlich.com 492
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Firing component
At the moment, your tower just sits there and very politely lets the dinosaur run
past. That's not entirely to plan, and so it's time to give your tower some firepower.
Your two tower types, Wood and Rock, will fire projectiles at the dinosaurs at
different rates. As the rate of fire is determined by the tower type, the TowerType
enum is an excellent place to put this property.
Open TowerEntity.swift and add the following property to the TowerType enum:
The Wood towers will fire twice per second, and the Rock towers will fire once per
second.
Each of the towers will also do a different amount of damage to the dinosaurs, so
add another property to TowerType for the damage inflicted:
The wooden arrows that the Wood tower fires do 20 points of damage, while a
dinosaur will receive 50 points of damage from a rock in the face!
It would also be cool if the towers had different ranges, so add another property to
TowerType:
The arrows from the Wood towers fly a little farther than the rocks from the Rock
towers.
raywenderlich.com 493
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
import SpriteKit
import GameplayKit
Here, you create your new GKComponent subclass and give it the following
properties:
• towerType: This identifies the tower type that's doing the firing, so you can get
the firing rate, damage done and the range of the tower.
• parentNode: This is the tower's SpriteComponent node and is used to add the
projectile node as a child.
• timeTillNextShot: Based on the firing rate of the tower, this holds the time
interval before the tower's next shot.
You've got the start of a firing component here, but before you can implement the
firing of the projectile, you're going to need a projectile entity. The projectile will
need its own sprite in the scene, and this is sounding very much like a GKEntity
subclass with a SpriteComponent.
Create a new Swift source file in the Entities group named ProjectileEntity.swift,
and replace the contents of the file with this:
import SpriteKit
import GameplayKit
init(towerType: TowerType) {
super.init()
raywenderlich.com 494
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
addComponent(spriteComponent)
}
This should all look very familiar. You create a new GKEntity subclass and given it
your SpriteComponent. By attaching the correct projectile sprite as a child to the
SpriteComponent node, you finish the new entity in just a few lines of code. Now you
can get back to firing this projectile from the FiringComponent.
Firing projectiles
Open FiringComponent.swift.
Since the firing of the projectile is a timed feature, the logic will go in the
component's updateWithDeltaTime(_:) function, much like the change of animation
in the AnimationComponent.
timeTillNextShot -= seconds
if timeTillNextShot > 0 { return }
timeTillNextShot = towerType.fireRate
}
If you have a valid target, you take the delta time from the timeTillNextShot and
see if the result is greater than zero—which indicates there's still time to wait until
the next shot is fired. If timeTillNextShot is 0 or less, then you reset
timeTillNextShot, and you're ready to take the shot. That's what you need to code
now.
// 1
let projectile = ProjectileEntity(towerType: towerType)
let projectileNode = projectile.spriteComponent.node
projectileNode.position = CGPointMake(0.0, 50.0)
parentNode.addChild(projectileNode)
// 2
let targetNode = target.spriteComponent.node
projectileNode.rotateToFaceNode(targetNode, sourceNode: parentNode)
// 3
raywenderlich.com 495
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
// 4
let soundAction = SKAction.playSoundFileNamed("\
(towerType.rawValue)Fire.mp3", waitForCompletion: false)
let fireAction = SKAction.moveBy(fireVector, duration: 0.4)
let removeAction = SKAction.runBlock { () -> Void in
projectileNode.removeFromParent()
}
let action = SKAction.sequence([soundAction, fireAction, removeAction])
projectileNode.runAction(action)
1. You create an instance of the ProjectileEntity and position the node toward
the top of the tower, before adding it as a child to the parent node.
2. You get the target node from the target's SpriteComponent and rotate the
projectile to face it.
3. You get the vector for the translation between the parent node's position and
the position of the target, so that you can move the projectile along the correct
line to the target.
4. You put together an SKAction series to play a fire sound, move the projectile to
the target and finally, remove the projectile when it's hit something. Then, you
run the action on the projectile node.
Again, this should be very familiar. You add a FiringComponent property and
initialize it with the tower type and the parent node: the tower's SpriteComponent
node.
There are still a few steps to complete before the tower will fire, the first of which is
to set the tower's current target when a dinosaur is in range. To do that, you're
going to check the distance between all of the dinosaurs and all of the towers in
your scene. If you spot any dinosaurs within range of a tower, you'll choose the
dinosaur that's closest to its destination as a priority.
The best place to do this check is after the scene has updated each frame. In an
raywenderlich.com 496
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
After every frame update, this function gets an array of the dinosaurs in the scene
and a separate array of the towers. For each tower, you're going to use a function
named distanceBetween(_:nodeB:)—provided in the Utils class that's been added
the starter project—to determine if any of the dinosaurs are in range.
// 4
if let t = target {
if dinosaur.spriteComponent.node.position.x >
t.spriteComponent.node.position.x {
target = dinosaur
}
} else {
target = dinosaur
}
// 5
tower.firingComponent.currentTarget = target
}
For each tower in the scene, you perform the following steps:
1. You store the tower type; this is what you use to get the range of the tower.
raywenderlich.com 497
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
2. You keep track of the target; this is optional because there might not be any
targets in range.
3. Next, you filter the dinosaurs to get those that are in range, and then loop
through these dinosaurs.
4. For each dinosaur that's in range of the tower, you check to see which is the
farthest across the scene—that is, which has the highest position.x value.
5. Finally, you set the target on the firing component for the tower.
As the dinosaur gets within range of the tower, the tower begins to fire its wooden
projectiles into the dinosaur. Pew pew!
Congratulations - this is starting to look like a real tower defense game, and you've
learned a lot about GameplayKit's Entity-Component system in the process. Keep
reading if you'd like a challenge to practice what you've learned - or skip forward to
the next chapter, where you'll learn about another great feature in GameplayKit:
Pathfinding!
raywenderlich.com 498
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Challenges
There's just one challenge in this chapter, and it's to add one more component to
your game: a health bar component for the dinosaur.
As always, if you get stuck, you can find solutions in the resources for this chapter
—but give it your best shot first!
These invincible dinosaurs need to respect the international gaming laws by using a
health bar. You're going to create a simple SKShapeNode that displays a green bar
above the dinosaur to indicate how much health it has remaining. The bar won't be
visible until the dinosaur has taken damage.
Create your final new Swift source file in your Components group and name it
HealthComponent.swift.
import SpriteKit
import GameplayKit
self.fullHealth = health
self.health = health
healthBarFullWidth = barWidth
healthBar = SKShapeNode(rectOfSize:
CGSizeMake(healthBarFullWidth, 5), cornerRadius: 1)
healthBar.fillColor = UIColor.greenColor()
healthBar.strokeColor = UIColor.greenColor()
healthBar.position = CGPointMake(0, barOffset)
parentNode.addChild(healthBar)
healthBar.hidden = true
}
raywenderlich.com 499
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
The HealthComponent stores the current health and the full health, and draws an
SKShapeNode whose size represents the amount of health remaining as a proportion
of the full health. The bar is attached to a parent node as a child. The parent node
will be the SpriteComponent node of the DinosaurEntity that's been targeted by the
tower.
The HealthComponent needs to know how to take damage and display it. It also
seems like the logical place to play a sound when the damage is done. So, add this
SKAction property to the HealthComponent class to let it play a sound when the
projectile hits the dinosaur:
healthBar.hidden = false
let healthScale = CGFloat(health)/CGFloat(fullHealth)
let scaleAction = SKAction.scaleXTo(healthScale, duration: 0.5)
healthBar.runAction(SKAction.group([soundAction, scaleAction]))
return health == 0
}
Here, you reduce the current health value by the provided amount of damage
taken. The health value can't go any lower than zero. You then make sure the
health bar is visible, and scale the health bar down using an SKAction to make it a
smooth transition. When scaling back the health bar, you also play the sound of the
dinosaur being hit.
Here, you set the Triceratops to be slightly weaker than the T-Rex. That T-Rex Boss
looks tough, doesn't it? More from him later. ;]
Your challenge is to add the your new health component to the dinosaur entity. To
raywenderlich.com 500
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
Now your dinosaurs have a health bar; all that's left is to apply the damage when
the projectile hits the dinosaur. Since the FiringComponent is responsible for all of
this violence, that's the best place to force the HealthComponent to take damage.
So, open FiringComponent.swift.
Then, add the damage action to the sequence, just before the removeAction, like
this:
Now, when the projectile hits the dinosaur, you can see the health bar appear and
take damage from each shot.
If you made it this far, congratulations! In this chapter, you learned how the Entity-
Component model can help to split your code into groups of functionality that you
can share between game objects without needing to rewrite any of the code. A
great example is the SpriteComponent, which works for dinosaurs, towers,
projectiles and—spoiler alert—in the next chapter, obstacles.
The component systems that you update during the update cycle in the scene also
provide an excellent way to trigger actions on your components, like the rate-
raywenderlich.com 501
2D iOS & tvOS Games by Tutorials Chapter 18: Entity-Component System
This is a great beginning for your Dino Defense game. In the next chapter, you'll
implement pathfinding, so your dinosaurs can avoid the towers and any other
obstacles in the scene. You'll also give the player the means to place her towers and
try to stop those pesky little—no, wait... BIG—dinosaurs!
raywenderlich.com 502
19 Chapter 19: Pathfinding
By Toby Stephens
One very noticeable issue with what you've created so far is that the dinosaur walks
straight through the tower without a care in the world. I think you'll agree, the
dinosaur's AI could use work!
Yes, there's a lot of fun coming up, but first, you'll take a brief look at the
pathfinding APIs provided by GameplayKit.
raywenderlich.com 503
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Pathfinding in GameplayKit
For your dinosaurs to navigate a path across the game scene, they'll need to be
aware of any obstacles in their way, such as towers, rocks and trees. To catalog
these obstacles, GameplayKit provides a GKGraph, which is a collection of locations
in the scene. You can deal with this collection of locations in one of two ways,
depending on which is the best fit for your particular game: You can use either grid-
based or obstacle-based paths.
Grid-based pathfinding
When the pathfinding is based on a playable grid, you can use a GKGraph subclass
named GKGridGraph to provide a list of valid locations on a two-dimensional grid.
The entities are only allowed to travel between the specified grid locations.
A good example of where you would use this kind of pathfinding is in Pac-Man, the
classic arcade game. Valid locations, specified using GKGridGraphNodes, would be the
paths on which Pac-Man and the ghosts can run. Locations on the grid occupied by
walls would be invalid, and wouldn't be added to the GKGridGraph.
Obstacle-based pathfinding
When your pathfinding needs to be less controlled and the entities need to avoid
obstacles while moving across an open field, you can use a GKGraph subclass named
GKObstacleGraph.
raywenderlich.com 504
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Given that the dinosaurs in Dino Defense are running across open terrain and
avoiding your towers, this is the pathfinding API you'll use.
When you add new towers to the game, GameplayKit will recalculate the paths
based on the dinosaurs' current positions.
Tower obstacles
Note: The next several pages of this chapter explain how to implement the
ability for the player to place towers into the game. This is a review of material
from previous chapters.
If you are here to learn about GameplayKit pathfinding, we recommend you
skip ahead to the "Creating an obstacle graph" section later in this chapter,
where we will have a starter project waiting for you.
But if you'd like some review or to learn how to let the player place the towers,
keep reading! :]
The first obstacles you're going to add to your obstacle graph will be the towers.
This would be a good time to enable the player to place the towers on her own.
To do that, you're going to give the player a number of possible tower placement
positions in the scene. If the player selects one of these placement nodes, she'll be
able to choose which tower she wishes to build, and the little elves inside the CPU
will build the tower. :]
raywenderlich.com 505
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Open GameScene.sks.
Do you recall the discussion of game layers from the previous chapter?
The .Background layer is the logical place to put your tower placement nodes. So,
start by locating the TowerPlaceholder.png in the Media Library.
Now, drag the TowerPlaceholder image into the scene and make the following
changes to the sprite node in the Attributes Inspector:
• Set the Z Position to 1 to ensure the image is always on top of the background
texture.
You'll use the name of the node later, when you test to find out which node the
raywenderlich.com 506
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Add seven more TowerPlaceholder nodes to the scene, making the following
changes in the Attributes Inspector:
Note: As a shortcut, try duplicating one of the tower placeholders with Option-
D and then change the name and position.
raywenderlich.com 507
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
You can see the tower selector nodes you created, sitting on the playing field.
When the player taps on one of these nodes, she needs to see the available tower
types so she can choose which tower to build. The starter project includes a simple
node that you'll use to show a tower type for selection.
The scene file provides an image of the tower type and also a label to tell the player
how much the tower will cost. Cost? Yes, in your completed game, the player will
have to pay gold to build each tower. You'll deal with the mechanics behind paying
for towers in the next chapter, but for now, you need to give each tower a cost.
Open TowerEntity.swift and add the following dynamic property to the TowerType
enum:
Those rock towers sure pack a punch, but they'll cost the player more than the
wood towers.
OK, now that you have a cost for each of your tower types, you can get back to the
tower selector node. You're going to create the class that displays the tower
selector in the scene file.
raywenderlich.com 508
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
import SpriteKit
override init() {
super.init()
}
Now, add the following method that will set the tower type on the selector node:
Taking a tower type, you set the correct tower image and also show the correct cost
of the tower in the costLabel. You also name the towerIcon, so you can identify the
tower type of the icon later, when you handle tower type selection.
raywenderlich.com 509
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
To implement this, your TowerSelectorNode needs to know the angle on the circle at
which to position itself. Add the angle argument to setTower(_:) so that it looks like
this:
To spin the TowerSelectorNode out to the correct angle, you're going to create, show
and hide animations using actions. Add the following properties to the
TowerSelectorNode class:
self.zRotation = 180.degreesToRadians()
The tower selector is initially rotated through 180 degrees, and then the
rotateAction rotates the node back into the correct position when you show the
tower selector to the user. The moveAction will move the node to its correct position
on the circle around the tower placement node, based on the provided angle.
The showAction combines the rotation and translation. The hideAction is simply the
reverse.
raywenderlich.com 510
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
func show() {
self.runAction(showAction)
}
So you can remove the tower selector node from the game scene, the hide function
takes a completion closure that runs after the hideAction has finished.
To make use of these new tower selectors, you need to create them in your game
scene. Open GameScene.swift and add a property for these tower selectors, as
follows:
You need to create one of these selector nodes per tower type. Create a function to
do this by adding this code to the GameScene class:
func loadTowerSelectorNodes() {
// 1
let towerTypeCount = TowerType.allValues.count
// 2
let towerSelectorNodePath: String = NSBundle.mainBundle()
.pathForResource("TowerSelector", ofType: "sks")!
let towerSelectorNodeScene = NSKeyedUnarchiver.unarchiveObjectWithFile(
towerSelectorNodePath) as! SKScene
for t in 0..<towerTypeCount {
// 3
let towerSelectorNode = (towerSelectorNodeScene.childNodeWithName(
"MainNode"))!.copy() as! TowerSelectorNode
// 4
towerSelectorNode.setTower(TowerType.allValues[t],
angle: ((2*π)/CGFloat(towerTypeCount))*CGFloat(t))
// 5
towerSelectorNodes.append(towerSelectorNode)
}
}
1. You get the number of tower types, so you can calculate the position of each
tower selector node as it's arranged in the diagram above.
3. For each tower type, you make a copy of the MainNode from the TowerSelector
scene.
4. You set the tower type on the TowerSelectorNode and also the angle at which
raywenderlich.com 511
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
5. Finally, you append the node to the TowerSelectorNode array you just defined.
Call this function as part of the scene setup by adding the following to
didMoveToView(_:), just after the call to super.didMoveToView(view):
loadTowerSelectorNodes()
Selecting a tower
You've loaded all of the selector nodes for the tower types you have in the game, so
you're ready to show them to the player when she taps a tower placement node.
First, remove the tower you temporarily placed in your scene by deleting the
following line from startFirstWave():
addTower(.Wood)
To track the player's tower placement activities, add the following properties to the
class:
// 2
self.runAction(SKAction.playSoundFileNamed("Menu.mp3",
waitForCompletion: false))
raywenderlich.com 512
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
2. The starter project came with a sound effect for opening and closing the tower
selectors, so use it. ;]
3. For each tower selector node, you set the position to the touch position passed
to the function. The tower selector nodes will spin out from this point.
4. You add the tower selector nodes to the .Hud game layer. This means you're
placing it above the other nodes in the game, which is what you want for game
UI elements.
5. You call the show() function you wrote to spin the tower selector nodes into
view.
Now, add a function to hide the tower selector nodes. Add this code:
func hideTowerSelector() {
if placingTower == false { return }
placingTower = false
self.runAction(SKAction.playSoundFileNamed("Menu.mp3",
waitForCompletion: false))
Note the closure you pass to the hide function on your tower selector nodes. As
discussed earlier, this is so you can wait for the hide animation to complete before
removing the node from its parent node—that is, before removing the tower
selector node from the .Hud layer.
raywenderlich.com 513
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Here, you go through the nodes located at the position of the player's touch. You
use a flatmap on the array of SKNodes returned from nodesAtPoint(_:) to only
include nodes with names that start with "Tower_" in the resulting touchedNodes
array.
Now that you have an array that only contains tower placement nodes, you can
check to see if the player actually touched any of these nodes. Add the following:
if touchedNodes.count == 0 {
hideTowerSelector()
return
}
You check to see if the player did, in fact, touch a tower placement node; if not, you
hide the tower selector if it needs hiding, and return.
So, what if the player did touch a tower placement node? Continue
touchesBegan(_:withEvent:) by adding the following:
if placingTower {
hideTowerSelector()
}
else {
placingTowerOnNode = touchedNode
showTowerSelector(atPosition: touchedNode.position)
}
Here, you take the first touched node. You shouldn't ever have more than one node
here, because that would mean you had more than one tower placement node at
the same position in the game scene. If you're already placing a tower, you hide the
tower selector; if you aren't placing a tower yet, you store the tower selector node
that was touched and show the tower selector.
Build and run, and when you start the game, touch one of the tower placement
nodes.
The tower selector nodes spin out from the point you touched. Now this is starting
raywenderlich.com 514
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Placing a tower
What about selecting and placing a tower? Well, to add a tower to a specified
position in the scene, you need to make some changes to addTower(_:).
In addTower(_:), remove the temporary line you added to set the position of the
tower:
placingTowerOnNode.removeFromParent()
self.runAction(SKAction.playSoundFileNamed("BuildTower.mp3",
waitForCompletion: false))
You remove the tower placement node from the game, because a tower has been
placed on top of it. You also play the build tower sound effect provided in the
starter project.
To finish placing the tower, you need to check which tower type the player has
selected. This will be on a touch event again, so head back to
touchesBegan(_:withEvent:).
if touchedNodeName == "Tower_Icon_WoodTower" {
addTower(.Wood, position: placingTowerOnNode.position)
}
else if touchedNodeName == "Tower_Icon_RockTower" {
addTower(.Rock, position: placingTowerOnNode.position)
}
If placingTower is true, then you now check to see if one of the tower icons in the
tower selector node has been touched. If it has, then you know which tower the
player selected, and you can place the tower before hiding the tower selector.
raywenderlich.com 515
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
The player can now select which type of tower she wishes to build!
Run the game again, and this time, make sure you build a tower on the center
placement node so that it's in the path of the dinosaur.
The dinosaur walks right through the tower. Hey, this chapter was supposed to be
about pathfinding, right? Right!
raywenderlich.com 516
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Recall that the obstacle graph is going to contain a collection of polygons that
define the areas where the dinosaurs can't go. The polygon obstacle for a tower
needs to be a shape around its base. Fortunately, you already have something that
defines a shape around the tower's base: the ShadowComponent.
You're going to let the ShadowComponent define a polygon obstacle. That way, any
entity that has the ShadowComponent can also have a polygon obstacle for the
obstacle graph. Clever, right?
Set it in init(size:offset:):
self.size = size
Here, you create a simple diamond-shaped polygon with four sides based on the
shape of the shadow node. This will provide an effective and efficient obstacle for
the pathfinding.
raywenderlich.com 517
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Note: The more complex the polygon used in the obstacle, the longer it will
take to calculate the path. This is because more complex shapes require more
computation from the pathfinding algorithms.
Now, open GameScene.swift so you can make use of this polygon obstacle.
To add an polygon obstacle to the obstacle graph, implement the following function:
This simply takes an array of GKPolygonObstacles and adds them to the obstacle
graph.
To create an obstacle and add it to the obstacle graph when you build a tower, add
the following to the end of addTower(_:position:):
addObstaclesToObstacleGraph(
towerEntity.shadowComponent.createObstaclesAtPosition(position))
You take the new tower entity's shadow component, create a polygon obstacle from
the shadow and add it to the graph.
At present, your dinosaur ignores the obstacle graph because it's following a simple
SKAction.moveTo(_:duration:) path and not using GameplayKit's pathfinding. It's
time to sort this out.
// 1
let startNode = GKGraphNode2D(
point: vector_float2(dinosaurNode.position))
obstacleGraph.connectNodeUsingObstacles(startNode)
// 2
let endNode = GKGraphNode2D(point: vector_float2(point))
obstacleGraph.connectNodeUsingObstacles(endNode)
// 3
let pathNodes = obstacleGraph.findPathFromNode(
startNode, toNode: endNode) as! [GKGraphNode2D]
raywenderlich.com 518
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
// 4
obstacleGraph.removeNodes([startNode, endNode])
}
This is the core of your obstacle graph's functionality. The graph needs to know
about the start and end positions—represented as GKGraphNode2D objects—so it can
calculate a valid path between them.
2. Then, you add the dinosaur's target end position to the obstacle graph, based
on the value passed into this method.
3. Using the nodes you just added, you calculate a valid path between the start
and end nodes. The resulting path is a collection of GKGraphNode2D objects that
provide the positions of each of the points the dinosaur will visit along its path.
4. Finally, you clean up the obstacle graph by removing the start and end nodes.
The last step is to use this path to create an SKAction sequence to move the
dinosaur between the points on the path. To do that, you first need to give the
dinosaur a speed—as mentioned in the last chapter, dinosaur types will move at
different speeds.
These values represent the relative speed of each dinosaur. Who knew the mighty
Triceratops was so nimble! ;]
// 1
dinosaurNode.removeActionForKey("move")
// 2
var pathActions = [SKAction]()
var lastNodePosition = startNode.position
for node2D in pathNodes {
// 3
let nodePosition = CGPoint(node2D.position)
// 4
let actionDuration =
raywenderlich.com 519
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
NSTimeInterval(lastNodePosition.distanceTo(node2D.position)
/ dinosaur.dinosaurType.speed)
// 5
let pathNodeAction = SKAction.moveTo(
nodePosition, duration: actionDuration)
// 6
pathActions.append(pathNodeAction)
lastNodePosition = node2D.position
}
// 7
dinosaurNode.runAction(SKAction.sequence(pathActions), withKey: "move")
Here you place the dinosaur on the path provided by the obstacle graph. There's a
lot going on, and it's really important, so take a close look at the steps:
1. You could be updating this dinosaur's path while it's already moving—say, if the
player adds a new tower—so you clear all the existing actions first.
2. You're going to create an SKAction for each step of the path and store them in
an array to be animated in sequence.
4. The duration of the SKAction for each step of the path is determined by the
distance between the nodes and also the speed of the dinosaur, which you
defined moments ago in the DinosaurType enum.
5. Using the target position for this step, and the duration of the action, you create
the SKAction to move the dinosaur to the next node on the path.
7. Finally, you run the array of move actions on the dinosaur as a sequence,
thereby setting the dinosaur off on its path.
Right now, to move across the screen, the dinosaur still uses the temporary
SKAction you wrote in the last chapter. You're going to replace that with your new
setDinosaurOnPath(_:toPoint:) function.
Before you see all of this at work in your game, you need to ensure you recalculate
the dinosaur's path when you add towers to the game. To do this, add the following
method:
raywenderlich.com 520
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
func recalculateDinosaurPaths() {
// 1
let endPosition = CGPointMake(1224, 384)
// 2
let dinosaurs: [DinosaurEntity] = entities.flatMap { entity in
if let dinosaur = entity as? DinosaurEntity {
if dinosaur.healthComponent.health <= 0 {return nil}
return dinosaur
}
return nil
}
// 3
for dinosaur in dinosaurs {
setDinosaurOnPath(dinosaur, toPoint: endPosition)
}
}
1. This is the same end position you use when adding the dinosaur.
2. You go through the dinosaur entities in the scene and get all those that have
health remaining. Dead dinosaurs don't move much. ;]
Now, simply call this function whenever you add a tower to the scene. Add the
following line to the end of addTower(_:position:):
recalculateDinosaurPaths()
OK, let's take it for a spin. Build and run, and put your tower on the same central
placement node as before.
The dinosaur now avoids the tower on its path to rampaging the player's village,
mwuahaha!
Congratulations, you've now learned the basics of GameplayKit's pathfinding
raywenderlich.com 521
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
system. Keep reading for some more practice, or skip forward to the next chapter
where you'll use GameplayKit's Agents, Goals, and Behavior system to add some
basic artificial intelligence into your game.
Challenges
This chapter has two challenges - one to get practice adding additional types of
obstacles, and one to fix an annoying bug you may have noticed while testing your
game with the z-ordering of sprites.
If you get stuck, you can find solutions in the resources for this chapter—but to get
the most from this book, give these your best shot before you look!
To place obstacles in the scene, you'll position them in the GameScene.sks scene
file, in the .Sprites layer. Then, when Sprite Kit creates an ObstacleEntity in the
GameScene, the entity will use its obstacle node in the scene file as a reference.
Drag the sprite into the scene and make the following changes to the sprite node in
raywenderlich.com 522
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
This obstacle will definitely get in the dinosaur's way! Add some more obstacles,
using these settings:
raywenderlich.com 523
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
This is really starting to look good! Each of these new obstacles will simply be a
placeholder for an ObstacleEntity. You're going to grab each of the obstacles in
turn and create an ObstacleEntity, then you'll remove the placeholder from the
scene and replace it with the SpriteComponent node of your ObstacleEntity.
First off, create a new Swift source file in your Entities group and name it
ObstacleEntity.swift. Replace the contents of the file with the following:
import SpriteKit
import GameplayKit
// 3
init(withNode node: SKSpriteNode) {
super.init()
// 4
spriteComponent = SpriteComponent(entity: self, texture:
node.texture!, size: node.size)
addComponent(spriteComponent)
// 5
let shadowSize = CGSizeMake(node.size.width*1.1, node.size.height *
0.6)
shadowComponent = ShadowComponent(size: shadowSize, offset:
CGPointMake(0.0, -node.size.height*0.35))
addComponent(shadowComponent)
raywenderlich.com 524
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
// 6
spriteComponent.node.position = node.position
node.position = CGPointZero
spriteComponent.node.addChild(node)
}
}
That's a lot of code—in fact, it's all of the ObstacleEntity code—but it should be
very familiar to you from the previous chapter. However, here's the breakdown so
you can be clear on each step:
3. You initialize the ObstacleEntity with a sprite node from the scene. These are
the sprite nodes you just added to the scene file.
5. You instantiate the ShadowComponent using the sprite node's size as a guide.
6. Finally, you add the existing sprite node as a child to the SpriteComponent node,
so that it looks exactly like it does in your scene file—only now, with all the
added benefits of being an ObstacleEntity.
To add each ObstacleEntity to the scene, open GameScene.swift and add a new
method:
Your challenge is to implement each of the commented lines in this method. Here
are some hints on each line:
1. You're going to add the obstacle at the same position as the sprite node in your
scene file. So, you get the position of the supplied node.
2. Since you're going to add the sprite component node of the new ObstacleEntity
to the scene, you need to remove the placeholder you put in the scene file.
raywenderlich.com 525
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
5. You take the polygon obstacle from the obstacle's shadow component.
6. You add the obstalce to the obstacle graph, using the helper method you wrote
earlier.
Once you get this working, all that's left is to call this function for each of the
obstacles in the scene when you first load the scene.
Here, you get all the obstacle nodes from the scene and call
addObstacle(withNode:) for each one.
All the obstacles are in the scene, with shadows. They're also clearly taking part in
the pathfinding, as the dinosaur is avoiding that rock at the start.
Run again, and add some towers to the scene. You'll see that you can get the
dinosaur to take some long paths around your obstacles and towers!
raywenderlich.com 526
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
This happens because you haven't explicitly set the z-positions of all the sprites in
the .Sprites layer, so Sprite Kit is assigning them randomly.
It's important to fix that. The desirable z-position for a sprite is directly related to
the sprite's y-position—the closer the sprite is to the bottom of the screen, the
farther forward its z-position should be. You can use this information to determine if
a sprite should be above another sprite.
// 1
let ySortedEntities = entities.sort {
let nodeA = $0.0.componentForClass(SpriteComponent.self)!.node
let nodeB = $0.1.componentForClass(SpriteComponent.self)!.node
return nodeA.position.y > nodeB.position.y
}
// 2
var zPosition = GameLayer.zDeltaForSprites
for entity in ySortedEntities {
// 3 - Get the entity's sprite component
1. This sorts all the entities in the scene by their position.y values.
2. You set an initial z position and loop through the sorted entities.
Your challenge is to implement lines 3-6, given the comments above. When you're
done, build and run again.
raywenderlich.com 527
2D iOS & tvOS Games by Tutorials Chapter 19: Pathfinding
Your dinosaur is now always where it should be—in front of the towers it's below in
the scene.
If you made it this far, congratulations - you now have a firm understanding of
GameplayKit's pathfinding feature!
In this chapter, you created an obstacle graph for your scene and used it to
calculate the path for your dinosaur. You gave the player the ability to add towers to
the scene, making sure to update the dinosaur's path whenever the player adds a
new tower.
With these tools, you'll see how you can give your pathfinding a more fluid and
organic feel, thereby completing your new blockbuster game.
raywenderlich.com 528
20 Chapter 20: Agents, Goals,
and Behaviors
By Toby Stephens
In the last chapter, you used GameplayKit's awesome pathfinding tools to make
your T-Rex attack the player's base while avoiding obstacles and towers along the
way. The pathfinding works well, but in this chapter, you're going to use a different
set of GameplayKit tools to move your other dinosaur type—the Triceratops.
raywenderlich.com 529
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
Note: This chapter begins where the previous chapter's challenge 2 left off. If
you were unable to complete the challenges or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter's
resources to pick up in the right place.
This is great for many games, but what if you want more than one goal at a time?
For example, maybe you want to follow a path while staying in formation with other
enemies at the same time.
This is what Agents, Goals, and Behaviors are all about. Let's take a look at each in
turn:
Goals allow you to specify a set of goals that may influence a node's movement,
such as:
• Avoiding obstacles;
• Staying on a path;
• Moving as a flock.
Behaviors are a collection of goals. For each goal, you can set a "weight" of how
important it is.
raywenderlich.com 530
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
updates the agent every frame, it evaluates each of the goals making up the
current behavior and update variables on the agent like position and speed
appropriately. Here's what that looks like:
Note that agents do not set the position of your sprite directly; you are responsible
for implementing a delegate method to set your sprite's position to the agent's
position.
Using a combination of goals to move to a target position, stay on a path and avoid
obstacles, your Triceratops will have an agent that dictates its movement in the
scene. This allows you to create more complex movement behavior, quite easily.
Adding an agent
Open DinoDefense where you left it off in the previous chapter's challenge 2 (or
load the starter project for this chapter).
You subclass GKAgent2D, in case you wish to add any agent logic to DinosaurAgent.
You won't be adding anything to this subclass in this tutorial, but it makes sense to
future-proof your design.
You make this an optional since you will only have an agent set for Triceratops
raywenderlich.com 531
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
dinosaurs.
if dinosaurType == .Triceratops {
agent = DinosaurAgent()
agent!.maxSpeed = dinosaurType.speed
agent!.maxAcceleration = 200.0
agent!.mass = 0.1
agent!.radius = Float(size.width * 0.5)
agent!.behavior = GKBehavior()
addComponent(agent!)
}
• maxSpeed comes from the dinosaurType and tells the agent that this is as fast
as the entity can move within the scene.
• maxAcceleration describes how quickly the Triceratops can reach its maximum
speed.
• mass affects how quickly the Triceratops responds to goals that change its speed
or direction.
• behavior is the default behavior for this agent—it has no goals and so the agent
has no motivation to move the entity.
You then add the agent component to the DinosaurEntity components. Remember
from earlier in this chapter that an agent is a sublcass of GKComponent.
raywenderlich.com 532
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
import Foundation
import GameplayKit
• Avoid obstacles;
• Follow a path;
To create these goals, your factory function will need to know the following:
• The path as calculated from the obstacle graph you created in the last chapter;
Now that you have the GKPath and GKPolygonObstacle array available in your
function, you can create your goals.
To add goals to a behavior, you need to give the goals a weighted value. The weight
value tells the behavior just how much the individual goal should affect the
decisions being made. A goal with a weight of 0.0 will have no effect on the
decisions made by the behavior, while other weight values are taken into account
relative to the weight values of the other goals in the behavior. You will use values
for weight up to a maximum value of 1.0, which will be reserved for the most
important goals.
Since staying on the path is far more important than the dinosaur reaching its top
speed, the target speed goal will have a weight of 0.5.
raywenderlich.com 533
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
behavior.setWeight(0.5,
forGoal: GKGoal(toReachTargetSpeed: agent.maxSpeed))
Here, you create a GKGoal for the behavior to reach a target speed, which in this
case is the maxSpeed you defined earlier in DinosaurAgent. You also set the goal to
have a weight of 0.5, as discussed above.
Now, add the following goal to help the behavior avoid obstacles:
behavior.setWeight(1.0,
forGoal: GKGoal(toAvoidObstacles: obstacles, maxPredictionTime: 0.5))
This is an important goal; you don't want your dinosaurs bumping into things, so
you give it a weight of 1.0. This goal uses the GKPolygonObstacle array you provided
in the function. maxPredictionTime is the maximum amount of time between
updates to the behavior for this goal. This means the behavior will wait a maximum
of 0.5 seconds before checking again for obstacles in its way.
The final goal for this behavior relates to pathfinding. Add the following two goals:
behavior.setWeight(1.0,
forGoal: GKGoal(toFollowPath: path, maxPredictionTime: 0.5,
forward: true))
behavior.setWeight(1.0,
forGoal: GKGoal(toStayOnPath: path, maxPredictionTime: 0.5))
These two goals each perform a distinct task: toFollowPath motivates the behavior
to keep the agent facing forward on the path, while toStayOnPath influences the
behavior to keep the agent within the radius of the path. Together, the two goals
ensure that the dinosaur will walk the path and head the correct way between the
nodes on the path.
Well, it actually is pretty easy thanks to GameplayKit. Now that you've defined the
behavior, you simply have to set the behavior on the DinosaurAgent for it to tow the
line.
It's only the Triceratopses that are going to use the DinosaurAgent to move, and so
logically, only the T-Rex and the T-Rex Boss are going to use your existing
pathfinding code. At the bottom of setDinosaurOnPath(_:toPoint:), locate the line:
raywenderlich.com 534
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
dinosaurNode.removeActionForKey("move")
Replace the code from that line until the end of the method with the following:
switch dinosaur.dinosaurType {
case .TRex, .TRexBoss:
dinosaurNode.removeActionForKey("move")
let actionDuration =
NSTimeInterval(lastNodePosition.distanceTo(node2D.position) /
dinosaur.dinosaurType.speed)
pathActions.append(pathNodeAction)
lastNodePosition = node2D.position
}
case .Triceratops:
if pathNodes.count > 1 {
let dinosaurPath = GKPath(graphNodes: pathNodes, radius: 32.0)
dinosaur.agent!.behavior = DinosaurPathBehavior.pathBehavior(
forAgent: dinosaur.agent!,
onPath: dinosaurPath,
avoidingObstacles: obstacleGraph.obstacles)
}
}
You use the existing pathfinding code only for the .TRex and .TRexBoss dinosaurs;
for the .Triceratops dinosaur type, you create a GKPath from the pathNodes and set
the behavior on the agent using the path and the obstacles from the obstacleGraph.
Now the Triceratops dinosaurs have a behavior that will drive their agents to move
them along the path to the target positions.
At the start of the chapter, you learned that GKAgent objects are updated by
updateWithDeltaTime(_:) as part of the update cycle in the scene. Back in Chapter
18, "Entity-Component System", you added GKComponentSystem objects to your
scene for each of the components that requires updates every frame.
Look at the componentSystems property in your GameScene class, and you can see the
component systems for the AnimationComponent and the FiringComponent, which you
added in Chapter 18.
raywenderlich.com 535
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
Now, the DinosaurAgent will also update itself every frame. Once it does, the agent
will move along the path according to the goals in its behavior.
This doesn't yet do anything to the actual entity in the game—you need to link the
two together so that updates to the agent are rendered in the game scene, and
updates to the position of the entity can be applied to the position of the agent. To
do this, first you need to make DinosaurEntity conform to the GKAgentDelegate
protocol.
Implementing GKAgentDelegate
GKAgentDelegate is a protocol that synchronizes the agent's state with its
corresponding entity—in this case, DinosaurEntity. The protocol provides two
events triggered by the agent's update function. These events are:
• agentWillUpdate(_:): tells the delegate the agent is about to perform its next
update.
When the agent is about to update itself, the entity needs to set the position on the
agent so that their positions match. After the agent has performed the update, and
has therefore moved according to its behavior, the agent needs to set the position
of the entity.
Now, add the following functions to update the agent and entity positions, before
and after the agent update:
In agentWillUpdate(_:), you set the position of the agent to equal the position of
raywenderlich.com 536
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
In agentDidUpdate(_:), the agent has performed its update, and has its new
position on the path, so you update the position of the entity's SpriteComponent
node to equal the position of the agent.
To set the agent's delegate, add the following line to init(dinosaurType:), just after
you create the DinosaurAgent:
agent!.delegate = self
That's it. You now have a dinosaur that uses the GameplayKit agent, behavior and
goals model to motivate it to move across the scene.
addDinosaur(.TRex)
addDinosaur(.Triceratops)
When you start the game, you'll see your Triceratops confidently pick its way across
the scene, avoiding the obstacles and towers as it goes. See how much smoother
the path is for the Triceratops than it was for the T-Rex? That's the behavior at work
trying to keep the Triceratops on the path, while it obeys the agent's other
behavioral goals.
Note: For the rest of this chapter, you're going to implement the remaining
gameplay and interface elements of Dino Defense. It's pretty cool stuff, but if
you only wanted to learn about agents, goals and behaviors, feel free to skip
these sections and go right to the challenge at the end. The dinosaurs will
forgive you!
raywenderlich.com 537
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
A musical interlude
That was a lot of new information to take on board—you deserve a small break!
Before you look at adding more dinosaurs to the game, why not give the player a
musical backdrop to their battle? All great games have great music, and there's
some great music included in the starter project.
But here's the sweet part—to get that music pumping, you just have to add the
following line to the end of didMoveToView(_:):
startBackgroundMusic()
The rest is already done for you, so this truly is a moment of light relief in your
code writing.
So, build and run, sit back, and enjoy the Dino Defense score. ;]
Attack waves
OK, break time is over—back to the dinosaurs.
Right now, sending one dinosaur on a solo mission to attack the base doesn't give
the player much of a challenge. It's time to send out those dinos in numbers, in a
series of waves. You're going to create a wave manager that handles sending out
waves of different numbers of dinosaurs straight at the player's defenses—a kind of
dinosaur general.
Each wave is going to contain only one type of dinosaur. The wave will send a fixed
number of dinosaurs across the scene, a certain number of seconds apart. To define
a wave, create the following struct in Waves.swift:
struct Wave {
let dinosaurCount: Int
let dinosaurDelay: Double
let dinosaurType: DinosaurType
}
raywenderlich.com 538
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
The wave manager will have an array of these Wave objects that it will issue. Define
the WaveManager class by adding the following to Waves.swift:
class WaveManager {
var currentWave = 0
var currentWaveDinosaurCount = 0
Note: At this point, Xcode will complain that WaveManager has no initializers;
you'll fix this momentarily.
The currentWave property holds the current index of the active wave.
currentWaveDinosaurCount tells you how many dinosaurs from this wave are
currently in the game scene. You'll set it when the wave starts, and decrement it
when a dinosaur is killed or reaches the base. The waves property holds the array of
Waves.
Every time the WaveManager is ready to send a new wave, you'll execute a closure
that you'll specify in GameScene. There will be another closure for the WaveManager to
execute when it wants to send out a new dinosaur.
init(waves: [Wave],
newWaveHandler: (waveNum: Int) -> Void,
newDinosaurHandler: (dinosaurType: DinosaurType) -> Void) {
self.waves = waves
self.newWaveHandler = newWaveHandler
self.newDinosaurHandler = newDinosaurHandler
}
You can now create a WaveManager by providing an array of Waves, and callback
closures for new waves and dinosaurs. Do that now.
At the end of didMoveToView(_:), add the following to define your waves for the
game:
raywenderlich.com 539
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
The WaveManager initializer takes the Waves array and also two closures:
• newWaveHandler is a closure that the WaveManager will call when it wants to start a
new wave. Here, you simply play a sound to tell the player a new wave of
dinosaurs is about to begin.
// 2
self.newWaveHandler(waveNum: currentWave+1)
// 3
let wave = waves[currentWave]
raywenderlich.com 540
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
// 4
currentWaveDinosaurCount = wave.dinosaurCount
for m in 1...wave.dinosaurCount {
// 5
delay(wave.dinosaurDelay * Double(m), closure: { () -> () in
self.newDinosaurHandler(mobType: wave.dinosaurType)
})
}
// 6
currentWave++
// 7
return false
}
This function kicks off the WaveManager and sends in the first wave of dinosaurs.
Every time a wave of dinosaurs is cleared—either by the player killing them all or by
the dinosaurs making it to the player's base—you call the function again. If
startNextWave() is called when there are still more waves to come, then it will
return false. If there are no waves left, the function will return true.
1. If there are no more waves left to deliver, then the player has won the game
and the function returns true.
5. For each dinosaur in the wave, you execute the newDinosaurHandler closure,
delaying each call by an increasingly longer time based on the dinosaurDelay.
6. You increment the currentWave so the WaveManager is ready to send out the next
wave.
7. You just kicked off a new wave—it's not game over yet, so return false.
To kick off the next wave whenever all of the dinosaurs in the current wave have
been dealt with, the WaveManager needs to know when to decrement the
currentWaveDinosaurCount.
raywenderlich.com 541
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
The GameScene can now tell the WaveManager when a dinosaur has been killed or has
made it to the base. removeDinosaurFromWave() calls startNextWave() if all of the
dinosaurs from the current wave have been removed. It will also return true or
false, depending on whether the last wave has finished—informing GameScene
whether or not the player has won the game.
To send out the first attacking wave, open GameScene.swift and locate
startFirstWave(). Then, replace this line:
addDinosaur(.Triceratops)
waveManager.startNextWave()
When the game starts, the trumpets announce the first wave and a proud flock of
five T-Rexs begin their assault on the player's base camp.
Now that the startPosition is a variable and not a constant, you can change its y-
value to a random value within a certain range. Do so by adding the following just
below the declaration of startPosition:
raywenderlich.com 542
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
Here, you randomize startPosition.y by adding a random float between -100.0 and
100.0. The random object is defined in the GameSceneHelper class and uses another
GameplayKit tool to generate a random number between 1 and 20. You'll take a
closer look at the GameplayKit randomization tools in Chapter 22, "Randomization".
Build and run, and this time, you'll see the dinosaurs come across the scene in a
much less predictable pattern.
When one of the dinosaurs dies, you'll play a death animation and sound effect, and
then remove the entity from the scene. When the dinosaur reaches the base, you
simply need to remove it from the scene without the death animation.
raywenderlich.com 543
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
This function takes a Boolean that specifies whether the entity is being removed
from the scene because it died or simply because it reached the end of the scene.
If the dinosaur died, then you play the death sound for the correct dinosaur type
and request the .Dead animation. After giving the animation two seconds to play
out, you remove the entity's sprite and shadow from the scene.
If the dinosaur hasn't died, then you simply remove the sprite and shadow from the
scene.
The best place to determine whether a dinosaur has died or reached the base is at
the end of every frame update in the game scene.
You go through the dinosaurs in the scene and check each one to see if it has any
remaining health. If it doesn't, then you call removeEntityFromScene(true). If the
dinosaur still has health, then you check to see if it's reached the player's base at
the edge of the scene. If it has, then you call removeEntityFromScene(false). In
both cases, you remove the dinosaur from the array of GKEntity objects in the
scene, as you're now done with that dinosaur.
Build and run, and construct enough towers to take out a dinosaur.
raywenderlich.com 544
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
When the dinosaur dies, you'll hear its death rattle as it collapses to the ground.
You'll watch with amusement, though, as it continues on its path. You need a way
to stop it dead in its tracks—pun intended!
This function checks the dinosaur type and stops the dinosaur from moving.
• .TRex and .TRexBoss dinosaurs move along a path dictated by a series of actions,
so stopping them is simply a matter of removing all actions on the
SpriteComponent node.
Note: At the time of writing, setting a maxSpeed to 0.0 on a GKAgent causes the
sprite to disappear from the scene, so you're actually using 0.1—that's pretty
slow. ;]
Head back to where you were just working in didFinishUpdate() and add this line
raywenderlich.com 545
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
stopDinosaurMoving(dinosaur)
If the dinosaur has no remaining health or has reached the end of the scene, you
stop it in its tracks.
Before testing this, also make sure you get a second wave of dinosaurs once the
first wave of dinosaurs are dead. Inside the if statement where you check to see if
the dinosaur has zero health, add the following code:
If the dinosaur dies, you remove it from the current wave in the WaveManager,
checking to see if the player has killed the last dinosaur in the game. If all the
dinosaurs are dead, then you set the GameSceneWinState in the game's state
machine, and the player has won the game.
Inside the second if, where you check to see if the dinosaur has reached the end of
the scene, add the following:
waveManager.removeDinosaurFromWave()
Since removing a dinosaur because it's reached the player's base can never mean
the player has won the game, there's no need to check the returned Boolean.
Now that the WaveManager knows when dinosaurs have exited the scene, it will start
the second wave as soon as the first has finished.
Build and run, and construct enough towers to take out all the dinosaurs in the first
wave.
When the dinosaurs die, they now stop and behave like dead dinosaurs. As the last
of the dinosaurs from the first wave is dispatched, you hear the trumpets announce
the second wave, and the Triceratopses begin their stampede.
raywenderlich.com 546
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
The HUD
Your game is now progressing through the waves of dinosaurs, so it would be useful
for the player to know which wave she's up against and how many waves are left.
To convey this information, you'll show the HUD elements that are already available
in the .HUD layer of your game scene.
These are the labels in the .HUD layer for each of these game elements:
1. The number of lives the player's base has left. This will start at 5, and every
time a dinosaur reaches the base, it will decrement by a certain amount per
dinosaur type.
3. The amount of gold the player has available for building towers.
The .HUD labels for each of these elements is initially hidden from the player. To
make them visible when the player starts the game, add the following to the end of
startFirstWave():
You run an SKAction on each of the labels; this action fades the labels into the
scene over a period of 0.5 seconds.
You haven't yet implemented the base lives or the cost of a tower, but you can now
update the wave label every time there's a new wave.
Find where you initialize the WaveManager. Inside the newWaveHandler closure, add
the following:
Now, when the WaveManager calls the newWaveHandler at the start of each wave, you
update the waveLabel to display the current and total number of waves.
raywenderlich.com 547
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
The HUD now displays the correct current wave number and tells the player there
will be a total of five waves.
Decrementing lives
It's time to deal with the next element in the HUD: the number of lives of the
player's base.
As you can see, the player starts the game with five lives. Every time a dinosaur
reaches the base, you'll decrement the number of lives remaining. However, to
make it more interesting, each dinosaur type will inflict a different amount of
damage to the base.
The Triceratops is a little weaker than the T-Rex, but that Boss can take out the
player's base in one hit!
To implement this lives system, you need to decrement the number of base lives by
the dinosaur's baseDamage value whenever the dinosaur reaches the base.
You already have a check for when a dinosaur has reached the base, so open
GameScene.swift and go to didFinishUpdate(). In the if statement where you
check to see if the dinosaur has passed an x-position of 1124, find this line:
waveManager.removeDinosaurFromWave()
raywenderlich.com 548
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
//1
baseLives -= dinosaur.dinosaurType.baseDamage
// 2
updateHUD()
// 3
self.runAction(baseDamageSoundAction)
// 4
if baseLives <= 0 {
stateMachine.enterState(GameSceneLoseState.self)
}
1. Here, you reduce the baseLives by the amount of damage done by the dinosaur
type.
3. GameSceneHelper also contains SKAction sound effects for things like damage to
the base, winning the game and losing the game.
4. If baseLives has reached zero, the game is over and the player has lost, so tell
the game's state machine to change to the GameSceneLoseState.
Build and run, and let at least one of the dinosaurs reach the player's base at the
far right side of the screen.
The dinosaur roars as it attacks the base, and the number of base lives decreases
by the correct amount, based on the type of dinosaur that made it to the base.
raywenderlich.com 549
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
You already gave towers a cost in the previous chapter. Now you need to define
how much gold to award the player for killing each dinosaur type.
You award the player twice as much gold for killing a T-Rex as for killing a
Triceratops. Killing the Boss will earn the player a big bag of shiny gold coins.
You check to see if the player has enough gold to afford the tower she's trying to
build. If she doesn't have enough gold, you play a sound to give her an audible cue,
and return from the function without building the tower.
Directly after you make this check in addTower(_:position:), add the following:
gold -= towerType.cost
updateHUD()
You've established that the player can afford the tower, so now you decrement the
cost of the tower from the player's gold reserves. Then, you update the HUD to
show the new amount of available gold.
To award gold for a dinosaur kill, go to didFinishUpdate() and find the point at
which you check to see if a dinosaur's health has been reduced to zero. Inside that
if statement, just after you remove the dinosaur from the entities array, add the
following:
gold += dinosaur.dinosaurType.goldReward
updateHUD()
In the same way as you reduce the gold when the player builds a tower, you
increase the amount of available gold when the player kills a dinosaur. Then, you
update the HUD just as you did before.
raywenderlich.com 550
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
As you buy towers, the amount of gold shown in the HUD becomes lower and lower.
Every time you kill a dinosaur, you get a little bit of gold back.
Playing the game, you quickly run out of available gold, and suddenly the location
of your first tower is of vital importance. That's a much better game balance! Now,
it's a real challenge for the player.
Continue playing the game and see if you can win. It's pretty difficult, right? I think
you may have pushed the game balance a little bit against the player now, but you
have one last thing to add to your gameplay that might just tip back the scales.
A slowFactor of 1.0 means the projectile has no effect on the dinosaur's speed,
whereas a slowFactor of 0.0 means it stops the dinosaur completely. The wood
tower doesn't slow dinosaurs and so has a slowFactor of 1. The rock tower, with a
slowFactor of 0.5, slows the dinosaurs to half their speed.
So you can quickly check if a tower slows a dinosaur, add the following to the
raywenderlich.com 551
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
TowerType enum:
This is a simple convenience function that returns true if the slowFactor is less than
1.0, telling you that the tower does indeed slow down dinosaurs.
When a dinosaur slows down, you'll reduce its speed, of course, but you're also
going to highlight the dinosaur's sprite with a blue tint.
First, you add the blue tint to the AnimationComponent node. Then, you adjust the
speed of the dinosaur, depending on the dinosaur type.
• .TRex and .TRexBoss dinosaurs use actions to move along their paths, so you
adjust their speeds by changing the speed on the SpriteComponent node, which
affects all actions running on that node.
After it slows down a dinosaur, the rock tower is going to see if it can hit any other
dinosaurs in its range before it hits the same dinosaur again. This is so the rock
tower can slow down multiple dinosaurs if they're in range, thereby giving the
player a fighting chance.
Track this state by adding the following property to the DinosaurEntity class:
hasBeenSlowed = true
You now have a Boolean that will tell you whether or not the tower has already
slowed down a dinosaur.
raywenderlich.com 552
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
Since it's the tower's FiringComponent that damages the dinosaur, this is a logical
place to also slow down the dinosaur, if necessary. Open FiringComponent.swift
and locate damageAction in updateWithDeltaTime(_:).
damageAction is an SKAction that executes a closure block. Inside the closure for
damageAction, add the following:
if self.towerType.hasSlowingEffect {
target.slowed(self.towerType.slowFactor)
}
All that remains is to change the targeting logic of rock towers—or indeed, any
tower types that have a slowing effect.
if dinosaur.spriteComponent.node.position.x >
t.spriteComponent.node.position.x {
target = dinosaur
}
if towerType.hasSlowingEffect {
if !dinosaur.hasBeenSlowed && t.hasBeenSlowed {
target = dinosaur
}
else if dinosaur.hasBeenSlowed == t.hasBeenSlowed
&& dinosaur.spriteComponent.node.position.x >
t.spriteComponent.node.position.x {
target = dinosaur
}
}
else if dinosaur.spriteComponent.node.position.x >
t.spriteComponent.node.position.x {
target = dinosaur
}
Instead of simply checking which dinosaur is the furthest down the path, now you
check to see if there's a dinosaur in range that hasn't been slowed. If you find one,
it takes precedence as a target. If both dinosaurs have already been slowed, the
one furthest along on the path takes priority as a target.
Build and run, and at some point, build a rock tower to slow down those rampaging
dinos.
raywenderlich.com 553
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
That rock tower certainly packs a punch, and with its new slowing effect, it could
just about tip the balance in the player's favor.
Try to beat your new game with a combination of rock and wood towers—and watch
out for that T-Rex Boss, as he can take a lot of punishment!
• In the first chapter, you learned how to use entities and components to design
your game to reuse functionality. You separated key game object functions into
components and used those components in your game entities without having to
rewrite any of the code.
• In the chapter after that, you used GameplayKit's pathfinding tools to move your
T-Rex dinosaur across the scene, avoiding obstacles and towers along the way.
• And finally, in this chapter, you used agents, goals and behaviors to move your
Triceratops dinosaurs along their paths. They avoided the obstacles and towers in
your scene by obeying the goals set in your pathfinding behavior.
But you're not quite done yet... a game like this deserves to be seen on a bigger
screen!
raywenderlich.com 554
2D iOS & tvOS Games by Tutorials Chapter 20: Agents, Goals, and Behaviors
Challenges
There's only one challenge for this chapter, and it's a great one. One of the main
things we want to see developers like yourself create with this book are games for
AppleTV, so why not take on this challenge and convert your tower defense game to
tvOS?
This book has a whole section devoted to developing games for AppleTV, so you
should have everything you need, but here are a few tips regarding gameplay:
• Tapping a tower selector node isn't going to be an effective way to play the game
with the AppleTV remote. Instead, how about having a highlighted tower selector
node in the scene and switching the highlighted node with a swipe on the
remote.
• With a tower selector node highlighted, a tap on the remote should bring up the
tower selection interface. Then, swiping right could give the player a wood tower,
and swiping left a rock tower.
As always, our implementation of the challenge is available with the source code for
this chapter, but no cheating—and good luck. ;]
raywenderlich.com 555
Section V: Advanced Topics
In this section, you'll delve into some more advanced topics like procedural level
generation, GameplayKit randomization, and game controllers.
In the proces, you'll create a tile-based dungeon crawler called Delve where you try
to guide your miner through a rock-elemental infested dungeon.
raywenderlich.com 556
21 Chapter 21: Tile Map Games
By Neil North
Tile map games are a great way to create large scale games with minimal device
memory usage. Instead of creating artwork for an entire world, the environment is
broken down into small square images; this lets you build your world with smaller
images, called tiles, rather than having to use one giant continuous image.
But memory management isn't the only reason you may want to consider using a
tile map design for your game. Here are just a few more:
• Less artwork required: With a little bit of creativity, you can create numerous
beautiful environments from a rather small set of tiles. You get all of the benefits
of smaller builds and smaller memory imprints, plus you require less work from
the artist.
• Easier to create and scale levels: You can add extra levels, and extend
existing levels, usually without requiring any new artwork. If you need to adjust
the distance between a jump or fine-tune the layout, no problem!
• Ability to procedurally generate levels: Pre-built levels are great, but if you
want to generate levels using software algorithms, tile maps make this process
easy.
• Works great with GameplayKit: Using GKGridGraph and pathfinding, you can
create impressive board games and other games with grid-based movement.
• It's awesome!: No game design principle has withstood the test of time as
strongly as tile map level design. It's used successfully in a massive catalog of
2D games and has huge nostalgic appeal.
raywenderlich.com 557
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
At the time of writing this chapter, the Sprite Kit scene editor, built into Xcode,
doesn't do a great job of supporting tile map levels; it's time consuming to lay out
the tiles, and the resulting .sks file isn't anywhere near as efficient as a basic text
file with tile code.
However, there are plenty of good alternatives to the scene editor, and you'll be
exploring some of the best ones as you build the mini-game for the next few
chapters, Delve.
Introducing Delve
Grab your mining pick and prepare to delve into a mystical dungeon. The objective
of the game is to reach the stairs that descend to the next level.
Well, it would be—except for the fact that you need to get out before your hero
runs out of health. But, even that's a challenge: Your health deteriorates over time,
and hordes of dark magic rock golems aren't happy that you're invading their
dungeon. As you make your way through their world, the angry golems will try to
stop you.
You will build this game across the next four chapters, in stages:
• Chapter 21, Tile Map Games: You are here! In this chapter, you will learn
techniques for building tile map levels, including how to create a fully functional
tile map game.
• Chapter 23, Procedural Levels: Remove some of the random aspects of the
level generation to make the process more predictable, but still an adventure into
raywenderlich.com 558
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
the unknown.
• Chapter 24, Game Controllers: This game is perfect for external game
controllers; you'll be adding a tvOS target and exploring how to use the Apple TV
remote as a game controller.
By the end of these chapters, you'll have learned some advaned game creation
techniques, and will have made a fun new dungeon crawler!
Getting started
I have created a starter project for you to use, which you can find in the resources
for this chapter under starter\Delve.
Although you're going to build the game from scratch, the starter project contains
groups that you'll use to build the game and keep things organized.
After you open the project, drag the Tiles.atlas folder from starter\Resources
into the Resources group in Xcode.
This texture atlas contains all the tiles you'll use to build the levels for Delve.
Take a moment to review the tiles within the texture atlas and create a quick
mental plan for your first level.
raywenderlich.com 559
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
• Floor: This is the space on which the hero and the enemies move.
• Wall: An impassible object. Walls can be used to create corridors, rooms and
paths. You need to give consideration as to how much space the characters need
in order to move through their world.
• Triggers: The level may contain tiles or objects that do something when the
characters step onto or over them. For example, a special tile may exist at the
end of a level, or a collectible health enhancement may sit on top of a tile.
Note: Tiles can be layered, so you can place a collectible object on a floor tile.
You'll use this class to prepare levels before they're passed to the scene.
import SpriteKit
import GameplayKit
struct tileMap {
This imports the frameworks you need for this class. It also sets the basis for your
tile map struct.
Now you need to define exactly what tiles you wish to support in your game. This
includes walls and floors, but it can also include hero and enemy spawn locations as
well as other triggers, like health objects.
raywenderlich.com 560
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
This is a simple enumeration that includes a value for each type of tile you will add
into Delve.
You also need a way to inform the game scene what type of tile you need and
where to build it. To do this, add the following protocol above your struct:
protocol tileMapDelegate {
func createNodeOf(type type:tileType, location:CGPoint)
}
Now, your tileMap struct has to track its delegate. Add the following line inside your
struct:
There are a few more instance variables you need for your tile map. Below the
delegate variable, add this:
//1
var tileSize = CGSize(width: 32, height: 32)
//2
var tileLayer: [[Int]] = Array()
//3
var mapSize = CGPoint(x: 5, y: 5)
1. Tile Size: Tile maps are made up of tiles of equal size; this variable holds the
sprite size for each tile on the tile map.
2. Tile Layer: The tileLayer variable holds an array, also known as a two-
dimensional (2D) array. The first part of the array stores the row information;
the second part stores the column information as an Int. That integer relates
directly to the tileType enum.
3. Map Size: The mapSize variable holds the total number of columns and rows in
the tile map.
Excellent! You've created all of the attributes you need to define the tile map's
layout. Now, you just need to implement the logic to lay them out!
raywenderlich.com 561
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
Under your instance variables and within tileMap, add the following:
repeat {
var rowArray:[Int] = Array()
repeat {
rowArray.append(defaultValue)
} while rowArray.count < Int(mapSize.x)
columnArray.append(rowArray)
} while columnArray.count < Int(mapSize.y)
tileLayer = columnArray
}
The defaultValue variable passed into the function defines the tile map you're
creating. This number directly relates to your tileType enum.
The repeat loops go through each column of each row and append the
defaultValue. Then, the tileLayer variable is updated with the new tile map.
To see how this tile map looks, go to GameScene.swift and insert the following
instance variable inside the class:
Now, add this function below the other functions, within that same class:
func setupLevel() {
worldGen.generateLevel(0)
print(worldGen.tileLayer)
}
You'll use this function to generate the level each time the game is loaded;
therefore, you need to call this function in didMoveToView(_:). Do that now by
adding this line to didMoveToView(_:):
setupLevel()
Now, build and run the game. Keep an eye on your debug output; it should read:
[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0,
0, 0, 0]]
Each group of five numbers is your row of column values. Since you passed zero
into generateLevel(_:), all of the tiles have been assigned the default value.
raywenderlich.com 562
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
switch type {
case .tileGround:
break
case .tileWall:
break
case .tileWallLit:
break
case .tileStart:
break
case .tileEnemy:
break
case .tileEnd:
break
case .tileFood:
break
default:
break
}
}
When your LevelHelper struct is ready to build the level, it will tell its delegate to
add a tile of tileType at a specified location. It will use this switch statement to
determine which tile to load. You'll come back to this switch statement soon.
func setupLevel() {
worldGen.generateLevel(3)
worldGen.presentLayerViaDelegate()
}
The number passed in has changed from 0 to 3; 3 is the ground tile. You removed
the print statement and replaced it with a function the helper will use to tell its
delegate what to do.
raywenderlich.com 563
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
func presentLayerViaDelegate() {
for (indexr, row) in tileLayer.enumerate() {
for (indexc, cvalue) in row.enumerate() {
if (delegate != nil) {
delegate!.createNodeOf(type: tileType(rawValue: cvalue)!,
location: CGPoint(
x: tileSize.width * CGFloat(indexc),
y: tileSize.height * CGFloat(-indexr)))
}
}
}
}
In presentLayerViaDelegate(), you loop through each row, and then each column,
using for-in loops. Since you also need the index for each value to pinpoint its
position on the tile map, you use .enumerate() to return a tuple containing the
index and the value.
Once you have the indices of the row and the column for your value, you have
everything you need to tell the GameScene what to do.
To calculate the tile position, you simply multiply the tile size by its column and row
indices.
A later part of this chapter will refer to the MARK: comments to show you where to
add additional code.
The code above sets some key properties for the node and then adds it to the
scene.
OK—there are a few more things you need to do before you can build and run the
game:
2. You need an implementation of the scene's camera node so you can see your
raywenderlich.com 564
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
tiles.
func updateCameraScale() {
if let camera = camera {
camera.setScale(0.44)
}
}
These two functions update the location and set the scale of the camera.
You're almost done. Now, you need to implement the camera in didMoveToView(_:).
Replace the existing function, in its entirety, with the following:
//Delegates
worldGen.delegate = self
//Setup Camera
let myCamera = SKCameraNode()
camera = myCamera
addChild(myCamera)
updateCameraScale()
//Gamestate
setupLevel()
}
First, you assign the delegate. Then, you set up the camera node and add it to the
scene. Finally, you call the function that updates the camera's scale. And, of course,
you call setupLevel().
centerCameraOnPoint(location)
That's it! Now you can build and run to see your five-by-five grid of floor tiles.
raywenderlich.com 565
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
Under //MARK: Setters and getters for the tile map, add the following functions:
//1
mutating func setTile(position position:CGPoint, toValue:Int) {
tileLayer[Int(position.y)][Int(position.x)] = toValue
}
//2
func getTile(position position:CGPoint) -> Int {
return tileLayer[Int(position.y)][Int(position.x)]
}
//3
func tilemapSize() -> CGSize {
return CGSize(width: tileSize.width * mapSize.x, height:
tileSize.height * mapSize.y)
}
1. Suppose you wanted to change the value at row 3, column 2 to 5. You could
access it directly by using tileLayer[3][2] = 5; however, by setting up this
function to handle the interaction, you can potentially expand upon its
functionality later—for example, if you need to guard certain tiles.
2. This function lets you retrieve the value of a point by providing a CGPoint.
raywenderlich.com 566
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
3. This is useful when trying to calculate the bounds of the tile map environment,
because it obtains/returns the total size of the tile map.
Return to GameScene.swift and locate the instance variable at the top of the
class. Update that variable, and add a few more variables, too:
//World generator
var worldGen = tileMap()
//Layers
var worldLayer = SKNode()
var guiLayer = SKNode()
var enemyLayer = SKNode()
var overlayLayer = SKNode()
Soon, you'll add a lot more content to the game world, and these layers will help
you manage them.
Now, it's time to add the layers. In didMoveToView(_:), below the camera setup, add
the following:
//Config World
addChild(worldLayer)
camera!.addChild(guiLayer)
guiLayer.addChild(overlayLayer)
worldLayer.addChild(enemyLayer)
You add the worldLayer and enemyLayer to the scene. You add the guiLayer and
overlayLayer to the camera so they'll move wherever the camera moves.
case .tileWall:
let node = SKSpriteNode(texture: atlasTiles.textureNamed("Wall1"))
node.size = CGSize(width: 32, height: 32)
node.position = location
node.zPosition = 1
node.name = "wall"
worldLayer.addChild(node)
break
You added the ground tile to the worldLayer, and now you implement a wall tile in
the same way.
To test your wall tile, use setTile(position:toValue:) by adding the following line
to setupLevel(), between generating the base map and presenting the map:
When you're done, the new method will look like this:
raywenderlich.com 567
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
func setupLevel() {
worldGen.generateLevel(3)
//Add
worldGen.setTile(position: CGPoint(x: 0, y: 0), toValue: 1)
worldGen.presentLayerViaDelegate()
}
Now build and run, and you'll have a wall tile in the first column of the first row.
Manually setting the type of each tile isn't the most efficient way to create a level.
Luckily, there are ways to pre-build levels—take a look!
Pre-building a level
In GameScene.swift, navigate back to setupLevel() and update it as follows:
func setupLevel() {
//Update
worldGen.generateLevel(0)
//Add
worldGen.generateMap()
worldGen.presentLayerViaDelegate()
}
Head back to LevelHelper.swift and add the following code after //MARK: Level
creation:
raywenderlich.com 568
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
Every tile in the game is assigned by the template. The position of the number in
the template relates to the position of the tile; once again, the numbers relate
directly to the type of tile.
The wall tiles are surrounding the ground tiles like in a room. In this setup, there's
a gap in the wall in the bottom row to represent a doorway or path to the next
room.
Build and run the game to see your tile map. It looks just as you'd expect from the
template.
raywenderlich.com 569
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
As you learned in Chapter 15, "State Machines", state machines are a feature in iOS
9's new GameplayKit framework that make it easy to handle different behaviors for
your game world—or your characters—depending on the current state of the game.
• Initial: When the game starts, it loads the level and assets.
• Active: The game is playable and all timers—and the game loop—are running.
• Paused: All timers, animations and game functions stop until the state returns
to active.
• Limbo: All controls are disabled; however, the game is still active.
• Win: Provides a game over screen if you win; increments the level or difficulty
and restarts the scene in its initial state.
• Loss: Provides a game over screen if you lose; restarts the level in its initial
state without altering the game progress.
Inside the States group, create a new Swift file named GameState.swift.
import Foundation
import GameplayKit
import SpriteKit
//1
class GameSceneState: GKState {
unowned let levelScene: GameScene
init(scene: GameScene) {
self.levelScene = scene
}
}
//2
class GameSceneInitialState: GameSceneState {
raywenderlich.com 570
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
1. The GameSceneState class acts as a base class and inherits from GKState. As the
class is initialized, it stores a pointer to the instance of GameScene to which it's
attached.
2. Each state inherits its functionality from the base GameSceneState. This is where
you'll add your behaviors for each state.
Note: You can split each class into its own file, if you wish. But since this
game won't have a lot of state information, it's perfectly fine to leave it in a
single file.
Go back to GameScene.swift and add the following code, just below the layer
variables:
//State Machine
lazy var stateMachine: GKStateMachine = GKStateMachine(states: [
GameSceneInitialState(scene: self),
GameSceneActiveState(scene: self),
GameScenePausedState(scene: self),
GameSceneLimboState(scene: self),
GameSceneWinState(scene: self),
GameSceneLoseState(scene: self)
])
This creates your state machine and adds all of the states you've just created to it.
The compiler will complain about not finding GKStateMachine; that's because you
haven't imported GameplayKit. Do that now by adding the import statement at the
top of the file:
import GameplayKit
stateMachine.enterState(GameSceneInitialState.self)
raywenderlich.com 571
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
When you added the state, you removed the instruction to build the level. You need
to re-add this instruction to GameState.swift.
//Scene Activity
levelScene.paused = false //to be changed later
}
}
Build and run to verify everything still works. Although it looks exactly the same, in
reality, it's not—it's structurally superior!
First, you'll need some artwork for your character. Drag the player.atlas folder
from starter\Resources into the Resources group in Xcode.
raywenderlich.com 572
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
• Animation Component: The character will have idle and walking animations
facing in four directions. This component handles all of the animation logic and
provides the animated character for the sprite component.
When designing the sprite and animation components, keep in mind that other
entities within your game will use them, too.
Inside the Entities group, add a new Swift file named PlayerEntity.swift. Replace
the contents of the file with the following:
import SpriteKit
import GameplayKit
After implementing each component, you can add its functionality to the entity.
That's the beauty of the Entity-Component architecture—it's truly modular.
import SpriteKit
import GameplayKit
// 1
class EntityNode: SKSpriteNode {
weak var entity: GKEntity!
}
// 2
let node: EntityNode
// 3
init(entity: GKEntity, texture: SKTexture, size: CGSize) {
node = EntityNode(texture: texture,
color: SKColor.whiteColor(), size: size)
node.entity = entity
raywenderlich.com 573
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
1. Your sprite component is attached to the entity; the node also needs a
connection to the entity or else it won't be able to identify itself as belonging to
an entity. This subclass of SKSpriteNodeNode adds an entity property.
3. Upon initializing the component, you set the entity property of the node that's
linked to the component.
That's all the code you need for your sprite entity.
Now, go back to PlayerEntity.swift and add the following code within the class:
override init() {
super.init()
You initialize the sprite component and provide a link to the entity. Then you add
the component to the entity.
let template =
[
[1, 1, 1, 1, 1],
[1, 3, 3, 3, 1],
[1, 3, 4, 3, 1],
[1, 3, 3, 3, 1],
[1, 1, 3, 1, 1]
]
With that out of the way, open GameScene.swift. Start by adding a new property
to the top of the class, underneath the stateMachine property:
raywenderlich.com 574
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
//ECS
var entities = Set<GKEntity>()
You will use this to store all entities added to the scene in a set. This lets you keep
track of the entities and also holds a strong pointer to each entity so it isn't
released earlier than expected.
Each object added from an entity has a few standard places it needs to be set up.
To avoid complications, its worth implementing a function to handle entity creation.
Add this function just below createNodeOf(type:location:):
//2
if let spriteNode =
entity.componentForClass(SpriteComponent.self)?.node {
worldLayer.addChild(spriteNode)
}
}
1. You add the entity to the set of entities for the scene.
2. You add the node from the sprite component to the scene, on the correct layer.
//1
let node = SKSpriteNode(texture: atlasTiles.textureNamed("Floor1"))
node.size = CGSize(width: 32, height: 32)
node.position = location
node.zPosition = 1
worldLayer.addChild(node)
//2
let playerEntity = PlayerEntity()
let playerNode = playerEntity.spriteComponent.node
playerNode.position = location
playerNode.name = "playerNode"
playerNode.zPosition = 50
playerNode.anchorPoint = CGPointMake(0.5, 0.2)
//3
addEntity(playerEntity)
1. The tile location still needs a ground tile, so you place one down first.
raywenderlich.com 575
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
2. You create the entity and update the sprite component's node information.
Now build and run; your hero will be visible at the spawn point:
import SpriteKit
import GameplayKit
struct Animation {
let animationState: AnimationState
let textures: [SKTexture]
let repeatTexturesForever: Bool
}
You'll use the animation struct to represent each individual animation. This struct
lets the game pre-load the animation to avoid frame rate issues while the game is
running.
From here onwards, you'll be adding a lot more enums and game settings. To make
things easier, you can create a file to hold all of these global constants—which,
incidentally, will clear up the error you're seeing in Xcode.
raywenderlich.com 576
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
Create a new Swift file inside the Helpers group named GameSettings.swift.
Replace the contents of the file with the following code:
import Foundation
import SpriteKit
//1
enum AnimationState: String {
case Idle_Down = "Idle_12"
case Idle_Up = "Idle_4"
case Idle_Left = "Idle_8"
case Idle_Right = "Idle_0"
case Walk_Down = "Walk_12"
case Walk_Up = "Walk_4"
case Walk_Left = "Walk_8"
case Walk_Right = "Walk_0"
case Die_Down = "Die_0"
}
//2
enum LastDirection {
case Left
case Right
case Up
case Down
}
1. This is a list of all the possible animations. The string values correspond with the
names of the textures. In the strings, each number beside the action word
represents a direction. Right is 0, Up is 4, Left is 8, and Down is 12.
2. If the hero is walking, and then suddenly stops walking, it will return to an idle
animation. You're tracking the direction of movement to make sure the idle
raywenderlich.com 577
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
//1
static let actionKey = "Action"
static let timePerFrame = NSTimeInterval(1.0 / 20.0)
//2
let node: SKSpriteNode
//3
var animations: [AnimationState: Animation]
//4
private(set) var currentAnimation: Animation?
//5
var requestedAnimationState: AnimationState?
//6
init(node: SKSpriteNode, textureSize: CGSize,
animations: [AnimationState: Animation]) {
self.node = node
self.animations = animations
}
1. These are the default name and frame rate settings for the Animation SKAction.
2. This is the node that you are animating (the one from the sprite component).
3. This is the collection of all animations that have been set up in the current
instance of the animation component.
5. This property lets the scene, entity or another component notify this component
that the animation is about to change. The change will then be picked up by this
component's updateWithDeltaTime(_:) function.
The component will need to be capable of updating the current animation regularly.
If a character is walking through a dungeon, it's likely to change direction
frequently, and a delayed animation can make the character look rather silly.
raywenderlich.com 578
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
//2
guard let animation = animations[animationState] else {
print("Unknown animation for state \(animationState.rawValue)")
return
}
//3
node.removeActionForKey(AnimationComponent.actionKey)
//4
let texturesAction: SKAction
if animation.repeatTexturesForever {
texturesAction = SKAction.repeatActionForever(
SKAction.animateWithTextures(animation.textures,
timePerFrame: AnimationComponent.timePerFrame))
} else {
texturesAction = SKAction.animateWithTextures(animation.textures,
timePerFrame: AnimationComponent.timePerFrame)
}
//5
node.runAction(texturesAction, withKey: AnimationComponent.actionKey)
//6
currentAnimation = animation
}
1. You check if the animation state requested is the same as the current animation
state. If both animation states are the same, you do nothing, since the
animation is already running. If you revert to a new animation state every
frame, then you'll only ever see the first frame of each animation.
4. Next, you set up the new animation using SKAction to either play once, or
repeat until removed.
Since the update function is called via the game loop, you'll need to check if a new
animation has been requested. To do so, add the following code below the new
function you just added.
raywenderlich.com 579
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
requestedAnimationState = nil
}
}
There's one last thing your animation component needs: a way to take a texture
atlas and create an animation using the Animation struct you added at the top of
this component.
return Animation(
animationState: animationState,
textures: textures,
repeatTexturesForever: repeatTexturesForever
)
}
This code filters the texture atlas for files that contain the identifier string for the
animation. It then sorts the frames into numerical order and constructs them into
an animation using the Animation struct.
Head back to PlayerEntity.swift and add this instance variable under the
spriteComponent variable:
Now, in the initializer, add the following code after you initialize the spriteComponent
variable:
There's an Xcode error that loadAnimations() is missing, so let's add that now. Add
this function after your initializer:
raywenderlich.com 580
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
animations[.Walk_Down] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Walk_Down.rawValue,
forAnimationState: .Walk_Down)
animations[.Walk_Up] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Walk_Up.rawValue,
forAnimationState: .Walk_Up)
animations[.Walk_Left] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Walk_Left.rawValue,
forAnimationState: .Walk_Left)
animations[.Walk_Right] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Walk_Right.rawValue,
forAnimationState: .Walk_Right)
animations[.Idle_Down] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Idle_Down.rawValue,
forAnimationState: .Idle_Down)
animations[.Idle_Up] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Idle_Up.rawValue,
forAnimationState: .Idle_Up)
animations[.Idle_Left] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Idle_Left.rawValue,
forAnimationState: .Idle_Left)
animations[.Idle_Right] = AnimationComponent.animationFromAtlas(
textureAtlas,
withImageIdentifier: AnimationState.Idle_Right.rawValue,
forAnimationState: .Idle_Right)
return animations
}
As mentioned, the character has four walking and four idle states. You'll cover
switching between them when you implement the movement component. Right
now, you must be itching to build and run—but first, you need to add the character
to the scene.
To do this, open GameScene.swift and add these new properties below the
existing entities property:
raywenderlich.com 581
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
//1
var lastUpdateTimeInterval: NSTimeInterval = 0
let maximumUpdateDeltaTime: NSTimeInterval = 1.0 / 60.0
var lastDeltaTime: NSTimeInterval = 0
//2
lazy var componentSystems: [GKComponentSystem] = {
let animationSystem = GKComponentSystem(componentClass:
AnimationComponent.self)
return [animationSystem]
}()
1. You store information about the delta time so it can be calculated in the game
loop.
Note: If you aren't sure if you need to add your component to a component
system, have a look at its structure; if it adopts an updateWithDeltaTime(_:)
loop, consider adding it to a component system to perform the loop.
At the top of update(_:), add this code to calculate the delta time, or the number of
seconds between the last update and this one:
Next, in update(_:), add this code after the delta time calculation:
For each component associated with the entity, you add it to a component system if
one exists for that type of component.
Finally, go to createNodeOf(type:location:) and scroll to the .tileStart case; add
raywenderlich.com 582
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
playerEntity.animationComponent.requestedAnimationState = .Walk_Down
Note: If you skipped ahead from earlier in this chapter, you can continue with
the starter\Delve_Hero project in the resources for this chapter. This has
the hero entity and two components to display and animate the sprite.
It's time for this explorer to begin exploring. Begin by creating a new Swift file
inside your Components group named PlayerMoveComponent.swift.
import SpriteKit
import GameplayKit
//1
var movement = CGPointZero
//2
var lastDirection = LastDirection.Down
//3
var spriteComponent: SpriteComponent {
guard let spriteComponent =
raywenderlich.com 583
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
//4
var animationComponent: AnimationComponent {
guard let animationComponent =
entity?.componentForClass(AnimationComponent.self) else { fatalError("A
MovementComponent's entity must have an animationComponent") }
return animationComponent
}
2. You store the last direction of movement to let the player fall back to an idle
state while maintaining the direction the character is currently facing.
3. You need a link to the sprite component to update its position based on the
calculated movement of the character.
4. You also need a link to the animation component to update its state between
walking and idle, as well as the direction of the animation.
Note: Note you look up the sprite and animation components up dynamically
here. An alternative would be to pass references to these components in the
initializer, but this is more flexible.
Great! Now, you need to fill that function with all of the relevant calculations to
move the character and update the animations.
Start by calculating and setting the new position of the sprite node. Do this by
adding the following at the bottom of the function:
raywenderlich.com 584
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
let yMovement =
((movement.y * CGFloat(seconds)) * playerSettings.movementSpeed)
spriteComponent.node.position = CGPoint(
x: spriteComponent.node.position.x + xMovement,
y: spriteComponent.node.position.y + yMovement)
For each of the x- and y-axes, you find the product of the movement property for
that axis, the delta time and the movement speed. Combined, these three
properties give you the character's movement.
At this point, you'll see an error message because you haven't yet implemented a
movement speed setting. You need to do that now.
struct playerSettings {
//Player
static let movementSpeed: CGFloat = 320.0
Revisit PlayerEntity.swift and add this instance variable, right below the others:
In the initializer for the entity, add the moveComponent declaration right below the
other component declarations:
moveComponent = PlayerMoveComponent()
addComponent(moveComponent)
Tilt to move
This is a good point to implement the controls for your game. In this chapter, you'll
use accelerometer controls.
Head over to GameScene.swift. Once there, you need to import Core Motion, so
add the import statement at the top of the file. Because Core Motion is only
available on iOS (and later you will make this game work on tvOS), you need to use
the #if os(iOS) macro:
#if os(iOS)
import CoreMotion
#endif
With the necessary framework imported, add this code just after your properties:
//Controls
#if os(iOS)
lazy var motionManager: CMMotionManager = {
let motion = CMMotionManager()
raywenderlich.com 585
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
motion.accelerometerUpdateInterval = 1.0/10.0
return motion
}()
#endif
var movement = CGPointZero
The motion manager will receive approximately 10 updates per second, which
should be sufficient for this game.
Scroll down to update(_:) and find the gap between the delta time calculation and
the updating of the components. Once you locate it, add the following code:
//Motion
#if os(iOS)
if (motionManager.accelerometerData != nil) {
movement = CGPointZero
if motionManager.accelerometerData!.acceleration.x > 0.02 ||
self.motionManager.accelerometerData!.acceleration.x < -0.02 {
movement.y =
CGFloat(motionManager.accelerometerData!.acceleration.x)
}
if motionManager.accelerometerData!.acceleration.y > 0.02 ||
self.motionManager.accelerometerData!.acceleration.y < -0.02 {
movement.x =
CGFloat((motionManager.accelerometerData!.acceleration.y) * -1)
}
}
#endif
If the accelerometer differs more than 0.02 in the x- or y-scale, then update the
movement property accordingly. The x- and y-values relate to the device being in
portrait position and don't automatically compensate for screen rotation.
Currently, this game only supports landscape orientation. When you set the
movement property, you compensate by assigning the x-value to the y-value of the
movement property, and the y-value to the x-value of the movement property. You
also need to reverse the y-axis of the accelerometer data.
Now, add this code directly below the code you just added, after #endif:
//player controls
if let player = worldLayer.childNodeWithName("playerNode") as?
EntityNode,
let playerEntity = player.entity as? PlayerEntity {
if !(movement == CGPointZero) {
playerEntity.moveComponent.movement = movement
}
}
This updates the movement property on the player's movement component to the
raywenderlich.com 586
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
The last thing you need to do to get the character moving is tell the motionManager
to begin receiving accelerometer updates.
#if os(iOS)
levelScene.motionManager.startAccelerometerUpdates()
#endif
Excellent! Now build and run the game on your physical device. The character will
move around the screen as you tilt the device.
However, there are a few issues to address with the hero's movement:
• The animations don't update based on the movement speed and direction.
• The camera is static; it would be a lot better if the camera followed the hero
wherever he went.
• The hero can walk through walls, or rather, over the top of them. Adding
raywenderlich.com 587
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
In the root directory for this book, locate SKTUtils and drag the contents into your
Helpers group in Xcode.
switch movement.angle {
case 0:
//Left empty on purpose to break switch if there is no angle
break
case CGFloat(45).degreesToRadians() ..<
CGFloat(135).degreesToRadians():
animationComponent.requestedAnimationState = .Walk_Up
lastDirection = .Up
break
case CGFloat(-135).degreesToRadians() ..<
CGFloat(-45).degreesToRadians():
animationComponent.requestedAnimationState = .Walk_Down
lastDirection = .Down
break
case CGFloat(-45).degreesToRadians() ..<
CGFloat(45).degreesToRadians():
animationComponent.requestedAnimationState = .Walk_Right
lastDirection = .Right
break
case CGFloat(-180).degreesToRadians() ..<
CGFloat(-135).degreesToRadians():
animationComponent.requestedAnimationState = .Walk_Left
lastDirection = .Left
break
raywenderlich.com 588
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
An extension from SKTUtils lets you convert a CGPoint to an angle. Based on the
angle of the movement, you assign a different animation by updating the
requestedAnimationState property.
Also, include the idle animations by adding the following code, right below the code
you just added:
If the movement is zero, then you set an idle animation based on the last direction
moved. Once you've set all the animations, you set the movement property back to
zero.
Build and run your game, and enjoy fluid animations as you move the character
around the screen by tilting the device.
centerCameraOnPoint(location)
You originally added this line to move the camera to the location of the last tile
added. Now, you want the camera to follow the hero.
Find update(_:) and add this code at the bottom of that function:
raywenderlich.com 589
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
You add the code at the end of the function to make sure it executes after the
component updates that cause the hero to move. Build and run the game to test
the camera.
Adding collisions
Walls aren't very useful if the player can simply walk through them. You also need
to think ahead about how game entities will interact and make contact with other
entities—such as the hero picking up a health boost or getting trampled by a rock
golem.
That having been said, open GameSettings.swift and add the following enum
under the other enums:
enum ColliderType:UInt32 {
case Player = 0
case Enemy = 0b1
case Wall = 0b10
case Projectile = 0b100
case Food = 0b1000
case EndLevel = 0b10000
case None = 0b100000
}
Now, navigate to PlayerEntity.swift and add the following code to the initializer,
just after where you add the components:
physicsBody.dynamic = true
physicsBody.allowsRotation = false
physicsBody.categoryBitMask = ColliderType.Player.rawValue
physicsBody.collisionBitMask = ColliderType.Wall.rawValue
physicsBody.contactTestBitMask = ColliderType.Enemy.rawValue |
ColliderType.Food.rawValue | ColliderType.EndLevel.rawValue
spriteComponent.node.physicsBody = physicsBody
You can set up the physics body anywhere, but remember—you must set it to the
physics body property of your sprite component node.
raywenderlich.com 590
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
Now, head back to GameScene.swift and scroll to the top of the file.
physicsWorld.contactDelegate = self
physicsWorld.gravity = CGVector.zero
You state that this class accepts the contact delegate protocol, and you set up the
physics world. All that's left to do is assign a physics body to your walls.
case .tileWall:
let node = SKSpriteNode(texture: atlasTiles.textureNamed("Wall1"))
node.size = CGSize(width: 32, height: 32)
node.position = location
node.zPosition = 1
node.physicsBody = SKPhysicsBody(edgeLoopFromRect: CGRect(origin:
CGPoint(x: -16, y: -16), size: CGSize(width: 32, height: 32)))
node.physicsBody?.categoryBitMask = ColliderType.Wall.rawValue
node.name = "wall"
worldLayer.addChild(node)
break
Build and run, and watch as the walls stop the character dead in his tracks. Where's
his trusty mining pick?
You've only just started building this game—there's plenty more to add! Proceed to
the next chapter, where you'll be procedurally generating levels using
GameplayKit's randomization. But first, if you've been itching to build a bigger tile
raywenderlich.com 591
2D iOS & tvOS Games by Tutorials Chapter 21: Tile Map Games
Challenges
There's just one challenge for this level - and it's a chance for you to be creative!
As always, if you get stuck, you can find solutions in the resources for this chapter
—but give it your best shot first!
Remember, the template variable isn't the only thing you need to update to make
the level bigger. The mapSize property is also very important to avoid any overflow
errors.
It can be tedious to update the template, but remember that you can always copy
and paste rows to save time.
Make sure you post a screenshot of your design on the raywenderlich.com forums
to show off your creativity!
raywenderlich.com 592
22 Chapter 22: Randomization
By Neil North
The first time you proceed into a game dungeon, your senses are alive with the
feeling of adventure. But once you've thoroughly explored the landscape and
learned the secrets of its environment—the thrill is gone, as they say. Along with
some of the entertainment value. You know too much!
What if every time you went back, the layout and environment were completely
different? There may be some familiar elements, but no matter how many times
you went to the dungeon, you'd never learn the fastest or safest way to the end of
the level. No online guide or tutorial could help you; the world would remain elusive
—and random!
When you use the techniques of procedural level generation, you're no longer
involved in building levels. Instead, you build the model, and the game uses that
model to build the levels—dynamically.
raywenderlich.com 593
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
The model contains information about how the level must be built. This can be as
simple as requiring that the end of the level is reachable from the start of the level,
or as complex as requiring that the rooms and pathways connect in a logical way.
To get a wide variety of results from your model, it's essential to use randomization.
This isn't always as simple as picking a random number between 1 and 10.
Thankfully, GameplayKit includes a lot of advanced randomization features that are
well worth exploring.
In the real world, if you throw down some dice, you would call the result random,
because you're unable to predict how the dice will land. This may be functionally
true, but theoretically, if you could somehow measure every contributing factor—
the way the dice were thrown, the velocity of the dice, the hardness of the surface
—and apply the laws of physics, you could predict the result, and it would no longer
be random.
Just like the real world, generating truly random numbers in a software
environment is fundamentally flawed by the fact that we live in a world of cause
and effect. Because computers require logical instructions, it becomes difficult to
perform a calculation without knowing all of the components that go into the task
and how they affect each other.
Confused? Try not to stress about it too much; instead, focus on the point that if
you—or more importantly, the player—are unable to predict an outcome, then it's
technically random.
With apps and games, though, true randomness can reek havoc. For example walls
could spawn between you and the exit that make the level unable to be completed,
raywenderlich.com 594
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
or too many enemies could spawn in narrow spaces creating huge difficulty spikes.
The trick is finding a calculation that results in an acceptable number as far as the
gameplay is concerned, as well as one the user is unable to predict. This means the
results must be free of perceivable and predictable patterns.
Before iOS 9 introduced GameplayKit there were three ways to generate random
numbers:
• arc4random(): This function originated from BSD and has been the most
popular choice of the three functions in recent years. It doesn't require a seed in
order to produce different patterns every time it is used and has a maximum
range twice the size of the other two options, this also allows it to be "more
random" and less likely to follow a pattern than the latter two.
The result will be unpredictable, and thus random—even if, in reality, that result
was pre-determined.
• Sources: These are the algorithms used to produce random numbers. The ARC4
random method is one source, but GameplayKit also includes the Linear
Congruential and Mersenne Twister sources. These will be covered in detail
after you look at the types of distributions available.
raywenderlich.com 595
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Randomization distributions
GameplayKit comes with three standard types of distributions from which you can
choose:
You can combine all three distributions with any of the three sources to provide a
result.
This will let you generate random numbers between 1 and 5, inclusive, using a
random distribution. Think of it as simulating a five-sided die. The distribution itself
does not represent a random number, it is the object that provides random
numbers that comply to the rules of the distribution.
randomFive.lowestValue
randomFive.highestValue
randomFive.numberOfPossibleOutcomes
These return the results 1, 5 and 5, respectively. Each of these properties describes
attributes of the distribution, the lowest value, the highest value and the total
number of possabilities the distribution can yield.
raywenderlich.com 596
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
//1
var i = 0
var numbers:[Int] = []
//2
repeat {
numbers.append(randomFive.nextInt())
i++
} while i < 10
//3
print(numbers)
1. A variable to track the times your loop has been iterated and an array to store
the results.
2. Loop the function ten times requesting the next integer from the distribution
each time.
3. Display the random numbers between 1 and 5, inclusive, that were added to the
array.
The results sidebar will show a similar pattern to the following output: 2, 3, 4, 4,
4, 2, 3, 4, 3, 4 (again, your numbers may vary). Now, the result is weighted
toward the middle of the range of numbers—which in this case is 3.
Finally, modify the initializer again. This time, change it like so:
Now you are using a GKShuffledDistribution with the same initializer as previous,
once again the only thing that changes is how the distribution is handled.
raywenderlich.com 597
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Notice how this result does not repeat a number until all the numbers have been
returned. It's still possible to have two of the same number, side by side, at the
beginning and end of cycles as all numbers are available again at the end of the
cylcle.
The shuffled distribution becomes the most predictable of the distribution as you
know the last number of the cycle will always be the number that hasn't appeared
yet.
Randomization sources
So far, you've only used GameplayKit's default source, ARC4 random, but there are
two other sources available:
• Linear Congruential: The algorithm is faster than ARC4 but also considered to
be less random and more likely to follow a pattern. This source is recommended
when you need to generate results quickly.
• Mersenne Twister: This is the slowest of the three but also the most random,
when true unpredictability is important over speed this is the one to use.
All three methods produce numbers that you're unlikely to predict—so technically,
they're all sufficiently random, and definitely good enough for most games.
raywenderlich.com 598
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Add the following code at the end of your playground to have a look at how you can
implement different random sources:
//1
let randomSource = GKARC4RandomSource()
//2
let randomDist = GKRandomDistribution(randomSource: randomSource,
lowestValue: 1, highestValue: 5)
//3
i = 0
numbers = []
repeat {
numbers.append(randomDist.nextInt())
i++
} while i < 10
print(numbers)
1. First, you set your random source; in this example, you use the ARC4 random
source. You can also try using GKLinearCongruentialRandomSource() and
GKMersenneTwisterRandomSource() to see if you can spot the differences in their
outputs.
3. Using the same variables you assigned above, you loop through ten iterations of
random numbers.
Can you tell the difference in randomness between each result? If you can't detect
a pattern then it is safe to say each method is sufficiently random. The take away
from this section is that the default of ARC4 random should be suitable in most
situations unless performance is an issue.
In particular, you'll be using the drunkard walk algorithm, for which you'll
implement these steps:
raywenderlich.com 599
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
2. Next, you'll select a random spot on the tile map as the start point and mark it
as a ground tile.
4. Then, based on the direction, you'll select a point on the tile map next to the
currently selected point, and mark it as a ground tile, if it isn't one already.
5. You'll repeat steps 3 and 4 until you've reached your desired number of ground
tiles.
This algorithm has several advantages over other level generator algorithms:
• Plenty of possibilities: Just like a drunk stumbling home after a night at the
pub, this algorithm could take you anywhere.
• Guaranteed path: The procedure works by walking the path, so you can be
sure the end of the level is reachable.
• Too random: It's entirely possible that the end of the level could be on top of—
or next to—the start of the level. You can attempt to manage this with different
random distribution types, but ultimately, it could be a problem.
• Not consistent with real world patterns: While this algorithm might work
well for a cave level, it might not for other types of environments, and even
though it's random, you'll run into a lot of the same level attributes.
But not to worry—the solutions to these problems will be covered in the next
chapter, where you'll look at advanced procedural level generation. Before you can
get there, though, you need to learn the basics.
Getting Started
It's time to pick up your dungeon crawler, Delve, from where you left off in the last
chapter.
Before you get started implemementing the drunkard walk algorithm, there are two
gameplay features you have to add to the game: a health bar for the player, and
the ability to tap to start the game.
raywenderlich.com 600
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Note: This is an optional section. If you'd like to build this yourself, keep
reading. But if you'd like to get straight to the drunkard walk algorithm, feel
free to skip to the next section, "Implementing the drunkard walk", where we
will have a starter project ready for you.
First, open GameScene.sks and set the scene's background color to black:
This will look better for areas that don't have any tiles, rather than the default gray.
enum tapAction {
case startGame
case attack
case dismissPause
case nextLevel
case doNothing
}
var gameDifficultyModifier = 1
var gameLoopPaused = true
Here you create an enumeration to keep track of what action a tap will currently
perform. At the beginning of the game, you'll tap to start, but later a tap will
attack, and so on.
You also add some variables to keep track of the current game difficulty, and if the
game is paused.
Open GameScene.swift and add these two properties to the top of the class:
Here you create a property to keep track of the player's health, and keep track of
the current action a track will perform. To start, a tap will start the game, as
mentioned earlier.
Next you'll add a basic GUI to the game to display the player's health. To do this
you'll need some art, drag GUI.xcassets from the resources for this chapter into
your Resources group in Xcode. Be sure to select Copy items if needed, Create
groups, and the Delve target.
raywenderlich.com 601
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Now let's display the GUI. To do this, open GameState.swift and find
GameSceneInitialState. Inside didEnterWithPreviousState(_:), replace the line
levelScene.paused = false with the following:
levelScene.paused = true
gameLoopPaused = true
levelScene.tapState = .startGame
This pauses the game through SKScene's built in `pause' property, and adds the GUI
elements to the game. This should be review from previous chapters in this book.
This dismisses the temporary "Tap to Start" UI that you placed within the overlay
layer when you leave this state.
raywenderlich.com 602
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
After the user taps, you will enter the active state. When you enter this state, you
unpause the game and set the tap action to attack.
Later you'll add the ability to pause the game. When you enter the pause game
state, you pause the game and set the tap state to dismiss the pause.
This state may not make much sense just yet. This state allows the game to
continue without any actions taking place, it will be used when the player completes
the level to give them a moment to enjoy their victory before the victory state is
called.
levelScene.tapState = .nextLevel
raywenderlich.com 603
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
When the player eventually finds the exit, you'll transition to this state. It displays a
"You won" overlay, and increases the difficulty for next time.
levelScene.tapState = .nextLevel
If the player runs out of health, you'll transition to this state. It displays a "You
lose" overlay.
switch tapState {
case .startGame:
stateMachine.enterState(GameSceneActiveState.self)
break
case .attack:
//To be added
break
case .dismissPause:
stateMachine.enterState(GameSceneActiveState.self)
break
case .nextLevel:
if let scene = GameScene(fileNamed:"GameScene") {
scene.scaleMode = (self.scene?.scaleMode)!
let transition = SKTransition.fadeWithDuration(0.6)
view!.presentScene(scene, transition: transition)
}
break
default:
break
}
Based on the tap state, you perform different actions here. Usually, you switch to a
different state in the state machine.
raywenderlich.com 604
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
if gameLoopPaused { return }
This simply updates the health label to the player's current health after update()
has finished.
Finally, add the splash screen and icon for this app. To do this, delete
Assets.xcasset from your project and add the replacement I've created for you in
the resources for this chapter. Then, add an image view to your
LaunchScreen.storyboard set up to display DELVE-splashscreen2. If you forgot
how to do this, refer to Chapter 1, "Sprites".
raywenderlich.com 605
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
workspace works well for random walk paths as it gives you room to move in each
direction. Modify this property like so:
Next, you need a way to ensure the path stays within the bounds of the map to
avoid overflow errors—after all, there's no telling where a drunk might end up.
To do this, add the following function below tileMapSize(); not within the function,
but after it:
Based on the position provided in relation to the map size, this function returns
true if the tile is within bounds, and false otherwise.
You also move the boundary in by a tile to avoid ending up with a ground tile next
to "air", inadvertently letting the player walk outside the bounds of the map.
//1
var currentLocation = CGPoint(
x: GKGaussianDistribution(
lowestValue: 2,
highestValue: Int(mapSize.x) - 2).nextInt(),
y: GKGaussianDistribution(
lowestValue: 2,
highestValue: Int(mapSize.y) - 2).nextInt())
//2
let direction = GKRandomDistribution(forDieWithSideCount: 4)
1. You need a starting point along both the x- and y-axes. While anywhere within
the bounds would be fine, using a Gaussian distribution ensures that most of
the time, the start point will be toward the middle of the tile map.
2. There are four cardinal directions in which the generator can move; this random
distribution will select from among them.
Since the current location will be your starting point, mark it as such by adding the
following line right below the two variables you just added:
raywenderlich.com 606
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Build and run to see the result (ignore the warnings for now).
Note: Don't forget to tap to start the game—and see the map!
That's not the most exciting map—yet. However, at least you know procedural map
generator is definitely capable of assigning a start point. :]
One problem you can see straight away is that there's no limit to where the
character can move. You can fix this by changing the default tile type: The level
generator requires them to be walls.
worldGen.generateLevel(1)
Build and run again to see the results of this change. Now the character will be
correctly imprisoned in a world of bricks.
What a nightmare! And he doesn't even have his mining pick yet.
To give your miner more space, navigate back to LevelHelper.swift and add the
following line at the end of generateMap():
raywenderlich.com 607
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
var i = 40
This will act as your count of how many tiles the procedure should move when
creating the path. This number may not be perfect, but you'll be able to tweak it
after you begin to see results.
//1
repeat {
//2
var newPosition = CGPointZero
switch direction.nextInt() {
case 1:
newPosition = CGPoint(x: currentLocation.x, y: currentLocation.y - 1)
case 2:
newPosition = CGPoint(x: currentLocation.x, y: currentLocation.y + 1)
case 3:
newPosition = CGPoint(x: currentLocation.x - 1, y: currentLocation.y)
case 4:
newPosition = CGPoint(x: currentLocation.x + 1, y: currentLocation.y)
default:
break
}
//3
if isValidTile(position: newPosition) {
//4
if getTile(position: newPosition) <= 3 {
currentLocation = newPosition
i--
//5
if i == 0 {
setTile(position: currentLocation, toValue: 5)
} else {
setTile(position: currentLocation, toValue: 3)
}
}
}
} while i > 0
2. You create a newPosition variable and assign it a location on the grid, based on
the four cardinal directions in which you can move. It doesn't matter which
number is which, as long as you cover all four directions.
3. You need to test if the new tile is within the acceptable bounds of the tile map,
so you use the isValidTile(position:) function you created earlier.
4. You check if the tile is a wall type. This will make sure you aren't overwriting a
start or end point.
5. If all checks pass, you set the tile as a ground tile. If the variable i has reached
0, then you set it to an end tile.
raywenderlich.com 608
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Once again, if all goes according to plan, yours should look completely different
from mine. :]
Before you tweak the generator further, now would be a good time to take care of
that black hole in your level where the end point should be.
Create a new Swift file inside the Entities group and name it
LevelEndEntity.swift. Replace the contents of the file with the following:
import Foundation
import UIKit
import SpriteKit
import GameplayKit
override init() {
super.init()
raywenderlich.com 609
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
physicsBody.categoryBitMask = ColliderType.EndLevel.rawValue
physicsBody.dynamic = true
spriteComponent.node.physicsBody = physicsBody
}
}
This entity is fairly standard. It handles a sprite component and a physics body to
detect the touch between the hero and the stairs. It has no other components.
case .tileEnd:
let levelEndEntity = LevelEndEntity()
let levelEndNode = levelEndEntity.spriteComponent.node
levelEndNode.name = "levelEnd"
levelEndNode.position = location
levelEndNode.zPosition = 1
addEntity(levelEndEntity)
break
Once again, this is very similar to the code you added for your other tiles.
Build and run to make sure the exit tile appears correctly.
Now, you need to make the exit tile trigger the level change, and make the scene
reload with a new level to explore.
Scroll to the bottom of GameScene.swift and include this code at the bottom:
raywenderlich.com 610
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
movement = CGPointZero
}
}
In the previous chapter, you added the SKPhysicsContactDelegate, and this is one of
the functions that goes with the protocol.
centerCameraOnPoint(location)
Now when you build and run, it will look a lot neater as the camera will center on
the start point. When you tap to start it will already be in the correct position.
Build and run, and you can now beat the level!
raywenderlich.com 611
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
Some of these results aren't great, and if you want to add features to the game,
such as enemies or health pick-ups, then there's no logical place to put them right
now.
Now you will implement a few improvements to the level generator to create more
interesting levels.
In theory, the shuffled distribution will always produce rooms of various sizes and it
would be impossible to create a straight corridor, since the generator will have to
move in all four directions before it can move in the same direction again.
raywenderlich.com 612
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
You would expect the result to still be random, but include corridors in the two
directions in the middle of the range.
Since the Gaussian distribution doesn't follow strict rules, it's entirely possible that
your level looks nothing like this. Also note that this level was built next to a
bounding wall, so down may not have been the generator's first choice for the
direction. That aside, there does seem to be a massive trend in at least two
directions.
Combining distributions
You can draw some conclusions from the exercise you just completed:
• Gaussian distributions are good for making corridors in the two most
prominent directions.
raywenderlich.com 613
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
No changes have been made to these lines from when you first implemented them.
You select a start point weighted towards the middle of the map by using gaussian
distribution then set the first tile as the start point.
You add all distribution types to an array, and then you use an additional random
distribution to pick a distribution from the array. Clever, right? :]
You could also cycle between Gaussian and shuffled to get a selection of rooms
connected by corridors, but this should produce more interesting results.
let movementsPerSet = 35
let numberOfSets = 15
Each generator will run 35 times and then switch to the next generator, 15 times
total. Once again, you can tweak this to your liking.
raywenderlich.com 614
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
//1
for (var i = numberOfSets; i >= 0; i--) {
//2
let currentGen = generators[generatorPicker.nextInt() - 1]
//3
for (var j = movementsPerSet; j >= 0; j--) {
var newPosition = CGPointZero
switch currentGen.nextInt() {
case 1:
newPosition = CGPoint(x: currentLocation.x, y:
currentLocation.y - 1)
case 2:
newPosition = CGPoint(x: currentLocation.x, y:
currentLocation.y + 1)
case 3:
newPosition = CGPoint(x: currentLocation.x - 1,
y: currentLocation.y)
case 4:
newPosition = CGPoint(x: currentLocation.x + 1,
y: currentLocation.y)
default:
break
}
if isValidTile(position: newPosition) {
if getTile(position: newPosition) <= 3 {
currentLocation = newPosition
if i == 0 && j == 0 {
setTile(position: currentLocation, toValue: 5)
} else {
setTile(position: currentLocation, toValue: 3)
}
}
}
}
}
2. You select a random distribution, at random. Tired of hearing the word random
yet? :]
3. You prepare another for loop for the number of generations within each set.
raywenderlich.com 615
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
While the levels should be a lot more interesting, after you've generated a few,
you'll notice a trend where the level path moves toward the bottom left of the
screen.
It would be great if you could shuffle the directions for each set. It's a good thing
you know all about shuffling numbers!
Right under the constants you set for movements per set and numbers of sets, add
this:
Now, just inside the first for loop, under the currentGen constant, add the
following:
Since you're using a shuffled distribution of four possible outcomes, you know the
pattern will never contain more than one of each number.
Finally, add this constant above the switch and update the switch to this:
This means that for the entire set, the directions will be based on the pattern. This
provides some consistency to the set; however, it also means the next set will
probably be completely different. That's exactly what you want, it allows for the
possability of consistent hallways but doesn't allow them to always trend in the
same direction.
raywenderlich.com 616
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
It doesn't have to end here—you can keep tweaking the randomness to your heart's
content, as well as adjust the number and size of the sets.
In the real world, everything happens for a reason. The next chapter brings Delve
closer to reality by using room/block/sector-based level design with a touch of
randomness to make sure there's always variation in the gameplay. This is the
same method that games like Diablo III and Spelunky use to generate their levels
to be both functional and unpredictable.
It's also time to think about those rock golems. They're out there somewhere,
lurking in the shadows. Don't worry, though—you have a weapon for that!
Challenges
There's just one challenge for this chapter, designed to give you a bit more practice
with GameplayKit's randomization.
If you get stuck, you can find solutions in the resources for this chapter—but give
this your best shot before you look!
raywenderlich.com 617
2D iOS & tvOS Games by Tutorials Chapter 22: Randomization
would all look the same. They would have different colorations, be in different
conditions, some might have rock golem graffiti...
Lucky for you, there are eight different tile types in the Tiles.atlas folder for you to
use.
You can add all of these tiles to an array of strings, and then use a random
distribution to choose a tile based on the count of the array.
Don't forget, you could also randomize the ground below your player's feet at the
start point as well.
raywenderlich.com 618
23 Chapter 23: Procedural Levels
By Neil North
In Chapter 21, "Tile Map Games", you created an entire level of a dungeon game by
setting number codes in an array. Then, in Chapter 22, "Randomization", you used
GameplayKit's randomization features to procedurally generate entire levels.
As the levels layout become more logical, it also gives you more logical places to
spawn game objects. For example, when you enter a kitchen in a new building, it
may be completely different to any kitchen you have seen before but you would
expect cupboards and benchtops against the walls, a window on at least one side,
and that appliances and a sink be on the benchtops. If these expectations aren't
met then the kitchen would not feel like a kitchen.
Your overall objective is to make levels that feel as if they were each handcrafted by
a human being, not generated on the fly by a machine. Using room-based
procedures is one of the most effective ways to achieve this. In simple terms, you
prebuild a selection of regions or rooms which the game then stitches together in a
random arrangement.
In this chapter, you will implement the new world generator and finish the core
functionality of Delve.
raywenderlich.com 619
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Note: This chapter begins where the previous chapter's challenge left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter to pick
up in the right place.
• You provide the game with a selection of pre-built rooms that it can place in
different patterns.
• While the rooms are pre-built, their arrangement within the grid, or in the level
at large, is almost completely random.
• Structured and controllable: The resulting level is still unpredictable, but you
maintain a lot of control over the layout and structure of the level.
• Human-built feel: Each room is handcrafted by you, making the level feel more
like a "real" dungeon.
• Stability: While you may not be able to test every permutation your level
generator can create, you'll have a lot more confidence than you would in fully
random methods.
raywenderlich.com 620
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
One drawback is that it can be a lot of work to hand-craft enough rooms to keep
the game interesting. But for a simple game like Delve, there isn't much to dislike
about this method.
To simplify the process of laying out the level, you'll make each room the same
size: 10 tiles wide by 10 tiles high.
You're going to be procedurally distributing the rooms throughout the grid, but not
in an entirely random fashion. So you need to think about any restrictions you
might want to put on the positions of the different rooms in order to get a level
design that suits your purposes. For example, you don't want to inadvertently leave
any gaps in the walls that lead outside the bounds of the level.
Below is a sample level with slicing to show where each room is situated and what
type of room it is.
raywenderlich.com 621
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Each section contains wall pieces for the left side and top of the room only (with the
exception of sections at the bottom and right sides of tile map). This makes it
easier to create adjoining rooms, and helps you avoid walls of double thickness,
which would cost you space.
Now that you've seen the layout, it's time to start building the rooms.
There are a few different ways you can store your room data. One of the simplest
and safest is using structs.
import Foundation
struct tileMapSection {
struct sectionMiddle {
}
struct sectionTopLeft {
}
struct sectionTopRight {
}
struct sectionBottomLeft {
}
struct sectionBottomRight {
}
struct sectionLeft {
}
struct sectionRight {
}
struct sectionTop {
}
struct sectionBottom {
}
}
Each section of the grid with have its own set of one or more rooms, each room
with a different tile configuration.
raywenderlich.com 622
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Next comes the creative part: making the rooms. It can be quite confusing to work
with a bunch of numbers, so I recommend keeping this reference somewhere
handy:
Air = 0
Wall = 1
Wall Light = 2
Ground = 3
Start = 4
Finish = 5
Enemy Spawn = 6
Food Spawn = 7
Each section in the array is a ten by ten tile map room. In sectionMiddle the walls
have been placed on the left and top sides. It can be daunting to look at a block of
numbers but with some practice you will start to see each room like this:
raywenderlich.com 623
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Now, you'll need to create rooms for all the section types. How many rooms you
create for each section depends on the level of variation you'd like.
If you aren't feeling creative or simply don't wish to make all of the rooms, you can
find an already completed version of the LevelComponents.swift file located at
starter\Resources for your convenience.
Note: Having lots of physics bodies can present complicated calculations each
frame, and the more of them you have, the more likely you are to experience
a frame rate drop. Keep this in mind when adding wall tiles to your rooms.
Once you have at least one room for each section, you're ready to update the level
generator. Feel free to add more than one room per section. I'm sure you realize by
now that more is better!
The sectionSize constant sets the size of each room as 10 by 10 tiles. You also
need to know how many rooms there will be on the grid; you handle this in the
sections variable.
Now your mapSize variable is wrong. There's a quick and easy way to handle this
problem. Update mapSize as follows:
raywenderlich.com 624
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
var mapSize:CGPoint {
get {
return CGPoint(x: sections.x * sectionSize.x,
y: sections.y * sectionSize.y)
}
}
This code will calculate the map size every time you request it, based on the
number of sections and the number of tiles within each section.
You need a simpler way to copy a room template to your tile map layer. Add this
function under the level creation area of the level helper class:
The function accepts the template, which is a two-dimensional array of tiles. It also
accepts a section index, which lets you locate the position of each tile within its
section.
Now you need a way to handle the layout of the rooms. Add this function below
generateMap():
//Left Tiles
randomSection = GKRandomDistribution(
forDieWithSideCount: leftTiles.count)
setTilesByTemplate(leftTiles[randomSection.nextInt() - 1],
sectionIndex: CGPoint(x: 0, y: rowIndex))
//Right Tiles
randomSection = GKRandomDistribution(
forDieWithSideCount: rightTiles.count)
setTilesByTemplate(rightTiles[randomSection.nextInt() - 1],
sectionIndex: CGPoint(x: Int(sections.x - 1), y: rowIndex))
//Middle Tiles
var i = 2
randomSection = GKRandomDistribution(
forDieWithSideCount: middleTiles.count)
repeat {
setTilesByTemplate(middleTiles[randomSection.nextInt() - 1],
sectionIndex: CGPoint(x: i - 1, y: rowIndex))
raywenderlich.com 625
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
i++
} while i < Int(sections.x)
When you run this function, you pass in the row index for the section, and the
sections from which you wish to select rooms. For example, if you were adding the
bottom row of the grid, then you'd pass in an index of the count of total rows minus
one, and then the bottom-left, bottom-middle and bottom-right sections.
//top Row
setTemplateBy(0,
leftTiles: tileMapSection.sectionTopLeft.sections,
middleTiles: tileMapSection.sectionTop.sections,
rightTiles: tileMapSection.sectionTopRight.sections)
//Middle Row
var row = 2
repeat {
setTemplateBy(row - 1,
leftTiles: tileMapSection.sectionLeft.sections,
middleTiles: tileMapSection.sectionMiddle.sections,
rightTiles: tileMapSection.sectionRight.sections)
row++
} while row < Int(sections.y)
//Bottom Row
setTemplateBy((Int(sections.y) - 1),
leftTiles: tileMapSection.sectionBottomLeft.sections,
middleTiles: tileMapSection.sectionBottom.sections,
rightTiles: tileMapSection.sectionBottomRight.sections)
For each type of row, you run the function you just added. Since there can be more
than one middle row, you repeat the function until you run out of middle rows.
raywenderlich.com 626
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Even though our results are completely different, I can say with confidence that
your dungeon looks great! Except for the black holes everywhere—it's time to fill
them with content.
Note: For the rest of this chapter, you're going to implement the remaining
tiles and gameplay for Delve. It's pretty cool stuff, but if you only wanted to
learn about procedural levels, feel free to skip to the next chapter, where you'll
learn how to add support for game controllers.
case .tileWallLit:
let node = SKSpriteNode(texture: atlasTiles.textureNamed("Wall2"))
node.size = CGSize(width: 32, height: 32)
node.position = location
node.zPosition = 1
node.physicsBody = SKPhysicsBody(edgeLoopFromRect: CGRect(
origin: CGPoint(x: -16, y: -16),
size: CGSize(width: 32, height: 32)))
raywenderlich.com 627
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
node.physicsBody?.categoryBitMask = ColliderType.Wall.rawValue
node.name = "wall"
worldLayer.addChild(node)
break
That's it. Build and run to make sure your wall tiles appear correctly and that your
miner can't pass through any of them.
There are still some black spots. That's OK! You'll fill in those areas next.
Handling health
There isn't anything in the level that can harm you yet. The rock golems will
emerge from the shadows soon, but in the meantime, you need something to give
the character a sense of urgency to escape the level.
Dungeons aren't always the most healthy of environments, so the player's health
will decline automatically over time.
//Timers
var lastHealthDrop: NSTimeInterval = 0
This will let you track the time between drops in health.
Proceed to the update(_:) loop and insert the following right above the "Update all
components" comment:
raywenderlich.com 628
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
//2
if health < 1 {
stateMachine.enterState(GameSceneLoseState.self)
}
1. This makes the miner's health drop by five for every two seconds the game is
running. All told, this gives the player about six and a half minutes to complete
the level. That may seem like a lot, but when enemies are attacking, time will
fly!
2. If health is below 1, then the hero has died, and you'll progress the game to the
losing screen.
To make sure everything works as expected, build and run, and keep an eye on the
health bar at the top of the screen.
Gameplay can feel rather bleak when all you seem to do is lose health. You can fix
that by adding health boosts.
In your Entities group, add a new file named FoodEntity.swift. Replace the code
inside with the following:
import Foundation
import UIKit
import SpriteKit
import GameplayKit
override init() {
super.init()
raywenderlich.com 629
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
spriteComponent.node.physicsBody = physicsBody
}
}
This entity is very straightforward: It's a non-animated sprite node with which the
hero can make contact.
case .tileFood:
//1
let node = SKSpriteNode(texture:
atlasTiles.textureNamed(textureStrings[randomFloorTile.nextInt() - 1]))
node.size = CGSize(width: 32, height: 32)
node.position = location
node.zPosition = 1
worldLayer.addChild(node)
//2
let food = FoodEntity()
food.spriteComponent.node.name = "foodNode"
food.spriteComponent.node.position = location
food.spriteComponent.node.zPosition = 5
addEntity(food)
break
1. Since the food will be collectable, you need to add a ground tile underneath it.
2. Add the food to the scene, and remember to name it correctly, as you'll be using
the name for the contact delegate.
Build and run to make sure the food appears correctly in your scene.
raywenderlich.com 630
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
While it looks tasty, nothing happens when you make contact with it. Scroll down to
didBeginContact(_:) and add this if statement at the bottom:
if bodyA?.name == "foodNode" {
if bodyB?.name == "playerNode" {
bodyA?.removeFromParent()
health = health + 40
}
}
When a node named "playerNode" makes contact with a node named "foodNode",
you remove the food node from the scene and increase the hero's health by 40
points.
Note: You still want the player to feel under pressure, so don't set the health
boost too high. It should be enough to make it worth picking up, but not
essential to completing the level successfully.
A texture atlas has been provided for you at starter\Resources with the name
enemy.atlas. Drag the texture atlas into the Resources group in your project.
You'll need a new entity to support your enemy characters. In your Entities group,
add a new file named EnemyEntity.swift.
raywenderlich.com 631
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
import Foundation
import UIKit
import SpriteKit
import GameplayKit
You provide a link to the sprite and animation components and a variable for the
enemies’ health. They're made from stone and some sort of magic, so you would
expect they could take a hit or two, right?
override init() {
super.init()
physicsBody.categoryBitMask = ColliderType.Enemy.rawValue
physicsBody.collisionBitMask = ColliderType.Wall.rawValue
physicsBody.contactTestBitMask = ColliderType.Player.rawValue
physicsBody.allowsRotation = false
spriteComponent.node.physicsBody = physicsBody
}
• You don't currently have a movement component for enemies. You'll be adding
this soon.
raywenderlich.com 632
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
return animations
}
2. You'll be doing your best to fight back against the golems, so if you have any
intention of winning the battle, you definitely need to give them a death state.
The entity is almost ready, but you're still missing a movement component. In the
Components group, add a new file named EnemyMoveComponent.swift.
import SpriteKit
import GameplayKit
Before you add the component, it's important to consider that it will work a little
differently than the other updatable components.
This component will require the position of the hero character for every update; it
will check if the hero is within the follow range, and if so, attempt to follow him.
There are a few ways you could handle this, but one of the most efficient would be
to modify the component's system. It already sends an update command every
frame, so it may as well provide the location of the hero, too.
raywenderlich.com 633
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
A component system normally has a function to update all of its components; this
one now has a function to do that and pass in the playerPosition as well.
The movement component will need access to the sprite to alter its location, and
access to the animation to change the animation that's currently running. Add this
code inside the new class you just created:
init(entity: GKEntity) {
raywenderlich.com 634
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Before you add the update loop, there's some additional information you'll need
about how the enemies move and acquire their targets.
//Enemy
static let enemyMoveSpeed: CGFloat = 70.0
static let enemySenseRadius: CGFloat = 300.0
static let enemyDamagePerHit: CGFloat = 0.55
You can adjust the speed and target acquisition radius as required. The health
property you added above is equal to 1.0; if each attack does 0.55 damage, then it
will take two hits to kill an enemy.
The first thing you'll need to decide is if the enemy should start attacking. Add this
code within the braces:
raywenderlich.com 635
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
if spriteComponent.node.position.distanceTo(playerPosition) <
playerSettings.enemySenseRadius {
isAttacking = true
}
Using an SKTUtils extension named distanceTo(_:), and the game setting you
added above, you check if the hero is within range. If the hero's close enough, you
send the enemy on the attack.
If isAttacking is true, then you'll need to move the enemy. Add this right below the
code you just added:
if isAttacking {
//1
var direction = (playerPosition - spriteComponent.node.position)
direction.normalize()
direction = CGPoint(x: direction.x * (CGFloat(seconds) *
playerSettings.enemyMoveSpeed), y: direction.y * (CGFloat(seconds) *
playerSettings.enemyMoveSpeed))
//2
spriteComponent.node.position += direction
//3
switch direction.angle {
case CGFloat(45).degreesToRadians() ..<
CGFloat(135).degreesToRadians():
animationComponent.requestedAnimationState = .Walk_Up
break
case CGFloat(-135).degreesToRadians() ..<
CGFloat(-45).degreesToRadians():
animationComponent.requestedAnimationState = .Walk_Down
break
case CGFloat(-45).degreesToRadians() ..<
CGFloat(45).degreesToRadians():
animationComponent.requestedAnimationState = .Walk_Right
break
default:
animationComponent.requestedAnimationState = .Walk_Left
break
}
}
1. You find the direction from the enemy to the player by subtracting the enemy
position from the player position. Then, using the SKTUtils normalize function,
you reduce the distance to a common peak. Then, you work out how far to
move the hero by using the delta time, the new direction and the enemy speed.
2. You update the sprite node's position by adding the direction. Once again, this
isn't standard functionality and requires SKTUtils.
3. Just as the hero required, you'll need to calculate the angle of the enemy's
walk. Unlike the hero, there's no idle state, so you don't need to handle a zero
angle.
raywenderlich.com 636
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Those are all the components your enemy needs! But before you can preview it,
you need to add the character to the scene.
Scroll down to update(_:) and replace the code just below the "Update player
after components" comment with this:
1. If the player exists, then you tell the enemy movement system the player's
current location and the delta time.
3. You haven't yet implemented what happens when the player takes damage.
When you get to this stage, this code will remove the shader applied to your
character when he takes damage.
Since you haven't implemented the lastHurt timer, go to your class properties
again and add this under the lastDeltaTime property:
case .tileEnemy:
let node = SKSpriteNode(texture:
atlasTiles.textureNamed(textureStrings[randomFloorTile.nextInt() - 1]))
node.size = CGSize(width: 32, height: 32)
node.position = location
raywenderlich.com 637
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
node.zPosition = 1
worldLayer.addChild(node)
To make sure you've added the enemy to the right layer, go to addEntity(_:) and
replace what's currently there with this:
entities.insert(entity)
They flock toward the hero just as you want, but they don't do anything when they
make contact with him.
if bodyA?.name == "enemyNode" {
if bodyB?.name == "playerNode" {
//1
bodyA?.removeFromParent()
raywenderlich.com 638
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
//2
health = health - 50
if let player = worldLayer.childNodeWithName("playerNode")
as? EntityNode,
playerEntity.spriteComponent.node.removeActionForKey("flash")
playerEntity.spriteComponent.node.runAction(SKAction.sequence([
SKAction.colorizeWithColor(SKColor.redColor(),
colorBlendFactor: 1.0, duration: 0.5),
SKAction.colorizeWithColor(SKColor.whiteColor(),
colorBlendFactor: 1.0, duration: 0.5),
]), withKey: "flash")
lastHurt = 0.0
}
}
}
1. Upon impact with a player, you destroy the rock golem. It's harsh, but if they
want to protect the dungeon, sacrifices must be made.
2. You reduce the player's health by 50, which means that even if you're a fast
runner, you can't really afford to take too many hits.
3. In all of the chaos, it can be hard to tell if you're taking damage. You signal this
quite clearly by adding a simple sequence of actions that makes the hero flash
red when hit.
It works as you intended. But it's a little rough: one miner against hordes of rock
golems, with no way to defend himself. Now you're going to tip the scales.
Creating a projectile
Armed with an unlimited supply of mining picks, the hero will be prepared to fight
raywenderlich.com 639
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
All movement in the game happens via the accelerometer, so the screen itself is
completely free to accept inputs that affect the gameplay.
Thanks to the camera, the hero remains at the center of the screen. A tap to the
left of the hero will throw the projectile left, while a tap to the right will throw right,
and so forth all the way around the circle. You'll be able to throw the projectile from
all 360 degrees around the hero, based on the tap location.
To make the gameplay more fluid and ease the player's frenzied hands, when the
player taps a direction, the hero should continue throwing in that direction until the
player taps a different direction. You'll also need to make sure the hero can't throw
the weapon too frequently.
Go to your properties in GameScene.swift and locate the timers. Add this to the
timers:
Also, in your properties, there's a section for controls. Add the following in that
section:
This is where you'll the store angle of the attack until the player selects a new
angle.
Now, scroll down to touchesBegan(_:) and find the .attack case; update it as
follows:
case .attack:
for touch in touches {
let location = touch.locationInNode(self)
if let player = worldLayer.childNodeWithName("playerNode") {
playerAttack = location - player.position
}
}
break
Based on the location of the tap in relation to the hero's position, you update the
playerAttack instance variable.
Now, navigate to update(_:), which is where you'll be telling the hero to throw his
weapon. At the bottom of the function, add this code:
if playerAttack != CGPointZero {
if lastUpdateTimeInterval > (lastThrow + 0.3) {
if let player = worldLayer.childNodeWithName("playerNode") {
let atlasTiles = SKTextureAtlas(named: "Tiles")
let node = SKSpriteNode(texture:
atlasTiles.textureNamed("Projectile"))
raywenderlich.com 640
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
To recap, you've already set up the controls for selecting the direction of the throw,
and with this code, you implement the throwing action itself.
Note: If you don't want the player to auto-throw the weapon, you can set
playerAttack back to CGPointZero at the end of the if statement, and it won't
fire again until the next tap.
You'll get an error at this point, because you haven't created the projectile entity
yet. Now would be a great time to add it. :]
In the Entities group, create a new file named ProjectileEntity.swift and replace
its contents with the following:
import Foundation
import UIKit
import SpriteKit
import GameplayKit
import SpriteKit
import GameplayKit
node.entity = entity
node.position = origin
nodeDirection = direction
//4
nodeDirection.normalize()
raywenderlich.com 641
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
1. Since there are no animations, there's no reason why this component can't have
its own sprite instead of using a sprite component.
2. You can tweak the speed and rotation until you feel comfortable with the
results.
3. This initializer is a little different, since the entity already knows its behavior at
the point node creation.
5. You set the rotation of the node based on the speed, delta time and current
rotation.
6. You set a new position for the node based on the direction, speed and delta
time.
node.position = CGPointZero
physicsBody.categoryBitMask = ColliderType.Projectile.rawValue
physicsBody.collisionBitMask = ColliderType.None.rawValue
physicsBody.contactTestBitMask = ColliderType.Wall.rawValue |
raywenderlich.com 642
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
ColliderType.Enemy.rawValue
physicsBody.dynamic = true
projComponent.node.physicsBody = physicsBody
projComponent.node.name = "projectile"
projComponent.node.addChild(node)
}
This is very similar to your other entities, but take note that each initializer must
know the node, the origin point of the throw and the direction of the throw.
The function adds nodes on the assumption that they belong to a SpriteComponent.
Since this component is a little different, add the following code at the end of the
function:
There's one last thing: You need to implement the component system for the
projectile move component—unless you want to leave a trail of pick axes behind
you, that is.
Now you can build and run to make sure the projectile works as intended.
raywenderlich.com 643
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
The weapon works, but you still need to configure its contacts. If the projectile hits
an enemy, you need to inflict damage on that enemy and destroy the projectile.
You should also destroy the projectile if it hits a wall. You don't want the hero to be
able to kill enemies through the walls.
1. You destroy the projectile on impact and remove it from the scene.
2. You inflict the standard damage on the enemy from your playerSettings.
3. If the enemy’s health is equal to or below 0.0, you kill the enemy. This includes
setting its animation to the death state and removing its move component.
4. To stop the miner from taking damage as he walks over the body, you remove
the enemy’s physics body. After a short period of time, you fade the alpha
channel of the enemy to 0.0 and remove the enemy from the scene.
//1
if bodyA?.name == "wall" {
if bodyB?.name == "projectile" {
bodyB?.removeFromParent()
}
raywenderlich.com 644
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
//2
if bodyA?.name == "projectile" {
if bodyB?.name == "enemyNode" {
damageEnemy(bodyA!, enemyNode: bodyB!)
}
}
if bodyA?.name == "enemyNode" {
if bodyB?.name == "projectile" {
damageEnemy(bodyB!, enemyNode: bodyA!)
}
}
1. If the projectile makes contact with the wall, you destroy the projectile.
2. If either the projectile or the enemy makes contact with each other, then you
run the function you created in the last code block.
You've now implemented the core mechanics and given your levels structure. This is
the point at which you can have some fun trying different grid sizes and room
layouts.
But there's still a lot of great functionality to cover. Once you're ready, proceed to
the next and final chapter for Delve, where you'll add a lot of important polish to
take this game from beta to production quality. You'll also prepare Delve to run on
the new Apple TV with tvOS, and give it production-quality resources.
Dungeon crawlers like Delve feel great when played with a video game controller.
You'll be adding support for controllers in both iOS and tvOS environments, using a
class that's easy to port to any iOS or tvOS game!
raywenderlich.com 645
2D iOS & tvOS Games by Tutorials Chapter 23: Procedural Levels
Challenges
In addition to procedurally generating levels, you can increase the game's difficulty
procedurally too. Your challenge is to give that a shot!
As always, you can find the solution among the chapter's resources—but give it
your best shot first.
Challenge 1: Difficulty
As you complete each level, the level counter goes up, but the difficulty doesn't
change at all. Can you think of a way to make the game incrementally more difficult
as the player delves deeper into the dungeon?
raywenderlich.com 646
24 Chapter 24: Game Controllers
By Neil North
Ever since the earliest video game consoles, the controller has been synonymous
with gaming: It's hard to think of one without the other.
When you consider how much technology and trends have changed, it's truly
amazing how similar the controllers of today look to the earlier models. It shows
just how well they were designed, even in their infancy.
The only thing that's come close to knocking the controller down a notch has been
the rise of touchscreen devices with accelerometers. The number of phones and
tablets available can't be ignored, and developers have changed the way they make
games in order to keep up with the emerging market.
In iOS 7, Apple announced support for MFI game controllers and provided
developers with a framework to tap into them—either via lightning connector or
Bluetooth.
If the vast majority of modern phone and tablet games are designed for
touchscreens and accelerometers, why should you care about game controllers?
Here's why:
• First person genre: Powerful engines like Unity and Unreal Engine have made it
possible for developers to bring the first person experience to mobile. You can
raywenderlich.com 647
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
use virtual controls onscreen, but they are nowhere near as precise or
comfortable as a controller.
• Precision and comfort: Onscreen thumbsticks have reached their peak, and
quite often don't offer the precision of real thumbsticks. It's also nice to sit with
the tablet in front of you and get into a comfy position rather than having to sit
looking down at it.
• No screen obstructions: When you're touching the screen, you're also blocking
your view of the game environment.
• The new Apple TV: You can support MFI controllers for the Apple TV with
almost exactly the same code.
• Not every genre makes sense: Can you imagine playing a match-three-style
game with a controller? Some modern touchscreen games simply don't work with
a controller.
• The rules: Apple wants to maintain an App Store where any game available can
be played entirely without a controller. This makes perfect sense, but it also
creates a few challenges for developers—especially developers who want to
release tvOS games meant to be played with a regular game controller due to
the limited inputs of the Apple TV remote.
• Market is still niche: This is both a good and a bad thing. While controllers are
generally not too hard to implement, there might not be enough positives to
support it in some cases.
In this chapter, you'll implement support for MFI game controllers in Delve. Then
you will add a target for tvOS and add controller support for tvOS as well including
using the Apple TV Remote as a controller.
Controller formats
There are a number of different controllers on the market, but don't worry, you
don't have to support all of them, individually. There are two standard layouts that
can be supported:
• Standard gamepad: The standard gamepad has a D-pad, two shoulder buttons,
raywenderlich.com 648
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
With the addition of the Apple TV and tvOS, there's a third gamepad type available:
• Micro gamepad: This gamepad really is micro. It allows for a software D-pad
that uses a touch surface, two action buttons—one located under the D-pad
touch area—and a motion accelerometer. So far, the only remote in this format is
the Apple TV remote.
For a more exhaustive list of available game controllers, have a look at this
website: https://mficontrollers.afterpad.com
Getting Started
Before you begin working with game controllers, there's one big improvement you
should make to Delve.
Currently, the game is pretty quiet. It's time to add some music and sound effects!
Note: This section is optional, and review of material found earlier in this
book. If you'd like to dive right into game controllers, feel free to skip ahead to
the "Creating the controller manager" section, where we'll have a starter
project ready for you.
But if you'd like to continue building the entire game yourself, keep reading!
raywenderlich.com 649
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Make sure you have Delve open from where you left it off after the previous
chapter's challenge. If you didn't complete the previous chapter's challenge, you
can find a starter project in starter\Delve.
First, drag the Sounds folder from the resources of this chapter into your project.
Next, open GameState.swift and find GameSceneInitialState. Add this to the top
of didEnterWithPreviousState(_:):
SKTAudio.sharedInstance().playBackgroundMusic("delve_bg.mp3")
SKTAudio.sharedInstance().backgroundMusicPlayer?.volume = 0.4
Next open GameScene.swift and add some actions for the sounds:
//Sounds
let sndEnergy = SKAction.playSoundFileNamed("delve_energy",
waitForCompletion: false)
let sndHit = SKAction.playSoundFileNamed("delve_hit", waitForCompletion:
false)
let sndKill = SKAction.playSoundFileNamed("delve_kill",
waitForCompletion: false)
let sndShoot = SKAction.playSoundFileNamed("delve_shoot",
waitForCompletion: false)
let sndDamage = SKAction.playSoundFileNamed("delve_take_damage",
waitForCompletion: false)
let sndWin = SKAction.playSoundFileNamed("delve_win", waitForCompletion:
false)
Now you just need to play the sound effects at the appropriate times. Start by
finding didBeginContact(_:), and add replace the code that handles the collision
between "levelEnd" and "playerNode" with this:
SKTAudio.sharedInstance().pauseBackgroundMusic()
self.runAction(SKAction.sequence([sndWin,SKAction.waitForDuration(2),SKAc
tion.runBlock({ () -> Void in
SKTAudio.sharedInstance().resumeBackgroundMusic()
self.stateMachine.enterState(GameSceneWinState.self)
})]))
}
raywenderlich.com 650
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Now rather than entering the .GameSceneWinState directly, you enter a new
GameSceneLimboState that gives the player time to enjoy the game over sound effect
and register that they lost before tapping to restart.
Next, play the damage sound effect in the collision between "enemyNode" and
"playerNode", right after decreasing the player's health:
runAction(sndDamage)
Play the energy sound effect in the collision between "foodNode" and "playerNode",
right after increasing the player's health:
runAction(sndEnergy)
Inside damageEnemy(_:enemyNode:), play the kill sound effect in the case where
enemyEnt.enemyHealth is less than 0:
runAction(sndKill)
In the else case of the same if statement, play the hit sound effect:
runAction(sndHit)
runAction(sndShoot)
Your game controller functionality is unlikely to differ greatly from game to game.
The class you'll build will be designed to make it very easy to move between
games, with minimal effort to implement.
import SpriteKit
import GameController
The GameController framework has the same name for both iOS and tvOS.
raywenderlich.com 651
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Note: Game controllers don't work with OS X, despite being able to connect
via Bluetooth.
You need a way to let actions from the controller update your game scene. In this
implementation, you'll use delegation. Add this protocol under the imports:
Button presses and thumbstick movements have their own separate functions.
Each button on an MFI controller is pressurized, so when you press a button, it will
give you a velocity reading between 0.0 and 1.0.
It's important to note that every time the velocity changes, the function will be
called again. You can handle the on/off state of buttons by watching the pushedOn
Boolean value.
Now, add this code below the code you just added:
enum controllerType {
case micro
case standard
case extended
}
class SKTGameController {
To let the game controller class retain information about the controller between
changing scenes, you'll use the singleton design pattern.
//1
weak var delegate: SKTGameControllerDelegate?
//2
var gameControllerConnected: Bool = false
var gameController: GCController = GCController()
raywenderlich.com 652
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
//3
class var sharedInstance:SKTGameController {
return GameControllerSharedInstance
}
1. This delegate is your link to the current scene that adopts the protocol.
2. The first variable tracks if a game controller is currently connected; the second
is a link to the game controller itself; the third stores the type of controller
based on the enum you added; and the last is a pause toggle.
3. This is a variable that returns the current singleton instance of the class.
//1
init() {
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "controllerStateChanged:",
name: GCControllerDidConnectNotification,
object: nil)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "controllerStateChanged:",
name: GCControllerDidDisconnectNotification,
object: nil)
GCController.startWirelessControllerDiscoveryWithCompletionHandler() {
self.controllerStateChanged(NSNotification(name: "", object: nil))
}
self.controllerStateChanged(NSNotification(name: "", object: nil))
}
//2
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self,
name: GCControllerDidConnectNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self,
name: GCControllerDidDisconnectNotification, object: nil)
}
raywenderlich.com 653
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
if GCController.controllers().count > 0 {
gameControllerConnected = true
gameController = GCController.controllers()[0] as GCController
//More code to be added here
controllerAdded()
} else {
gameControllerConnected = false
controllerRemoved()
}
The code above uses two functions that you haven't implemented yet:
controllerAdded() and controllerRemoved().
Before you can call controllerAdded(), you need to specify what sort of controller is
connected.
Add the following code in place of the //More code to be added here comment:
#if os(iOS)
if (gameController.extendedGamepad != nil) {
gameControllerType = .extended
} else {
gameControllerType = .standard
}
#elseif os(tvOS)
if (gameController.extendedGamepad != nil) {
gameControllerType = .extended
} else if (gameController.microGamepad != nil) {
gameControllerType = .micro
} else {
gameControllerType = .standard
}
#endif
On the iOS platform, you check to see if the current game controller adopts the
extended gamepad method; if it doesn't, it must be a standard controller.
Supporting tvOS is similar, but you need to account for micro controllers, as well.
Now you've covered all possible official MFI controllers, with minimal effort!
raywenderlich.com 654
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
func controllerAdded() {
if (gameControllerConnected) {
//Add code here
}
}
func controllerRemoved() {
gameControllerConnected = false
gameControllerType = nil
}
There are a few ways you could pass the information from the controller to the
scene. You could:
• Read the value of each control manually: You would generally do this via the
game loop. It's one of the simpler solutions, but it also means a lot of additional
heavy lifting for something that may not have changed every time you try to
read it.
• Use a value change handler: Your value change handler would execute a
preset action every time the value of an input button or thumbstick changed.
This is the method you'll implement for this game, as it has the least impact on
game performance and works well using a protocol.
• Use a pressed change handler: This would work similarly to the value change
handler, but would call the preset action when a button is pressed or depressed,
instead of with every change in pressure. Using a pressed change handler has no
impact on sensitivity as any value greater than 0.0 is considered to be "pressed",
it doesn't have to reach 1.0.
Assigning Controls
For this game, you'll use directional movement and weapon firing, and you'll use
the A button to pass through menu screens.
Each controller has its own limitations, so you'll need to plan accordingly:
• Extended gamepad: This is the most versatile controller, but note that most
extended controllers lack motion support.
• Standard gamepad: Losing two thumbsticks isn't a big deal for a game that
only involves the left thumbstick, because you can simply move that functionality
to the D-pad. In Delve, you'd normally want two thumbsticks, so you'll need to
decide on an alternative.
• Micro gamepad: This is where you really need to get creative. Luckily, you only
need one button; however, the single D-pad is still a problem. The micro
controllers all have motion, but it doesn't allow for the accuracy required for
attacking.
Your biggest issue is needing two controls that support 360-degree movement (one
raywenderlich.com 655
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
for movement, and one for firing). You could handle this in a few ways:
• Substitution: The player will only want to attack when there's an enemy nearby.
You could create a basic AI to find the closest enemy, and if it's within a certain
range, auto-attack in its direction.
• Stacking: You could assume that the hero will be attacking in the same direction
as he's walking, and combine the controls. This doesn't always work as you
intend, though, as more likely than not, your hero will be running away from an
enemy onslaught.
• Toggling: You could set the D-pad to move the player, but use it for attacking if
the user holds down another button.
Each option has its pros and cons, and you may want to use a different option for
each controller type, or let players choose what they prefer.
Standard gamepad
The standard gamepad will use the cooperation approach.
Add the following to controllerAdded() in place of the //Add code here comment:
if gameControllerType! == .standard,
let pad:GCGamepad = gameController.gamepad {
If the current controller is of the standard type, you access the gamepad property
and assign it to pad.
The change handler provides three variables to the action: a button property; a
value between 0.0 and 1.0 that represents how much pressure the user is placing
on the button; and pressed, a Boolean value representing whether or not the button
is in a state of being pressed.
raywenderlich.com 656
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Directly below this code, within the same if statement, add the following to cover
the D-pad:
This covers all of the controls found on the standard controller. Now you need to tell
the game scene what to do with this information.
Notice that you've added the SKTGameControllerDelegate. Now, you need to include
the two functions from the protocol.
//Game Controllers
SKTGameController.sharedInstance.delegate = self
raywenderlich.com 657
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
func buttonEvent(event:String,velocity:Float,pushedOn:Bool) {
func stickEvent(event:String,point:CGPoint) {
The tapState property introduced in the previous chapter let you have different
interactions for different states when the player is touching the screen. You can use
this same property to define how to handle button interactions.
switch tapState {
case .startGame:
if event == "buttonA" {
stateMachine.enterState(GameSceneActiveState.self)
}
break
case .dismissPause:
if event == "buttonA" {
stateMachine.enterState(GameSceneActiveState.self)
}
break
case .nextLevel:
if event == "buttonA" {
if let scene = GameScene(fileNamed:"GameScene") {
scene.scaleMode = (self.scene?.scaleMode)!
let transition = SKTransition.fadeWithDuration(0.6)
view!.presentScene(scene, transition: transition)
}
}
break
case .attack:
break
default:
break
}
This code is almost exactly like the code in touchesBegan(_:withEvent:). The big
change is that you've wrapped each event in an if statement that tests if the player
is pressing button A.
If you own a standard format controller, you can build and run the game to make
sure the A button dismisses the start-up screen and any game over scenes.
raywenderlich.com 658
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Movement controls
The cooperation approach is going to work well here, but you're going to need the
game to make decisions based on which controller is connected.
case .attack:
if event == "dpad_up" {
movement.y = CGFloat(velocity)
}
if event == "dpad_down" {
movement.y = CGFloat(velocity) * -1
}
if event == "dpad_left" {
movement.x = CGFloat(velocity) * -1
}
if event == "dpad_right" {
movement.x = CGFloat(velocity)
}
break
The velocity measure isn't just for buttons; it can also be used on the d-pad. Using
the velocity will give the player refined movement control, making the game feel
smoother.
raywenderlich.com 659
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
//Motion
#if os(iOS)
if (self.motionManager.accelerometerData != nil) {
//1
var motion = CGPointZero
if self.motionManager.accelerometerData!.acceleration.x > 0.02 ||
self.motionManager.accelerometerData!.acceleration.x < -0.02 {
motion.y =
CGFloat(self.motionManager.accelerometerData!.acceleration.x)
}
if self.motionManager.accelerometerData!.acceleration.y > 0.02 ||
self.motionManager.accelerometerData!.acceleration.y < -0.02 {
motion.x =
CGFloat((self.motionManager.accelerometerData!.acceleration.y) *
-1)
}
//2
if (SKTGameController.sharedInstance.gameControllerConnected == true) {
//3
if (SKTGameController.sharedInstance.gameControllerType ==
.standard) {
self.playerAttack = motion
}
} else {
self.movement = motion
}
}
#endif
1. Instead of setting the motion directly to the movement property, you assign a
motion variable.
2. If a controller is connected, you configure for the controller. If you don't find a
controller, you assign the motion to the movement property.
3. If the controller type is standard, you use the motion to aim the weapon. If the
controller type is extended, then you don't need to use motion—instead, the
extra thumbstick will do nicely.
There's an added advantage to checking the controller status every time the game
loop is called: You can easily and seamlessly switch between available controller
types.
Build and run, and you can now move the player via the D-pad, use the motion
from the device to define the attack direction, and dismiss menu screens using the
A button.
Extended gamepad
The extended controller type has plenty of controls available, so no tricks are
required to get great results.
Return to SKTGameController.swift and add this code below the if statement for
the standard controller type in controllerAdded():
raywenderlich.com 660
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
if gameControllerType! == .extended,
let extendedPad:GCExtendedGamepad = gameController.extendedGamepad {
Same as with the standard controller, you first check the type, and then access the
relevant property from the game controller based on its type.
//1
extendedPad.buttonA.valueChangedHandler = { button, value, pressed in
if self.delegate != nil {
self.delegate!.buttonEvent("buttonA", velocity: value, pushedOn:
pressed)
}
}
//2
extendedPad.leftThumbstick.valueChangedHandler = { dpad, xValue, yValue
in
if self.delegate != nil {
self.delegate!.stickEvent("leftstick", point:CGPoint(x:
CGFloat(xValue),y: CGFloat(yValue)))
}
}
extendedPad.rightThumbstick.valueChangedHandler = { dpad, xValue, yValue
in
if self.delegate != nil {
self.delegate!.stickEvent("rightstick", point:CGPoint(x:
CGFloat(xValue),y: CGFloat(yValue)))
}
}
1. You implement the A button in the same way you did the standard controller.
2. The thumbsticks are a little different. They pass in x- and y-values that you can
use to create a CGPoint to represent the character's movement. Each value
ranges from -1.0 to 1.0, with 0 being stationary.
switch tapState {
case .attack:
if event == "leftstick" {
movement = point
}
if event == "rightstick" {
playerAttack = point
}
break
default:
break
}
That's it! Connect an extended controller if you have one available, and build and
raywenderlich.com 661
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
You can move the hero with the left stick, attack with the right stick in whichever
direction you move it, and dismiss menu screens with the A button.
Next, you've going to implement the micro controller. But because the micro
controller only works on the Apple TV, you'll need to support that first.
In the template selector, choose tvOS/Application from the left menu and then
Game.
Click Next.
In the options, give the target a Product Name of DelveTV, make sure the
Language is set to Swift and the Game Technology to SpriteKit. Click Finish.
At this point, build and run the DelveTV target on your Apple TV or tvOS simulator.
You'll see the normal sample project, complete with spinning spaceships if you tap
or click the touchpad on the remote.
But you're headed underground, not into space! Locate the DelveTV folder,
rename it TV Controller and delete the following files:
raywenderlich.com 662
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Now, locate the resource files at starter\Resources and drag the folder named
Assets.xcassets into this same group.
Make sure when you select the target, you only select DelveTV:
Click Finish.
This assets file contains Delve's app icons, top shelf images and launch images.
While you're in the TV Controller target folder, access Info.plist and change the
Bundle name to Delve to override the target name.
Each file within each group of the Delve group must now have its target
relationships updated to both available targets.
raywenderlich.com 663
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Work through each file and select both targets from the Utilities panel:
When you're done, build and run the game to make sure it doesn't crash - that will
verify that you've set all of the proper targets.
After the launch screen, you'll get a blank screen for no apparent reason. Before
you start flipping tables, have a look at GameViewController.swift inside the TV
Controller group. Notice how the scene is created a little differently? The default
GameViewController.swift that is generated with the new target needs a small
change.
Note: Want to know how to find unknown errors like this? It was clear the
scene hadn't been loaded, as the background was the default gray.
The scene declaration is an if-let, which means that if the line fails for
whatever reason, it won't be run and won't produce an error. That's how you
know this is the point of failure.
Now the scene will load, but you have a few other problems with your GUI that
need attention:
Go to your GUI.xcassets folder, select one of the images from the middle-left
raywenderlich.com 664
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Check the Apple TV box in addition to the Universal option, because the Universal
option doesn't include tvOS.
Build and run again, and everything will look perfect! You aren't quite there yet,
though—you still need to implement game controller support.
raywenderlich.com 665
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
import GameController
In viewDidLoad(), add this right before you instantiate the game scene:
controllerUserInteractionEnabled = false
This disables the standard functionality of the controller—so, for example, a game
controller's B button won't exit the game.
If the controller at the first index has the name "Remote", and there is more than
one controller connected, then you change the current controller to the controller at
index 1.
Connecting a controller
The extended controller you set up earlier will also work on tvOS, now that you've
done this preliminary setup.
raywenderlich.com 666
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
Build and run. The remote still won't work as intended, as you haven't set up the
micro controller. However, a connected game controller will work with the same
controls as in the iOS version.
Micro gamepad
Just like the standard and extended gamepads, you need to set up value handlers
for the micro gamepad controls.
It should be noted that the Apple TV Remote functions a little differently when
enabled as a game controller. The play/pause button becomes known as the X
button, the Menu button becomes the pause button and the screen button becomes
the home button.
You'll use the toggling approach to handle the control problem presented by the
micro controller. When the player holds the X button down, the D-pad will change
the direction of attack.
Then add this code to controllerStateChanged(_:), right after the two if os(...)
blocks:
#if os(tvOS)
if gameControllerType! == .micro,
let microPad:GCMicroGamepad = gameController.microGamepad {
raywenderlich.com 667
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
//1
microPad.buttonA.valueChangedHandler = { button, value, pressed in
if self.delegate != nil {
self.delegate!.buttonEvent("buttonA", velocity: value, pushedOn:
pressed)
}
}
//2
microPad.allowsRotation = true
//3
microPad.reportsAbsoluteDpadValues = true
//4
microPad.dpad.valueChangedHandler = { dpad, xValue, yValue in
if self.delegate != nil && !microPad.buttonX.pressed {
self.delegate!.stickEvent("leftstick", point:CGPoint(x:
CGFloat(xValue),y: CGFloat(yValue)))
}
if self.delegate != nil && microPad.buttonX.pressed {
// 5
let curShootPoint = CGPoint(x: CGFloat(xValue),y:
CGFloat(yValue))
self.lastShootPoint = self.lastShootPoint * 0.9 + curShootPoint *
0.1
self.delegate!.stickEvent("rightstick",
point:self.lastShootPoint)
self.delegate!.stickEvent("leftstick", point:CGPoint(x: 0.0,y:
0.0))
}
}
}
#endif
Note: The A button on the Apple TV remote is the same as the click on the D-
pad touch area.
2. When you hold the remote in the portrait position, the D-pad functions as
expected; if you turn the remote to landscape, and expect it to change the
orientation of the D-pad controls, then this value needs to be true.
3. Absolute values consider the center of the touch area to be the center of the D-
pad, while non-absolute values consider the first point of touch to be the center
of the D-pad, and adjust accordingly with every new touch.
4. The micro controller provides different events, depending on the state of button
X.
5. You don't want to use the direct input of the right stick because the user may
start moving their finger on the trackpad intending to move fractions of a
raywenderlich.com 668
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
second before they release the play/pause button. If you used direct input, this
would cause them to shoot in the direction they intended to move, resulting in a
frustrating experience. Instead, here you blend the shoot input based on
previous shoot input to more accurately reflect the player's intention.
There's one last thing. Remember that on tvOS, your touch handlers are still called,
and you don't want to do anything in the attack state on tvOS. So in
touchesBegan(_:withEvent:), wrap the content inside the case .attack: state with
an ifdef:
case .attack:
#if (iOS)
for touch in touches {
let location = touch.locationInNode(self)
if let player = worldLayer.childNodeWithName("playerNode") {
playerAttack = location - player.position
}
}
#endif
break
Go to your tvOS settings menu and disconnect your controller if you did earlier, so
only the remote is connected. Build and run. Move around with the D-pad and hold
the play/pause button to stop and fire in the direction you're pressing on the D-pad.
Make sure you hold your controller in a landscape position.
Right now, the controls feel a little fast on both of the controllers and the remote.
The accelerometer controls felt OK, so you need to make sure you don't affect the
speed of the accelerometer controls when you fix the other controllers.
Note: Remember that the tvOS remote has an accelerometer, as well! While
you won't work with it in this chapter, with the right game, the remote's
accelerometer could enhance your gameplay and make up for the limited
controls on the Apple TV.
//1
func stickEvent(event:String,var point:CGPoint) {
switch tapState {
case .attack:
if event == "leftstick" {
//2
raywenderlich.com 669
2D iOS & tvOS Games by Tutorials Chapter 24: Game Controllers
1. You update the function declaration with var to make the point value mutable.
2. You normalize the movement to remove any excess acceleration, and then halve
it again to bring the max speed in line with the other controls.
That's it! Build and run your game. You are now successfully supporting three
control formats on iOS: device, standard gamepad and extended gamepad; and two
control formats on the Apple TV: the remote and the extended gamepad.
At this point, you've used GameplayKit's fantastic features to create tile map games
that you can easily scale and improve. You've covered powerful methods of
procedural generation that you can use with more complex algorithms to create
truly original levels. Your toolkit to make impressive procedural tile map games is
complete!
Challenges
There's just one short challenge for this chapter, to give you a little more
experience with controllers.
As always, if you get stuck you can find the solution in the resources for this
chapter, but give it your best shot first!
Using what you've learned about the game state and change handlers, implement a
pause button for Delve for all controller layouts.
Once you've implemented change handlers, you'll need to call the scene delegate to
change the scene's state.
raywenderlich.com 670
Section VI: Bonus Chapters
To thank you for purchasing this book, we’ve included some bonus chapters for you!
In these bonus chapters, you’ll learn about some APIs other than Sprite Kit that are
good to know when making games for iOS. In particular, you will learn how add
Game Center leaderboards and achievements into your game, use the new iOS 9
ReplayKit API, and add iAds into your game.
In the process, you will integrate these APIs into a top-down racing game called
Circuit Racer, where you take the role of an elite racecar driver out to set a world
record. It would be no problem if it weren’t for the debris on the track!
raywenderlich.com 671
25 Chapter 25: Game Center
Achievements
By Ali Hafizji
So far, all of the minigames you’ve created for this book have been single-player.
Personally, I prefer a little friendly competition!
Luckily, this is easy to do in iOS thanks to Apple’s Game Center app and APIs.
Over the next two chapters, you'll learn all about Game Center and how to
integrate it into a Sprite Kit game. Specifically:
• In this chapter, you’ll learn about Game Center and take a quick tour of its
features. Along the way, you’ll learn how to authenticate the local player, enable
achievements and add support for them to your game.
• In the next chapter, you’ll continue your exploration of Game Center by adding
support for leaderboards.
Getting started
In the next two chapers, you will integrate Game Center into a car racing came
called Circuit Racer.
CircuitRacer is an exciting game, but it's also quite simple. The player has to
complete laps around a track within a fixed period of time. For a little extra fun, the
player has the option of choosing a vehicle type and a track with a difficulty level of
either easy, medium or hard.
raywenderlich.com 672
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
By this point in the book, you've already learned enough about Sprite Kit to make
this game on your own, so to save you some time we've built a starter project for
you with the gameplay implemented. This way, you can stay focused on the main
theme of this chapter - Game Center.
This starter project is a great review of what you've learned so far in this book.
Before you begin, you should open the project in the starter folder and take a tour
of the existing code.
Now select the CircuitRacer target, build and run the game and get behind the
wheel for a few laps. You can race to beat the clock, but that's about it. It's your
job to turn on Game Center, integrate it into the app and add specific achievements
that the player can unlock, like completing the hardest level.
If you're ready to learn how to add a new level of interactivity to your game, then
buckle up, pull on your racing gloves and rev up your coding engine! It's time to
begin.
raywenderlich.com 673
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
On the developer side, Game Center provides a set of APIs you can integrate into
your game to support achievements, leaderboards, challenges, turn-based gaming,
real-time multiplayer gaming and more.
Why should you add Game Center support to your game? Well, not only can it
significantly up your game’s fun quotient—it can also increase your downloads. If
your game supports Game Center, you'll benefit from a number of extra ways
players can discover your app:
• Listed on profile. Each player’s game list, leaderboards and achievements will
include your app. When someone views a friend’s profile, your game might be
there!
raywenderlich.com 674
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
• Friend challenges and requests. Players can challenge their friends to beat
their high scores or—if your game has multiplayer support—invite them to play.
In either case, Game Center will present the new player with the option to
download or buy your app, providing a viral growth mechanism.
• More ratings. Game Center makes it easier for players to rate your game, or
like it on Facebook, by presenting those options on the selection screen. Anything
raywenderlich.com 675
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
that encourages ratings is good for you, because potential players are more likely
to download games with more and higher ratings. And your game will get high
ratings, won't it?
Now that you've seen the benefits of supporting Game Center, read on to explore
the API—specifically, its two most popular features: achievements and
leaderboards.
Note: If you’ve used Game Center before, this process may be routine for you
by now. In that case, feel free to complete these steps yourself and skip to the
"Authenticating local players" section.
raywenderlich.com 676
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Next, select the Capabilities tab. In the Game Center section, switch the selector
to the On position. This feature, introduced in Xcode 5, makes it easy to enable
Game Center in your apps.
That’s it—with the click of a button, you’re set up. Thanks, Xcode!
Note: If you receive an error with a Fix Issue button, click the button to fix
the issue. Usually, this is related to adding entitlements to your App ID or that
you need to come up with a unique bundle identifier.
Since, Game Center is also present on tvOS, perform the same operation for the
CircuitRacer-tvOS target. However, for this to work correctly you should have at-
least one Apple Tv registered in the Member Center. If you don't then just stick with
the iOS target for now.
raywenderlich.com 677
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Log in to iTunes Connect. Then, go to My Apps and click the + button in the top-
left corner. Choose New App to add a new app.
On the first screen, select both iOS and tvOS platforms and enter a unique value in
the App Name field; this is the name that appears in the App Store, which is why it
must be unique. I used “CircuitRacer-Swift”; you'll need to put on your racing-
helmet thinking cap and come up with your own app name.
Note: Because this is only a test app, you can use something like [Your
Name] Circuit Racer, using your actual name in place of the words “Your
Name”.
Next, enter 100 as the SKU Number. This can be any number or word, so if you
want, you can set it to something else. Finally, select the Bundle ID you created in
the previous step.
Note: If the bundle ID is not present in the drop-down, first verify if it exists
in the Member Center. It not then create it manually. Xcode is sometimes not
able to create and register the bundle Id with the Member Center.
raywenderlich.com 678
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
On the next screen, enter all of the required details. Since you just need to get
through these steps for the purposes of this chapter, fill in only the required values
and feel free to be as brief as possible.
Hooray! You’ve registered your app with iTunes Connect and there are only a few
more steps remaining to activate Game Center.
1. Destruction Hero: A player will earn this achievement whenever she hits the
crates more than 20 times during a single race. You’ll hide the achievement
initially, meaning the player won’t see this achievement in Game Center until
she’s earned it. A player will be able to achieve this multiple times.
2. Amateur Racer: A player will earn this when she completes the easy level.
3. Intermediate Racer: A player will earn this when she completes the medium
level.
4. Professional Racer: A player will earn this when she completes the hard level.
It's time to create your game's achievements. In iTunes Connect, select the
Features button and select Game Center from the left panel.
raywenderlich.com 679
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Click the + button in the Achievements section. iTunes Connect will present you
with a screen on which you can enter the details about the achievement.
• Enter [Your app Bundle ID].destructionhero for the Achievement ID. This
is a string that uniquely identifies each achievement. It’s generally good practice
to use the bundle ID as a prefix while setting up the achievement ID, as it
ensures the ID will be globally unique.
• Enter 100 for the Point Value. This refers to the number of points the
achievement is worth. Each achievement can have a maximum of 100 points,
and all achievements combined can be worth a maximum of 1000 points.
• Select Yes for Hidden. This property keeps the achievement hidden until the
player achieves it for the first time.
• Select Yes for Achievable More Than Once. As the name suggests, this
raywenderlich.com 680
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
property allows the player to earn this achievement more than once. Moreover,
when you set this property to Yes, players will be able to receive achievement
challenges from their friends even for achievements they’ve already earned.
Next, select Add Language in the Achievement Localization section and enter
the following details:
• Enter Destruction Hero for the Title. This is the achievement’s title as it will
appear in Game Center.
• Enter Bang into an obstacle more than 20 times for the Pre-earned
Description. This is the description you show before the player has earned the
achievement.
• Enter Banged into an obstacle more than 20 times for the Earned
Description. This is the description you show after the player has earned the
achievement.
raywenderlich.com 681
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Click Save to save the localization. Then, click Save on the achievement screen,
and this will take you back to the main Game Center screen. There, you’ll see your
new achievement in the Achievements section—Destruction Hero!
Now that you know what you’re doing, create the other three achievements yourself
by following the same process.
Here’s a table showing the values you'll use, but be sure to replace with your own
bundle ID:
For each achievement, set Point Value to 100, Hidden to No and Achievable
raywenderlich.com 682
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Also, add English localizations for all three achievements. You may enter whatever
you like for the title and descriptions. The Resources folder contains an
appropriate icon for each of the three achievements.
Once you’ve added all the achievements, click Save in the top right.
Sweet! Now that you've set up everything, it’s time to crack your knuckles and
write some code.
In Xcode, right-click on the Shared group and select New Group. Name the group
GameKit.
Next, right-click on the new group and select New File…. Choose the iOS\Source
\Cocoa Touch Class template and click Next. Name the class GameKitHelper
and make it a subclass of NSObject. Verify the Language is set to Swift and click
Next. Make sure you select both the CircuitRacer and CircuitRacer-tvOS target.
Select Create and save the file.
This GameKitHelper class is where you’re going to put all of your Game Center code.
As an added benefit, you’ll be able to use this same class in your own games
without having to rewrite anything. Now, that’s pretty awesome!
Open GameKitHelper.swift and replace the contents of the file with the following:
import UIKit
import Foundation
import GameKit
GameKit is the name of the framework that includes all the classes you need to
access Game Center, so you import it along with Foundation and UIKit.
The GameKitHelper class will behave as a singleton, meaning it's a shared instance
of GameKitHelper that you can access from anywhere in the app. The
sharedInstance static variable returns this instance.
raywenderlich.com 683
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Authentication callbacks
Since Game Center authentication happens asynchronously, the callback can be
triggered at any point, even when the player is racing around your tracks! To
handle this, you’re going to use NSNotificationCenter, so the first thing you need
to do is define a name for this notification.
Add this declaration to the top of GameKitHelper.swift, just after the import
statements:
let PresentAuthenticationViewController =
"PresentAuthenticationViewController"
Now, add the authentication code to the class by adding the following method:
func authenticateLocalPlayer() {
//1
let localPlayer = GKLocalPlayer()
localPlayer.authenticateHandler = {(viewController, error) in
if viewController != nil {
//2
self.authenticationViewController = viewController
NSNotificationCenter.defaultCenter().postNotificationName(
PresentAuthenticationViewController, object: self)
} else if error == nil {
//3
self.gameCenterEnabled = true
}
}
}
This method authenticates the player with Game Center. Here's a step-by-step
explanation:
1. First, you set the authenticateHandler of the GKLocalPlayer object. The Game
Kit framework may call this handler multiple times.
2. If the player hasn't logged into Game Center using either the Game Center app
or while playing another game, the Game Kit framework will pass a view
controller to the authenticateHandler closure. It’s your duty, as the game’s
developer, to present this view controller to the user, when appropriate. Ideally,
you should do this as soon as possible. You'll store this view controller in the
authenticationViewController variable. The code also raises your notification.
3. If the player has already logged into Game Center, you enable all Game Center
raywenderlich.com 684
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
The code is now in place to authenticate with Game Center—all you need to do is
call it.
Now you need to set the class of your game’s navigation controller to your new
class. Open CircuitRacer\Main.storyboard and select the navigation controller.
In the Identity Inspector, inside Custom Class, set the Class property to
CircuitRacerNavigationController.
Perform the same step for the tvOS target by changing the Class property for the
raywenderlich.com 685
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
NSNotificationCenter.defaultCenter().addObserver(self,
selector: Selector("showAuthenticationViewController"),
name: PresentAuthenticationViewController, object: nil)
GameKitHelper.sharedInstance.authenticateLocalPlayer()
}
Note: As a general rule, you should always authenticate the local player as
soon as the game starts.
func showAuthenticationViewController() {
if let authenticationViewController =
gameKitHelper.authenticationViewController {
topViewController?.presentViewController(
authenticationViewController,
animated: true, completion: nil)
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
With these methods, you simply present the authentication view controller over the
top view in the navigation stack and deregister for notifications when the object is
deallocated.
Build and run. If everything goes well, when the game launches, the system will
present you with the Game Center authentication view:
raywenderlich.com 686
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
As you may already know, you only need to authenticate with Game Center once.
The next time you launch the game, Game Center will present a banner similar to
the one shown below:
Note: At the time of writing this chapter there is no way to create a Game
Center account on tvOS. Hence, to authenticate the user you will first have to
sign in with valid credentials using the Settings app. Open the Settings app
and navigate to Accounts/Game Center to login.
Adding achievements
In the previous section, you added four achievements to the game in iTunes
Connect, so now you can go straight to writing code within Circuit Racer to award
the achievements at the appropriate times.
raywenderlich.com 687
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
GKAchievement.reportAchievements(achievements,
withCompletionHandler: errorHandler)
}
The good news is, once you have this array, there’s only a single line of code
required when you want to send it: simply call
reportAchievements(_:errorHandler:) from GKAchievement. This method
automatically handles network errors for you and resends the data to Game Center
until it arrives successfully.
It's time to integrate this into Circuit Racer for its specific achievements.
Right-click on the GameKit group, select New File…, choose the Swift File
template and click Next. Name the file AchievementsHelper.swift and click
Create. Make sure it's a part of both targets.
import Foundation
import GameKit
class AchievementsHelper {
raywenderlich.com 688
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Note: Change the achievement IDs to exactly match the ones you created in
iTunes Connect. Remember—capitalization matters!
//1
let percent =
Double((noOfCollisions/AchievementsHelper.MaxCollisions)
* 100)
//2
let collisionAchievement = GKAchievement(
identifier: AchievementsHelper.DestructionHeroAchievementId)
//3
collisionAchievement.percentComplete = percent
collisionAchievement.showsCompletionBanner = true
return collisionAchievement
This is a static helper method that makes it easy for you to report progress for the
Destruction Hero achievement. Remember, you're going to grant this achievement
once a user has collided with 20 crates in a single race.
1. You can report achievement progress even if it’s only partially complete. This
calculates the percent completed based on the number of boxes the player has
hit so far, compared to the maximum boxes set in the MaxCollisions static
variable (20).
3. You set the percentComplete property of the GKAchievement object to the one
calculated in the first step.
Now that you have a helper method to create the Destruction Hero achievement,
it's time to do the same for the achievements corresponding to each difficulty level.
Remember, you’ll unlock these achievements only when the player has completed
the corresponding levels.
raywenderlich.com 689
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
if levelType == .Medium {
achievementId = AchievementsHelper.IntermediateAchievementId
} else if levelType == .Hard {
achievementId = AchievementsHelper.ProfessionalHeroAchievementId
}
levelAchievement.percentComplete = 100
levelAchievement.showsCompletionBanner = true
return levelAchievement
}
Now that you have these helper methods in place, you need to make use of them in
the game.
import GameKit
Next, add a variable to the class to track the number of collisions between the car
and the boxes:
You also need to define constants for the physics categories: one for the car and
one for the boxes. You’ll use these physics categories to determine whether the
objects colliding are the car and a box. You aren’t interested in boxes colliding with
other boxes!
Note: You dealt with collision categories in previous chapters of this book, so
they probably look familiar to you by now. That’s a good sign!
raywenderlich.com 690
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
childNodeWithName("car")?.physicsBody?.contactTestBitMask =
GameScene.BoxCategoryMask
Next, you need to tell the scene about physics collisions. Create an extension that
implements SKPhysicsContactDelegate, and then, within that extension, implement
didBeginContact(_:) to test for collisions between the car and a box:
noOfCollisions += 1
runAction(boxSoundAction)
}
}
}
didBeginContact(_:) is called every time two physics bodies in the game collide. If
the collision is between the car and a box, you increment the collision counter you
declared earlier by 1 and, just for fun, play a collision sound.
Finally, add this line at the end of didMoveToView(_:) to set the GameScene object as
the physics contact delegate:
physicsWorld.contactDelegate = self
OK! Now all that’s left to do is report achievements. Add the following new method:
achievements.append(AchievementsHelper.collisionAchievement(
noOfCollisions))
if hasWon {
achievements.append(AchievementsHelper.achivementForLevel(
levelType))
}
GameKitHelper.sharedInstance.reportAchievements(achievements)
}
In this method, you pass in a Bool that describes if the player won or lost the
game. If the player won, you create an achievement for that level and report it to
Game Center. Either way, you report the collision achievement.
Lastly, you need to call that method from within your game. Switch to
GameActiveState.swift and in updateWithDeltaTime(_:), modify the code that
raywenderlich.com 691
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
It’s time to run some tests—on your mark, get set, go!
Build and run the program, and try to win on each of the tracks. I know it might be
difficult to finish all three levels, but if you want those achievements, you better
complete those laps before the time’s up! Every time you earn an achievement,
Game Center will show a banner like this:
At this point, their only option is to check their achievements in the built-in Game
Center app. But of course, this requires players to leave your app, which is never a
good thing. It would be much better if there were a way players could see their
progress right from within your app—and luckily, there is!
The Game Kit framework provides a class called GKGameCenterViewController that
allows your players to view their achievements, leaderboards and challenges from
raywenderlich.com 692
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
within your game, and you're going to add this to Circuit Racer.
To present the Game Center view controller, your GameKitHelper class will need to
conform to the GKGameCenterControllerDelegate protocol.
gameCenterViewController.dismissViewControllerAnimated(true,
completion: nil)
}
}
To show the Game Center view controller, add the following method to the class:
//1
let gameCenterViewController = GKGameCenterViewController()
//2
gameCenterViewController.gameCenterDelegate = self
//3
viewController.presentViewController(gameCenterViewController,
animated: true, completion: nil)
}
raywenderlich.com 693
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Drag a button into the view controller. Set its Type to Custom, delete its title and
set the image as btn_gamecenter.
To make the button appear correctly, set the width to 300 and the height to 54,
set the x-position to 149 and the y-position to 498, and then apply the following
constraints:
raywenderlich.com 694
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Next, select the Play button and look at the constraints in the Size Inspector.
Double-click on the Align Center Y constraint. Verify Button.Center.Y is on top
and set the Constant to 0 and the Multiplier to 1.29.
Similarly, select the Game Center button and look at the Size Inspector to see the
button’s constraints. Double-click on the Align Center Y constraint. Verify that
Button.Center.Y is the first item and set the Constant to 0 and the Multiplier to
1.75.
Your buttons will now appear centered and in a suitable position on all devices.
Finally, you need to add an action for the Game Center button. Select the
HomeScreenViewController and make sure the assistant editor is open with
HomeScreenViewController.swift showing. Control-drag from the Game Center
button to HomeScreenViewController.swift and enter gameCenter for the
name. For Connection, select Action; for Type, select UIButton; then, click
Connect.
raywenderlich.com 695
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Before you proceed perform the same steps for the tvOS target by modifying
CircuitRacer-tvOS/Main.storyboard and adding the Game Center button. Make
sure you set the focused state of the button to btn_gamecenter_focussed and
set the Custom Class as Button. Also, connect the Touch Up Inside action to the
game(_:) method defined in the HomeScreenViewController class.
Build and run the program and wait for Game Center to authenticate the player.
Once that happens, press the Game Center button. This will open the
GKGameCenterViewController:
That’s it! You’ve added Game Center, achievements and a Game Center view
controller to Circuit Racer! Remember every-time a player earns an achievement it
will be visible in his/her profile on the Game Center app. More achievement more
virality YAY!
In the following chapter, you’ll learn all about the latest Game Center leaderboard
features—but first, a quick challenge for you.
raywenderlich.com 696
2D iOS & tvOS Games by Tutorials Chapter 25: Game Center Achievements
Challenges
This challenge will ensure you’ve understood the majority of this chapter. If you get
stuck, you can find the solutions in the resources for this chapter, but give it your
best shot first!
• Create a variable to track the number of times the local player has played the
game.
• Every time the game completes, check to see if that variable has crossed 10.
• You can use an image already in use by the other achievements to create this
achievement.
raywenderlich.com 697
26 Chapter 26: Game Center
Leaderboards
By Ali Hafizji
In the previous chapter, you learned the steps required to set up your game to use
Game Center, authenticate the local player and enhance the player’s experience
with achievements. You also implemented Game Center’s built-in user interface.
It's time to try this out by adding some leaderboards to Circuit Racer!
Note: This chapter begins where the previous chapter’s challenge left off. If
you were unable to complete the challenge, don’t worry; simply open
CircuitRacer from the starter folder of this chapter’s resources to begin in the
right place. Don't forget to update the app's bundle identifier to your own, and
update the achivement IDs in AchievementsHelper.swift appropriately.
However, if you skipped the last chapter, to follow along with this chapter,
you'll need to set up Circuit Racer in iTunes Connect and register a number of
achievements. For instructions on how to perform these steps, see the
previous chapter.
raywenderlich.com 698
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Supporting leaderboards
There are five steps to add leaderboards to your game. Here’s a quick overview.
1. Authenticate the local player. Remember, to use any Game Center feature,
you first need to authenticate the local player.
2. Create a strategy for using leaderboards in the game. Decide how many
leaderboards the game is going to have and what scores will drive each
leaderboard.
4. Add code to report scores to Game Center. In the same way you added
code to send an array of GKAchievement objects to Game Center, you need to
add code to send an array of GKScore objects.
5. Add code to display the leaderboards to the player. As you did for
achievements, you'll use the GKGameCenterViewController. Optionally, you can
also retrieve the score data and display the leaderboard in a custom user
interface.
The rest of this section will take you through these steps one by one.
Since Circuit Racer is a race against time, there is a single scoring criterion: the
amount of time it took for the local player to complete each track. The players who
finished the track in the shortest amount of time will be at the top of the
leaderboard.
raywenderlich.com 699
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Before moving on to the next step, you also need to determine the number of
leaderboards your game will support. It’s not mandatory to have multiple
leaderboards, but it’s usually a good idea, as it gives the player a detailed view of
the position she holds among all the game’s players.
In Circuit Racer, the player chooses a car and then selects a difficulty level. You’re
going to create a leaderboard for each of the possible car/difficulty level
combinations, so that players have the chance to climb the leaderboard with their
favorite cars and tracks.
To account for all possible car/difficulty combinations, you’re going to create a total
of nine leaderboards. For example, the first leaderboard will be
Car_1_level_easy_fastest_time. Your other leaderboards will similarly cover all
the other car and difficulty level combinations. Later in the chapter, you'll use these
nine leaderboards to create leaderboard sets. Keep reading to learn more.
raywenderlich.com 700
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Note: The other option, Combined Leaderboard, allows you to create a new
“virtual leaderboard” that combines the results of several leaderboards.
For example, you could create a leaderboard for Circuit Racer named “Any car
level easy fastest time” that combines the results of “Car 1 level easy fastest
time,” “Car 2 level easy fastest time” and “Car 3 level easy fastest time”.
You would still report your players' scores to each individual leaderboard—this
just provides an easy way to aggregate the results for players across several
leaderboards that share the same score format type and sort order.
If you’re wondering what each of those fields are on the next page, don’t fret! Enter
the following values:
• Car 1 level easy fastest time for the Leaderboard Reference Name. This is
a string that represents the internal name for the leaderboard. You can use this
string to search for leaderboards within iTunes Connect.
• Elapsed Time – To the Second as the Score Format Type. This field specifies
the format of the scores you’ll send for this particular leaderboard, and it tells
Game Center how to interpret those scores. Your game’s scoring criterion is
based on time, so you choose the appropriate format.
• Best Score as the Score Submission Type. This field specifies which score the
leaderboard will display first: the best or the most recent one.
• Low to High as the Sort Order. This field dictates how the scores are arranged
in the leaderboard. In the case of Circuit Racer, the lowest time refers to the
highest score, hence your selection here.
• 1 to 60 for the Score Range. Even though this field is optional, I recommend
raywenderlich.com 701
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
you add a value here. You'll learn more about this field in the sections to come,
so stay tuned. For now, enter 1 and 60, which are the lowest and highest values
from the game’s LevelDetails.plist file.
The next step is to add a language. Select the Add Language button in the
Leaderboard Localization section. Keep in mind that these are the settings that
affect what your users will see, so choose wisely.
• Yellow Car Easy Level Fastest Time for the Name. This is the name of the
leaderboard that players will see.
• Elapsed Time (hours, minutes, seconds, ex. 5:01:18) for the Score
Format. This is the format Game Center will use to display the scores in the
leaderboard.
• seconds for the Score Format Suffix. This is the suffix that Game Center adds
to the score submitted by the device. Circuit Racer measures a player’s score in
seconds.
• Image You can optionally upload an image for each language. In this chapter,
you won't be adding any images to leaderboards, though I highly recommend
that you do so in your own games.
raywenderlich.com 702
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Note: You can add multiple languages to each leaderboard, and Game Center
will display the correct language, according to the locale set on the phone.
Click the Save button when you’re done entering all the details. This will create a
new leaderboard, as shown below.
Now you need to create the other eight leaderboards for the game. Since you
already know how to create a single leaderboard, the rest should be easy. Make
sure you follow the table below while entering the data required for each
leaderboard.
Yes, this may be a bit repetitive, but think of this way: by the time you’re done,
you’ll be an old pro at setting up leaderboards in iTunes Connect! :]
raywenderlich.com 703
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Enter whatever you like for the English localization for each, based on the previous
examples.
After you’re done entering all the details, your leaderboards table in iTunes Connect
should look like the one below:
raywenderlich.com 704
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Make sure Circuit Racer is open in Xcode, and then open GameKitHelper.swift
and add the following method:
//1
let gkScore = GKScore(leaderboardIdentifier: leaderBoardId)
gkScore.value = score
//2
GKScore.reportScores([gkScore], withCompletionHandler: errorHandler)
}
The code you just wrote is responsible for creating and sending a score to Game
Center. Here's a step-wise explanation:
• First, the method creates an object of type GKScore that holds information about
the player’s score. Game Center expects you to send scores using this object.
Game Center also returns objects of type GKScore when you retrieve scores. As
you can see, a GKScore simply stores a value: the number to send to the
leaderboard, which in this case is the number of seconds.
Great work! You have all the code in place to send scores to Game Center. Open
GameScene.swift and add the following property:
let leaderBoardIdMap =
["\(CarType.Yellow.rawValue)_\(LevelType.Easy.rawValue)" :
"com.razeware.circuitracer.car1_level_easy_fastest_time",
"\(CarType.Yellow.rawValue)_\(LevelType.Medium.rawValue)" :
"com.razeware.circuitracer.car1_level_medium_fastest_time",
"\(CarType.Yellow.rawValue)_\(LevelType.Hard.rawValue)" :
"com.razeware.circuitracer.car1_level_hard_fastest_time",
"\(CarType.Blue.rawValue)_\(LevelType.Easy.rawValue)" :
"com.razeware.circuitracer.car2_level_easy_fastest_time",
"\(CarType.Blue.rawValue)_\(LevelType.Medium.rawValue)" :
"com.razeware.circuitracer.car2_level_medium_fastest_time",
"\(CarType.Blue.rawValue)_\(LevelType.Hard.rawValue)" :
"com.razeware.circuitracer.car2_level_hard_fastest_time",
"\(CarType.Red.rawValue)_\(LevelType.Easy.rawValue)" :
"com.razeware.circuitracer.car3_level_easy_fastest_time",
"\(CarType.Red.rawValue)_\(LevelType.Medium.rawValue)" :
"com.razeware.circuitracer.car3_level_medium_fastest_time",
"\(CarType.Red.rawValue)_\(LevelType.Hard.rawValue)" :
raywenderlich.com 705
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
"com.razeware.circuitracer.car3_level_hard_fastest_time"]
Make sure you change the leaderboard IDs to exactly match the ones you entered
into iTunes Connect—and remember, capitalization matters.
This stores the total amount of time the player has to complete the current level.
You’ll use it to calculate the amount of time it took the player to complete the
current track.
maxTime = timeInSeconds
This reports a new score simply by calling the method you added to GameKitHelper.
Here, you're simply adding the line to make the call during the update cycle.
Finally, it’s time to test everything. Build and run the project.
After you successfully complete a track, the game automatically reports your score
raywenderlich.com 706
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
to Game Center. Check the debug console to see if anything went wrong. If you
don’t see any error logs, you can trust that Game Center received your score.
Note: You can also access the leaderboards using the Game Center button you
added in the last chapter. However, the default view is not the leaderboard
view. Guess what you're going to do now? =]
Conveniently, Game Center provides helper methods that retrieve the scores in
each leaderboard using the GKLeaderboard object. Once you have the scores, you
can present the leaderboards to the player in any view.
However, there’s a much easier way to display leaderboards if you don’t want to
create your own custom user interface: the GKGameCenterViewController. You added
support for this view controller in the last chapter, so the leaderboards are actually
available to the player in your app right now!
raywenderlich.com 707
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
If you only wanted to learn how to add basic leaderboards to your app, you can
stop reading now and skip ahead to the next chapter. But if you want to learn a few
advanced things about leaderboards, read on!
Leaderboard sets
Now that you’re familiar with leaderboards, I’d like to tell you about leaderboard
sets. This feature was introduced in Game Center with iOS 7.
Leaderboard sets gives developers the ability to combine several leaderboards into
a single group. Think of a leaderboard set as a tagging framework. Each
leaderboard can belong to one or several groups/sets. This allows you to organize
your leaderboards into a structured hierarchy, rather than a long list like the nine
leaderboards you currently have, which could feel overwhelming to a player.
To add support for leaderboard sets in Circuit Racer, you need an organizing
strategy. You’re going to group your leaderboards according to car type. Thus, all
the leaderboards for the yellow car will belong to the “Yellow car” group, and so
forth.
raywenderlich.com 708
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Note: You could also organize the leaderboards according to difficulty level.
Since leaderboard sets provide the capability to tag leaderboards multiple
times, it would be easy to do it both ways.
Log in to iTunes Connect and open the Features/Game Center page for Circuit
Racer just as you’ve done before.
Under the leaderboards section, you’ll find a button titled More. Click that button
and select the Move All Leaderboards into Leaderboard Sets option to create
your first leaderboard set.
On the next screen, enter Yellow Car for the Leaderboard Set Reference Name
and [Your app Bundle ID].yellowcar for the Leaderboard Set ID.
Click the Continue button when you’re done entering the data.
Now you need to add leaderboards to this set. On the next screen, under the
section Leaderboards in This Set, select the Add to Leaderboard Set button.
Since this is the Yellow Car group, all the leaderboards pertaining to the first car
should be a part of this group.
raywenderlich.com 709
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
As the image above depicts, you first select the leaderboard you want to add to the
set. Next, you enter a display name for the leaderboard within the set. Since the
set is named “Yellow Car,” it makes sense to name the
car1_level_easy_fastest_time leaderboard Easy level. After you’ve done that,
click Save.
In the same fashion, add the other two leaderboards pertaining to the yellow car to
this set, naming them Medium level and Hard level.
Now you need to name the leaderboard set. Under the Leaderboard Set
Localization section, select the Add language button. Select English as the
language and enter Yellow car for the display name. Choose the leaderboard
image leaderboard-yellow.png and click Save.
Using the Add Leaderboard Set button in the first section of the page, repeat the
raywenderlich.com 710
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
above procedure for the blue and red cars. Make sure you give them ID values of
[Your app bundle ID].bluecar and [Your app bundle ID].redcar to remain
consistent with the yellow car leaderboard set, replacing [Your app bundle ID]
with your own bundle ID, of course.
Note: If you decide to support leaderboard sets, you need to ensure that
every leaderboard is part of at least one group.
Finally, once you’ve organized all of the leaderboards into their respective sets,
select the Save button at the bottom-right. You will now see three display sets
under the Leaderboard Sets section:
Note the link to View Leaderboards in Leaderboard Sets. This can be quite
useful to visualize your leaderboard sets—click it, and you’ll see the following:
That’s it! Everything in iTunes Connect is ready. The next step is to show the
raywenderlich.com 711
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
leaderboard sets to the user/local player. Once again, the easiest way to do that is
to use the GKGameCenterViewController.
But you don’t need to add any code! Simply build and run.
When you tap the Game Center button on the home screen, the
GKGameCenterViewController opens and displays the leaderboard sets you created in
iTunes Connect. Note that it might take a few minutes for your leaderboards to
show up in your app—if they don’t appear right away, wait a few minutes.
Ah, sweet simplicity! Now instead of nine leaderboards, you see only three, and it’s
a lot easier to navigate between them thanks to the hierarchical organization.
When your game sends a score to Game Center, it doesn’t send it directly to the
Game Center servers. Instead, it sends the score to the gamed daemon, which in
turn sends it to the servers.
raywenderlich.com 712
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
Why does the system manage communication this way? To answer that question,
imagine that the device sending out the score has an internet connectivity problem.
In such a situation, the gamed daemon stores the scores and will send them to the
Game Center servers when the internet connection is back.
This means you don’t have to worry in case the internet connection is wonky. Game
Center takes care of all the heavy lifting for you.
Limit cheating
Believe it or not, there are people out there who are going to try to cheat by
submitting unearned scores—even scores that aren't humanly possible to achieve!
Game Center provides three ways to combat cheating within your game:
1. Signed submissions: This feature is totally free and you don’t need to make
any changes to your game to use it. When your game submits scores/
achievements to the gamed daemon, it automatically attaches a cryptographic
signature to the submission. Game Center rejects any submission that doesn't
have this signature.
2. Score range: Another way to limit cheating is to use the score range property
raywenderlich.com 713
2D iOS & tvOS Games by Tutorials Chapter 26: Game Center Leaderboards
on a leaderboard. The score range specifies minimum and maximum values that
you think a player could possibly achieve for a particular leaderboard. Any score
that's outside the specified range is accepted by Game Center, but never shown
to another player. Since Game Center holds onto the score, if later you find out
that it's possible to get such a score, changing the set range will make that
score visible. You already set up score ranges when you created your
leaderboards earlier.
Challenges
This chapter has only one challenge, designed to give you a bit more practice with
leaderboard sets.
You don’t need to make any changes to your project to complete this challenge, so
there is no solution project for this chapter.
raywenderlich.com 714
27 Chapter 27: ReplayKit
By Ali Hafizji
In the previous two chapters, you increased Circuit Racer's oomph factor by adding
basic support for Game Center. In this chapter, you're going to add support for
ReplayKit using the new APIs introduced in iOS 9.
Imagine beating your friend at a game, one the two of you have been glued to for
months. A moment like this must go down in history, forever! ReplayKit lets you
record these awesome gaming moments; the cherry on this cake is that you can
also share these moments with others.
ReplayKit records the audio and visuals of the running app. Along with that, it
records the audio from the device's microphone. The output from this recording is a
full HD video that looks amazing on the TV, phone and the Web. ReplayKit also lets
users trim, preview and share the final video on any social network.
Your friend will no longer be able to deny that time you crushed him like a bug on a
rock—a pixelated rock, of course!
Note: This chapter begins where the previous chapter's challenge left off. If
you were unable to complete the challenge or skipped ahead from an earlier
chapter, don’t worry—simply open the starter project from this chapter's
resources to pick up in the right place. Don't forget to update the app's bundle
identifier to your own, and update the achivement IDs in
AchievementsHelper.swift appropriately.
ReplayKit architecture
ReplayKit is extremely easy to integrate into your app. As a developer, you only
have to work with two APIs: RPScreenRecorder and RPPreviewViewController.
raywenderlich.com 715
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
RPScreenRecorder, as the name suggests, is used to start and stop recording. It's a
singleton, and every app has access to its own shared instance of this class.
Although you're only working with two simple APIs, under the hood, the
architecture is a bit more complicated:
raywenderlich.com 716
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
encodes the data in real time and writes it to a movie file in a secure location.
This file is accessible only to ReplayKit's internal services. You may be wondering
how it's possible to access a secure file. Luckily, the API is designed in such a way
that when you stop recording, RPScreenRecorder will create an instance of
RPPreviewViewController. This instance already knows the location of that secure
file. From there, all you need to do is present RPPreviewViewController when
recording is complete. Simple enough?
Integrating ReplayKit
To add support for ReplayKit, you need to perform these steps:
1. Create a strategy to start and stop recording. Decide when the game will
start and stop recording gameplay.
2. Modify the user interface. Add visual elements to let the user know that
recording is in progress, as well as an interface to let the user stop and preview
a recording.
RPScreenRecorder.sharedRecorder().available
// Start recording
let sharedRecorder = RPScreenRecorder.sharedRecorder()
sharedRecorder.delegate = self
sharedRecorder.startRecordingWithMicrophoneEnabled(true) { error in
// deal with error
}
// Stop recording
sharedRecorder.stopRecordingWithHandler { (previewViewController,
raywenderlich.com 717
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
error) in
// Display preview view controller
}
rootViewController.presentViewController(previewViewController,
animated: true, completion:nil)
The rest of this chapter will take you through these steps one by one. As you can
see, the actual API is easy and straightforward; most of your time will be spent on
integrating it nicely into the game.
Note: Since ReplayKit isn't present on tvOS at the time of writing this chapter,
the changes you'll make will only be a part of the iOS target.
This scene flow is quite common in most games. Almost every game has a menu,
followed by the game scene, which ultimately transitions to the end scene. From
there, you can either replay the level or start over.
Since the gameplay sessions are short in Circuit Racer, you're going to opt for an
automatic recording strategy. This means that you're going to tell ReplayKit to start
recording immediately as the level starts, and then stop when the level ends.
raywenderlich.com 718
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
You also want the user to preview the recording and share it; however, instead of
showing the preview controller as soon as the level ends, you're going to defer this
until you present the level "end scene", after all of the action has died down.
Here's how the scene flow will look when you're done integrating ReplayKit:
Note: Every game is different, and the above strategy may not work for all
games. You should always choose what's best for your game. For example, an
RPG has longer game sessions, so it doesn't make sense to record the entire
game session. In this case, you may want to give the player the option to start
or stop recording, rather than handling it automatically.
Before you add the required constraints, set both the Highlighted and Selected
images, listed in State Config, to btn_autorecord_on.png.
raywenderlich.com 719
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
To make the button appear correctly, set its x-position to 460 and its y-position to
30. Also, set its width to 120 and its height to 36. Finally, set the following
constraints:
Now, select the auto-record button and look at the constrains in the Size Inspector.
Double-click on the Trailing Space constraint and set the Constant to zero.
Similarly, set the Constant for the Top Space constraint to 10.
Next, double-click the Equal Width constraint and set the Multiplier to 0.2.
raywenderlich.com 720
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
The auto-record button should now appear in a suitable position on all devices.
Since your strategy is to automatically record gameplay, this button will let the
player toggle this feature on and off.
Build and run the program. You'll see the auto-record button on the home screen.
raywenderlich.com 721
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
Now, open the Media Library and drag and drop the btn_autorecord_small_off,
level_1_preview_frame and play_icon sprites to the canvas. Set the following
properties for each of the sprites you added:
raywenderlich.com 722
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
Set the z-position of the play_icon to 1. All the sprites will now be in suitable
positions, and your final canvas will look like the one below:
Before you proceed to the next section, make the same changes to CircuitRacer/
Scenes/FailureScene/FailureScene.sks.
Build and run the program. You should see the sprites on the Failure and Success
scenes.
The sprites you just added don't function as buttons—at least not yet. You'll take
raywenderlich.com 723
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
Here, you add two new cases to the enum: ScreenRecordingToggle and
ViewRecordedContent. The first one represents the auto-record sprite and the
second represents the preview video sprite.
case .ScreenRecordingToggle:
print("Screen recording toggle pressed")
case .ViewRecordedContent:
print("View recorded content button pressed")
raywenderlich.com 724
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
pressed:
Open GameScene.sks and add indicator_rec.png from the Media Library to the
canvas. Set its Name to record_indicator, its x-position to 1657 and its y-
position to 1275. It will look like this:
You have to look closely! You will only display this when gameplay is being
recorded.
If recording isn't available for one of those reasons, you're going to hide all of the
UI elements you added in the previous steps.
Right-click on the CircuitRacer group and select New File.... Choose the Swift
File template and click Next. Name the file AutoRecordProtocol.swift and click
Create. Make sure you're adding the file to the CircuitRacer target only.
raywenderlich.com 725
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
import Foundation
import ReplayKit
extension ScreenRecordingAvailable {
With the protocol implementation in place, it's time to switch to the home screen.
Although you have the auto-record button showing, tapping it doesn't really do
anything. You'll take care of this now.
Create a new group under the CircuitRacer group and name it ViewControllers.
Add a Swift File to the new group and name it AutoRecord
+UIViewController.swift. Then, add the following code to it:
import Foundation
import UIKit
extension UIViewController {
The above code simply changes the selected property of the button when the
IBAction is invoked. You're using an extension because it gives you the ability to
move the auto-record button to any other view controller and still reuse this code.
Pretty sweet, eh?
raywenderlich.com 726
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
Now, when you tap the auto-record button, it stays in its selected state.
Before you forget, switch back to Main.storyboard and connect the button to this
outlet.
raywenderlich.com 727
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
autoRecordButton.hidden = !screenRecordingAvailable
}
}
The above code simply hides the autoRecordButton when screen recording isn't
available. Of course, you'll need to connect the autoRecordButton to the new
IBOutlet within the storyboard before that can happen.
By the way, to see this working, you'll need to run the program on an older device—
any device older than the iPhone 5s, since ReplayKit is not supported on those
devices. When you do, you'll notice the auto-record button is now hidden.
Under the Screen Recording group, create a new Swift file named GameScene
+ScreenRecording.swift.
import Foundation
import UIKit
import ReplayKit
#if os(iOS)
let recordIndicator = childNodeWithName("record_indicator") as!
SKSpriteNode
recordIndicator.hidden = !screenRecordingAvailable
raywenderlich.com 728
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
if !recordIndicator.hidden {
recordIndicator.position = CGPoint(x: pauseButton.position.x -
pauseButton.size.width/2, y: pauseButton.position.y +
pauseButton.size.height/2)
}
#else
(childNodeWithName("record_indicator") as!
SKSpriteNode).removeFromParent()
#endif
In case the device is running iOS, the code first locates the recordIndicator sprite
by name, and then sets its hidden property based on the device's current screen
recording availability. Finally, it positions the recordIndicator if it's not hidden. If
the device is running tvOS, the record indicator is removed, since ReplayKit isn't
supported.
Finally, you need to change the FailureScene and SuccessScene. Since both the
GameFailureState and GameSuccessState extend the GameOverlayState, you'll make
the changes there.
The above method looks for a ButtonNode with the supplied identifier and returns it.
#if os(iOS)
buttonWithIdentifier(.ScreenRecordingToggle)?.hidden = !
gameScene.screenRecordingAvailable
buttonWithIdentifier(.ViewRecordedContent)?.hidden = !
gameScene.screenRecordingAvailable
#endif
This toggles the visibility of the buttons based on whether screen recoridng is
available.
Just like before, to see this working, you'll have to run the program on an older
device. When you do, you won't see the UI elements you created in the previous
steps—which is a good thing.
raywenderlich.com 729
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
// 1
let screenRecorderEnabledKey = "screenRecorderEnabledKey"
// 2
protocol AutoRecordProtocol: class {
func toggleAutoRecord()
var screenRecordingToggleEnabled: Bool { get }
}
// 3
extension AutoRecordProtocol {
func toggleAutoRecord() {
let autoRecord =
NSUserDefaults.standardUserDefaults().boolForKey(screenRecorderEnabledKey
)
NSUserDefaults.standardUserDefaults().setBool(!autoRecord, forKey:
screenRecorderEnabledKey)
}
}
1. First, you define a String variable to use as a key to access the auto-record
property in NSUserDefaults.
Now, when the user selects the auto-record button, you'll call toggleAutoRecord().
To do this, add the following line of code to the end of toggleScreenRecording(_:):
toggleAutoRecord()
By default, auto record should be enabled, so you'll need to set the default value in
NSUserDefaults.
raywenderlich.com 730
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
#if os(iOS)
NSUserDefaults.standardUserDefaults().registerDefaults([screenRecorderEna
bledKey : true])
#endif
Before you build and run the program, you need to set the default selected state
for the auto-record button.
autoRecordButton.selected = screenRecordingToggleEnabled
Build and run the program. Notice how the auto-record button is selected by
default. Also, if you turn off auto-recording, and then kill the app and start it again,
the auto-record button will remain off, thus remembering its selected state.
func startScreenRecording() {
// 1
guard screenRecordingToggleEnabled && screenRecordingAvailable else
{ return }
// 2
let sharedRecorder = RPScreenRecorder.sharedRecorder()
sharedRecorder.delegate = self
// 3
raywenderlich.com 731
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
sharedRecorder.startRecordingWithMicrophoneEnabled(true) { error in
if let error = error {
self.showScreenRecordingAlert(error.localizedDescription)
}
}
}
Note: The code you just added will generate Xcode compile errors. Don't
worry—you'll fix those next.
As the name suggests, the new method will start the recording process. Here's a
brief explanation of how it works:
1. The method first checks to see if screen recording is available and enabled.
2. Next, it gets access to the RPScreenRecorder singleton and sets the delegate.
dispatch_async(dispatch_get_main_queue(), {
self.view?.window?.rootViewController?.presentViewController(
alertController, animated: false, completion: nil)
})
}
The new helper method simply pauses the game and displays a message using a
UIAlertController.
raywenderlich.com 732
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
RPPreviewViewController?) {
if previewViewController != nil {
self.previewViewController = previewViewController
}
}
}
screenRecorder(_:didStopRecordingWithError:previewViewController:) is called
when recording stops. Notice that it has an optional argument of type
RPPreviewViewController. This represents the view controller that has the ability to
preview, trim and share the recording. You're going to store the instance of this
view controller in a variable.
Open GameScene.swift and add the following variable to the GameScene class:
At this point, build, but do not run, the project. You won't have any compile
errors.
Since you have a method to start recording, it's time to add a method to stop it.
Open GameScene+ScreenRecording.swift and add the following method to it:
// 2
sharedRecorder.stopRecordingWithHandler { (previewViewController,
error) in
if let error = error {
// 3
self.showScreenRecordingAlert(error.localizedDescription)
return
}
self.previewViewController = previewViewController
}
// 5
handler()
}
}
Note: Just like before, you'll have compile errors, but you'll fix those
momentarily.
raywenderlich.com 733
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
previewViewController?.dismissViewControllerAnimated(
true, completion: nil)
}
}
You call previewViewControllerDidFinish when the user selects the Cancel button
on the preview controller. All you're doing here is dismissing the view controller—
and fixing the compile error!
func discardRecording() {
RPScreenRecorder.sharedRecorder().discardRecordingWithHandler {
self.previewViewController = nil
}
}
func displayRecordedContent() {
guard let previewViewController = previewViewController else
{ fatalError("The user requested playback, but a valid preview controller
does not exist.") }
guard let rootViewController = view?.window?.rootViewController else
{ fatalError("The scene must be contained in a window with a root view
controller.") }
previewViewController.modalPresentationStyle = .FullScreen
SKTAudio.sharedInstance().pauseBackgroundMusic()
rootViewController.presentViewController(previewViewController,
animated: true, completion:nil)
}
discardRecording() discards the recorded video. You should call this method once
raywenderlich.com 734
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
Phew! That was a lot of code. But don't worry, you're done with the bulk of the code
you need to set up recording. All you need to do now is call these methods in the
appropriate places.
Open GameScene.swift and add the following line of code to the end of
didMoveToView(_:):
#if os(iOS)
startScreenRecording()
#endif
This will start recording when gameplay starts. Also, in the same method, replace
the code that changes the hidden property of the recordIndicator to the following:
Now, the recordIndicator will be hidden even when auto-record is turned off.
#if os(iOS)
// 1
buttonWithIdentifier(.ScreenRecordingToggle)?.isSelected =
gameScene.screenRecordingToggleEnabled
buttonWithIdentifier(.ScreenRecordingToggle)?.hidden = !
gameScene.screenRecordingAvailable
// 2
if self is GameSuccessState || self is GameFailureState {
if let viewRecordedContentButton =
buttonWithIdentifier(.ViewRecordedContent) {
// 3
viewRecordedContentButton.hidden = true
// 4
gameScene.stopScreenRecordingWithHandler {
let recordingEnabledAndPreviewAvailable =
self.gameScene.screenRecordingToggleEnabled &&
self.gameScene.previewViewController != nil
// 5
if self.gameScene.levelType == .Easy {
viewRecordedContentButton.texture = SKTexture(imageNamed:
"level_1_preview_frame")
} else if self.gameScene.levelType == .Medium {
raywenderlich.com 735
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
viewRecordedContentButton.texture = SKTexture(imageNamed:
"level_2_preview_frame")
} else {
viewRecordedContentButton.texture = SKTexture(imageNamed:
"level_3_preview_frame")
}
// 6
viewRecordedContentButton.hidden = !
recordingEnabledAndPreviewAvailable
}
}
}
#endif
}
The best way to go over this code is to break it down into smaller pieces:
1. First, you set the selected and hidden states for the auto-record button.
Build and run the program on a physical device. Now, recording will start and stop
when the game starts and ends.
Of course, you can't preview the recording yet, because you haven't written the
code to do so. You'll do that next.
Note: At the time of writing, ReplayKit doesn't work with the simulator; you
must run all of your tests on an actual device.
raywenderlich.com 736
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
#if os(iOS)
displayRecordedContent()
#endif
This line of code displays the RPPreviewViewController when the user selects the
preview recording button.
While you're at it, add the following to the ScreenRecordingToggle switch case:
#if os(iOS)
toggleAutoRecord()
button.isSelected = screenRecordingToggleEnabled
#endif
When you finish playing, tap the preview recording button. This will pop open the
RPPreviewViewController, as shown below:
You can use the scrubber on the bottom to trim the recording, and you can use the
share button in the top-right to share the video with your friends.
As you've learned, adding ReplayKit is quite easy and only requires working with
two classes: RPScreenRecorder and RPPreviewViewController. To summarize, here's
a quick round-up of the steps you need to follow to integrate ReplayKit:
raywenderlich.com 737
2D iOS & tvOS Games by Tutorials Chapter 27: ReplayKit
Armed with all of this knowledge, you can add ReplayKit to your own games.
Challenges
You're off the hook for this one; this chapter has no challenges. w00t!
raywenderlich.com 738
28 Chapter 28: iAd
By Ali Hafizji
With the advent of the freemium model, it's getting more and more difficult for app
developers to make a living through the app store. Gone are the days of the gold
rush for indie developers. With all of the changes taking place, smart developers
like you are looking at alternate ways to monetize their apps.
One such way is by displaying ads in your app. And what better way to do it than
with Apple's very own ad network: iAd.
Similar to the app store, 70 percent of the earnings through this network are
shared with you, the app developer. In return, iAd provides quality ads from leading
brands with rich media and interactive capabilities. It's a win-win.
Note: This chapter begins where the previous chapter's final project left off. If
you skipped that chapter, simply open CircuitRacer from the starter folder of
this chapter's resources to follow along. Don't forget to update the app's
bundle identifier to your own, and update the achivement IDs in
AchievementsHelper.swift appropriately.
Introduction to iAd
iAd is Apple's digital advertising platform. If you've used an iOS device, you've
likely come across apps that show ads, a large number of which use iAd. In fact,
iTunes Radio ads are powered by iAd.
One of the benefits of using iAd is that it provides ads with high production value.
The ads have rich media, are immersive and some of them are simply stunning.
There are even ads that run an entire game when the user taps on them. All of this
creates a perfect environment to integrate iAd into your high-quality apps.
In addition to providing high-quality ads, the iAd framework takes care of privacy
raywenderlich.com 739
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
for you. This means you don't need to worry about getting permissions from the
user in case the ad requires access to location data, or about not letting any ads
access personal information. All of this is taken care of by the guys at Apple.
If you're still not convinced, you should know that iAd is present in 14 countries.
This means ads will be shown to your users in those 14 countries. The list is always
expanding, and as new countries are added, more people will start viewing ads,
creating more revenue for you!
First, the developer integrates the iAd framework into the app. When the app wants
to present an ad, the iAd framework sends out a request to the iAd network, which
comes back with ads to display.
The request can sometimes come back with no ads; this completely depends upon
the app that's requesting ads, along with the user's current context. For example, if
the user is in a country where iAd isn't supported, the app won't show ads.
It's also important to note that revenue ultimately starts with a good app. If your
app isn't engaging, you probably won't have many users; therefore, the app will
show fewer ads, and you'll earn little actual revenue. The higher the engagement
and user loyalty, the more ads the iAd network will give you. It's a kind of virtuous
cycle.
raywenderlich.com 740
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
3. The higher the number of tap-throughs, the higher the fill rate, which is a
measure of the number of ads served by the iAd network in comparison to the
number of requests sent.
4. A higher fill rate ultimately leads to more ads. And more adds leads to more
revenue.
So it all boils down to creating a good app. iAd is merely a way to monetize your
app; if the app isn't engaging, then the number of ads served will reduce over time.
raywenderlich.com 741
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
Ad formats
Before you start integrating iAd, it's important to know the different types of ads
provided by the iAd framework. This way, you'll be able to choose the right ad
format depending on your app and the individual user's context.
Banner ad
A banner ad has a slim, device-width view. It's usually placed at the bottom of your
app's content and it continuously loads ads.
raywenderlich.com 742
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
When the user taps on a banner ad, the ad will launch a full-screen experience.
Interstitial ad
When you present an interstitial ad, it immediately takes over the entire screen.
The user can interact with the ad and then dismiss it.
Because these ads take over the entire screen, if you decide to use this format, it's
important to tell the iAd framework to pre-load the ads.
Medium rect ad
You must have come across this ad type on the web. Medium rect ads are
positioned inline with your content. The size for these ads is standardized to
250x300px.
raywenderlich.com 743
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
Similar to banners, the ad continuously cycles through ads. When a user interacts
with one, it shows the fullscreen ad experience.
Pre-roll ad
This ad is a short video that plays before your own video starts playing. It works
seamlessly with MPMoviePlayerController and AVPlayerViewController, so
integration is super easy.
Integrating iAd
To integrate iAd into Circuit Racer, you're going to follow these steps:
For Circuit Racer, you're going to work with banner and interstitial ads only, as
the other ad formats don't really fit the bill for the game.
raywenderlich.com 744
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
Log in to the iTunes Connect website and select the Agreements, Tax, and
Banking module.
Under the Request Contracts section, click the Request button for the iAd App
Network contract type.
iTunes Connect will ask you to accept the iAd agreement. Accept it.
Select the Submit button. Depending on your account, iTunes may or may not ask
raywenderlich.com 745
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
you to fill out your bank details and tax information. After you fill in all those
details, the contract will show up under the Contracts in process section, as you
can see below.
Once the contract finishes processing, it will be moved to the Contracts in Effect
section. This usually takes about 15 to 20 minutes.
With the contract processed, you're all set to start integrating iAd into your game.
Wasn't that easy?
Open the Link Binary With Libraries accordion and select the + button.
raywenderlich.com 746
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
In the pop-up that opens, select the iAd.framework library and click Add.
You've added the framework to your project, so you can start using the iAd APIs in
your game.
Note: Since tvOS doesn't support the iAd framework, you haven't added the
framework to the tvOS target.
raywenderlich.com 747
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
In Circuit Racer, you're going to display banner ads at the bottom of the home
screen, the level selection screen and the car selection screen.
Right-click on the CircuitRacer group and select New Group. Name the group
iAd.
Next, right-click on the new group and select New File.... From the options, choose
Swift File and click Next. Name the file iAd+UIViewController.swift and click
Create. Make sure that you only select the CircuitRacer target.
The iAd framework makes it extremely easy to display any type of ad. For example,
to display a banner ad, all you need to do is import the iAd framework and set the
canDisplayBannerAds property to true.
import UIKit
import iAd
// 1
protocol iAdBannerProtocol: class {
func enableBannerAds()
}
// 2
extension iAdBannerProtocol where Self : UIViewController {
func enableBannerAds() {
canDisplayBannerAds = true
}
}
raywenderlich.com 748
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
import UIKit
Similarly, create a new file under the iAd group for the level selection screen, name
it iAd+SelectLevelViewController.swift and add the following extension to it:
import Foundation
The above code simply shows banner ads on the level selection screen.
The last screen left is the home screen. Since you already defined an extension for
the HomeScreenViewController in the last chapter, you'll just reuse that. Open the
HomeScreenViewController+ScreenRecording.swift file under the
ScreenRecording group. Change the definition of the extension to implement the
iAdBannerProtocol:
enableBannerAds()
Similar to the implementation for the level and car selection screens, the above
code displays banner ads on the home screen.
Build and run the game. You should now see banner ads on your home screen, level
selection screen and car selection screen, as shown below.
raywenderlich.com 749
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
Interstitial ads need to be pre-loaded. It's best to do this as soon as the app starts,
so you can ensure that you have an ad to present to the user.
raywenderlich.com 750
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
#if os(iOS)
UIViewController.prepareInterstitialAds()
#endif
if !adPresented {
navigationController?.popToRootViewControllerAnimated(false)
}
#else
navigationController?.popToRootViewControllerAnimated(false)
#endif
}
You'll call the above method when the user selects the Quit button on the level end
screen.
Build and run the game. When you select the Quit button on the level end screen,
you'll see a full-screen ad.
raywenderlich.com 751
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
The only problem is that when you dismiss the ad, the game doesn't return to the
home screen. You can fix that easily.
Build and run the game. Dismiss the interstitial ad, and the app will navigate back
to the home screen.
1. App engagement time: If you're trying to drive revenue through ads, you
need to ensure that the app has sufficient engagement time. For example, if the
user only spends a couple of minutes on average in your app, that's not enough
time to display ads and allow interactions.
3. Don't mess with the fill rate: The fill rate is the number of ads delivered
divided by the number of ads displayed. The important point here is to display
the ad. If you simply send ad requests and don't display the ad, it's not counted
as an impression.
raywenderlich.com 752
2D iOS & tvOS Games by Tutorials Chapter 28: iAd
Challenges
This is your final challenge for Circuit Racer. You've taken a fun game, and over the
course of the last three chapters, added support for Game Center achievements,
leaderboards, ReplayKit and iAd. That's impressive!
As always, if you get stuck, you can find the solution in the resources for this
chapter.
raywenderlich.com 753
29 Chapter 29: 2D Art for
Programmers
By Mike Berg
In this book, you've created some great mini-games, but they’ve all used pre-made
art. You may wonder how you can get art like that in your own games—and that’s
what this chapter is all about!
These days, to succeed in the App Store, your game not only needs to be fun and
innovative; it needs to look attractive. This is a problem for many aspiring game
developers—because although you may be great at programming and game design,
you might not be so great at making your game look the way it does in your
daydreams. There’s a reason many games feature doodle or stick man art!
The good news is that you have two solid options as a game developer: You can
either hire an artist to help out, or you can make the art yourself. After all, making
art is a skill you can practice and improve, just like any other skill.
In this chapter, I’ll first help you decide whether you’d prefer to hire a game artist
or make your own art. In the event you choose to do it yourself, I’ll show you how
to create cartoon artwork for your video game in a style that's similar to the art
you’ve used in this book:
raywenderlich.com 754
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
I’ll also provide tips for how you can continue to develop your skills as a game
artist. By the end of this chapter, you'll have a solid starting point and a map for
making your games look as great as they play!
Hire an artist
There are advantages to hiring an artist:
• Work with a pro. If you hire an artist, you can get someone who's already
great at her craft, and likely has been practicing for years. She can focus on what
she's good at, leaving you free to focus on your own specialty—which is likely
programming!
• Choose your style. Different artists have different styles—and your game might
benefit from a certain type of style. By hiring an artist, you can look around and
find someone whose style is the perfect fit for your game.
• Rapid development. If you have to make the art as well as program the game,
you’ve just doubled your development time. Obviously, splitting up the work can
save a lot of time, letting you get your game to market faster.
• Collaboration. Sometimes working with an artist can help you improve your
game, as you share ideas and feed off each other’s energy and passion. Think of
the best games you’ve seen—I bet most of them were made by a team of at
least two!
Think this option is for you? If so, feel free to skip ahead to How to find and hire
an artist. But first, you may want to consider your other option...
Do it yourself
Doing it yourself has advantages, as well:
• Save money. Sadly, most artists won't be willing to make art for your game out
of the kindness of their hearts or for promises of future money—they’ll want cold,
hard cash. And often, that's the very thing that indie game developers lack. If
saving money is on your mind, doing it yourself might be the only option.
• Build skills. A lot of indie developers find the entire process of making a game
fun and exciting, and want to learn the skills involved in every step. If this
sounds like you, maybe you want to make art simply for the experience and skills
you’ll pick up along the way.
raywenderlich.com 755
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
• Lack of dependencies. Working with an artist does introduce a delay into the
process. If you need a piece of art right away, you’ll need to wait until the artist
is available to work on it. If you make the art yourself, or at least know enough
basics to make some placeholders, you can keep moving at the rapid speed that
game development often entails.
• Glory and honor. The final benefit of doing it yourself is the pure bragging
rights you'll earn. “See that game? Yeah, I programmed it and made all the art
myself. Booyah.” Just be prepared for a long development cycle! :]
Think this option is for you? If so, you can skip ahead to Creating your own
artwork.
• Unite: http://unity3d.com/unite
• IndieCade: http://www.indiecade.com/
• RWDevCon: http://rwdevcon.com/
As for meet-up groups, search meetup.com for your local area. The best meet-ups
are game developer, artist, or iOS hangouts. You can also check game developer
forums like http://forums.tigsource.com/ or http://www.gamedev.net/ to see if
there are any local meet-ups or game jams in your area.
Twitter
Many game developers hang out on Twitter. While you won’t necessarily get to
know someone personally on Twitter, it’s a great way to expand your list of
developer contacts around the world, people who you may eventually meet at a
raywenderlich.com 756
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
conference or event.
Be an active Twitter user and get to know the work of those you follow and those
who follow you. The larger your network of developers, the easier it will be for you
to find someone with the skills you need when the time comes.
• A lot of artists post their work on Deviant Art—it’s often a great way to find up-
and-coming artists: http://www.deviantart.com/
• If you’re into pixel art, look no further than Pixel Joint, a community of pixel
artists who regularly post their work and portfolios: http://www.pixeljoint.com/
• 3D Total has a terrific gallery that’s organized into categories like Character,
SciFi, Fantasy and Cartoon. The site also has very active forums: http://www.
3dtotal.com/index_gallery.php
• Concept Art has a job board with a lot of game illustrators: http://
www.conceptart.org/forums/
Of the applications you receive, discard those without good portfolios, regardless of
their experience. A portfolio should demonstrate the artist’s ability to do the job in
the style you want it done.
raywenderlich.com 757
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
The moral of the story is this: Get to know artists you admire, preferably in person,
and make connections—you never know when you might want to work together on
a project!
These types of contracts are often appealing to indie game developers with little
money to spend, but it’s very hard to find an experienced artist who is willing to
take this kind of deal. Many an artist has been burned by promises of a revenue
share that fails to deliver in the end!
These types of deals only seem to work if you have a strong, preexisting
relationship with the artist. For example, if your good friend or significant other is
an artist, you might be in business. Also, the artist will probably need to be
passionate about the project for it to work out—otherwise, she might lose
motivation and interest.
If you don't have an artist on hand who trusts you and believes in your project,
you'll probably have to pay an artist up front. This can be a good thing—if your
game does well, you'll get to keep the profits for yourself!
The principal advantage of a fixed-quote contract is that you know how much you're
going to spend.
raywenderlich.com 758
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
As I work on the project, I keep very detailed track of my time and provide the
client with regular updates that show how I’ve used my time. I send an interim
invoice every time the total owing reaches a certain agreed-upon amount. For
example, the client and I might have an agreement that I send an interim invoice
for every $2,000 of work.
This method gives both the client and the artist the freedom to make changes to
the to-do list on the fly, which is always necessary while a game is in development.
Potential disagreements over cost-value can be headed off before they develop. The
client knows exactly what she's getting for her money, and the artist stays
motivated and involved in the process.
Price expectations
People often ask me, “How much do artists charge to make art for a game?” If
you’re hiring an experienced artist, you can expect to pay anywhere from $30-$90
per hour. At the time of writing, my own rate is $70 per hour.
Generally, the more skilled the artist, the more you can expect to pay. You might
find an artist who is willing to take less, but expect her experience and the overall
result to be commensurate. “You get what you pay for” is an old adage for a
reason.
There’s also great value in finding an artist with experience making video game
art—iOS-specific experience is especially helpful. There are many ways an artist
can make your life as a developer easier (or harder!), and her level of game
development experience is a large factor. For example, an artist who knows about
object coordinates, anchor points, texture atlases and overdraw will be able to
provide you with graphic files that are ready to use in your tools of choice, requiring
as little extra processing on your end as possible.
Knowledge of games and how they work will help an artist create assets that are
efficient and extendible, for long-term reuse and adaptability in the event of future
updates. Occasionally, changes to one art asset can require updates to several
raywenderlich.com 759
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
others; good game artists will know how to keep these sorts of snowball effects to a
minimum.
That’s it for my advice when it comes to hiring an artist. If this is the route you’ve
decided to take, stop reading here and go find yourself an awesome artist.
But if you’re eager to learn how to make art yourself, read on!
Getting started
The rest of this chapter will show you how to create a cat sprite in a style similar to
that of the sprites you've used in your mini-games throughout this book:
You’ll create the artwork in Adobe Illustrator using vector shapes. This will let you
use the artwork at any resolution or size without degrading the quality of the
image.
If you don’t already have access to Adobe Illustrator, you can download a free trial
here: https://creative.adobe.com/products/download/illustrator
This will install an interface to Adobe’s Creative Cloud, which lets you try many
different Adobe products for 30 days.
Once you have Illustrator installed and ready to go, pull out a pencil and paper and
get ready to sketch.
raywenderlich.com 760
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
The cat you’ll be sketching is made up of four main shapes: head, body, front
legs and tail. Here’s a quick preview of the four main shapes:
Now follow the instructions below to draw the cat, consulting the images as you go.
The shapes shown here are just guides; if you’re comfortable with adapting as you
go, please put your own spin on them!
2. The pear-shaped bottom of the body should be a bit larger than the head, with
the upper body/neck stretching up to meet the head.
raywenderlich.com 761
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
3. Near the head and close to the edges of the body, draw two smooth lines that
converge a bit at the bottom for the legs.
4. At the bottom of the legs, add an open half-circle for the tops of the feet.
5. For the bottoms of the feet, add an arc that’s a bit flatter than the tops of the
feet.
6. Define the shape of the tail, starting with the leftmost curve. Add a second
curve to give it thickness. Cartoon tails can be a lot thicker than real tails!
raywenderlich.com 762
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
7. Now that you have the cat's basic shape, add some detail. Cartoon cats can
have pointy cheeks. Don’t ask why!
8. Look at each arc of the ear separately. Start near the middle of the head and
draw an arc slightly up, but mostly out. Then curve down and slightly inward to
finish.
9. Round out the sides a bit, where the ears meet the head, and add a bit of
height to the top of the head.
10. Think of the eyes as egg shapes, tilted inward a bit. The nose is a flat oval.
raywenderlich.com 763
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
11. Add a short, vertical line under the nose with a wide “W” shape underneath for
the mouth. A wide arc over the nose defines the cat’s snout.
12. Add large arcs for the irises, some straight whiskers pointing slightly upward,
and don’t forget those eyelashes for extra loveability.
13. Moving on to the legs, draw a straight line down the middle, not quite as high as
the outer lines for the legs.
14. Cut a small wide triangle out of the bottom center of the feet to make them
point slightly outward. Add two short lines on each foot to make toes.
raywenderlich.com 764
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
15. Smooth out the arc for the back a bit. Add a line for the top of the back leg.
16. One at a time, add curved points to make up the fur at the end of the tail. Make
them varied in size, with the middle one the biggest.
17. Add a curve near the end of the tail, and curves over the top edge of each paw.
These will define areas with white fur.
18. Optional: Erase unnecessary lines. You only need to do this if your drawing is
very “sketchy” (which is OK!) and it’s hard to see the final lines, which are the
ones you’ll be going over in Illustrator.
raywenderlich.com 765
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Email yourself the photo, using the Large setting when asked how you want to
resize the image. Save the image to your computer.
Note: If you have an older version of Illustrator, you won't see the Devices
option in the Size pop-up. Just set the pixel dimensions manually; it works out
to be the same thing.
raywenderlich.com 766
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Select File\Place… and select the file you emailed yourself for the sketch. Leave
the checkbox options as they are by default, with only Link selected. Click Place to
add your sketch to the Illustrator file.
raywenderlich.com 767
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
You'll see the “Place” cursor, along with an icon of your image.
Click and drag to draw a box that mostly fills the canvas. This sets the size and
position of your placed file.
Note: Again, older versions of Illustrator may look a little different here.
Many of the controls you’ll use to modify your artwork are organized into what
Illustrator calls palettes. You can find them on the right side of the screen. It's a
good idea to make sure the ones you’ll use most often are visible.
Select Window\Workspace\Essentials. If all you see are buttons, click the tiny
Expand Panels button at the top right of each panel:
raywenderlich.com 768
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
You'll see an arrangement of palettes that looks something like this—don’t worry if
it’s not exactly the same:
raywenderlich.com 769
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
In the Layers palette (select Window\Layers if you don’t see it), click the
empty square next to the “eye” icon on the layer for the cat. This will lock the
layer, preventing you from selecting it or moving it accidentally.
Now, click the Create New Layer button to add a layer; you’ll use the new layer to
hold your vector tracing.
At the bottom of the Tools palette, there are swatches for the current fill color
(the solid box) and the stroke color (the box with the thick outline). Illustrator will
use these fill and stroke colors for any object you create.
Press D to set the default colors for stroke and fill: white fill, black stroke. Click the
fill swatch, which is a white square:
Press the forward slash key (/) to clear the fill swatch and make it transparent—
a red line will appear through the swatch. Now the Pen tool will draw black lines
with no fill, which is ideal for creating outlines.
Next, select the Pen tool (shortcut: P). With the Pen tool, every click creates an
anchor point for the curve, and dragging lets you define the direction and
“strength” of the curve for that point. You may be familiar with the concept of a
Bezier curve from programming—that is what this tool lets you create!
Click to draw straight lines with sharp corners; click-and-drag to create curves. A
raywenderlich.com 770
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
curved point has “handles” that define the direction and strength of the curve. Each
handle is a dot at the end of the line coming out of the point:
Follow the instructions below to use the Pen tool to trace the ears of the cat, which
will be simple 3-point curves. Your first point will be a curve, so start by clicking
and dragging, as shown below. And yes, you can edit a line after you’ve drawn it—
more on that soon.
1. Click at the start of the line and drag toward the upper-left.
2. Click at the tip of the ear and drag out shorter handles, roughly perpendicular to
the first set of handles.
3. Click at the end of the line and drag a handle out until the line looks right.
Hold the Command key and click anywhere else on the canvas to finish creating
that line. Use the Selection tool (shortcut: V) to click on the line you just created:
At the top of the screen, increase the stroke width to something more substantial,
like 4px.
Continue creating paths for the top of the head and the other ear. The Pen tool
remembers the settings for the last line you selected, so new lines will have the
same stroke thickness.
raywenderlich.com 771
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Great work! It can take some time to get used to the Pen tool, but with a little
practice, you'll create your lines with minimal fuss.
Editing a path
Your line won’t always look exactly the way you want it to on the first try, and that’s
OK! It’s easy to adjust the shape of a line.
To change a line after you’ve created it, use the Direct Selection tool (shortcut:
A) to click on the line:
This will highlight the points. Click on a single point to show its handles. Drag the
point to move it, or drag the handles to adjust the angle and strength of the curve.
Note: Illustrator has a feature called Smart Guides. If it's turned on, it will
attempt to snap points, paths and even 90-degree angles automatically. It's
often easier to edit your lines with Smart Guides turned off. Select View
\Smart Guides (Command-U) to toggle this feature.
3. With the Pen tool still active, Option-drag the handle so it points roughly
toward the next corner.
raywenderlich.com 772
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
This method lets you create almost any shape of line with a single series of mouse
gestures, all without having to change tools. Remember, once you've created a line,
you can always refine its shape with the Direct Selection tool.
Use this method to create the rest of corners for this line, as shown here:
raywenderlich.com 773
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Note: Press Command-S to save. If you want to have a look at the lines I’ve
created at this point, go to the Resources folder and open cat-01-outlines.ai.
raywenderlich.com 774
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Note: The Width tool is only available in Illustrator CS5 and up.
1. With the Width tool, hover your cursor over the tip of the ear. The tool shows
a little white circle that indicates it will add a new width point here.
2. Click and drag to make the line slightly wider at that point.
3. Illustrator makes the line thicker, with a smooth transition to the end points.
Use the Selection tool (V) to click on the line you just edited. At the top of the
screen, you’ll see some options for the current path, including a pop-up that shows
the Width Profile you just created.
Name it Thick in the middle. This creates a reusable width profile that you can
raywenderlich.com 775
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
apply to any path. Press Command-A to select all the paths in your drawing, and
then select your new profile from the Width Profiles pop-up.
The effect is subtle, so look closely if you don’t think you see it. Now those lines
really do look more like they were hand-drawn. And originally, they were!
Notice how in the picture above, the path on the left ear (the first one you made) is
thicker than the others. If this happened to you, select the thicker path with the
Selection tool (V) and change its stroke width back to 4px in the bar at the top of
the screen:
Experiment with the Width tool to customize your lines. Keep going until you’re
raywenderlich.com 776
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
The Width tool is powerful, with several features not covered here. For a more
detailed look, watch this video: http://tv.adobe.com/watch/learn-illustrator-cs5/
using-variablewidth-strokes/
Note: Press Command-S to save your file. My version of the file at this point
is in the Resources folder and is called cat-02-width-tool.ai.
Press Shift-X to reverse the fill and stroke colors. Your fill was transparent before,
so now your strokes are transparent and your fills are black. It looks a little
strange, but you’re about to fix that:
You need to edit these points, but since they’re behind and completely covered by
the outline paths, it’s impossible to be sure you’re editing the points for the fill and
not the paths for the stroke.
raywenderlich.com 777
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
To solve this, select Object\Group (Command-G) to group the new set of paths,
and double-click the group to enter Isolation Mode. This lets you work on the
contents of the group without any other objects getting in the way.
In Isolation Mode, Illustrator has faded the rest of your art a bit, showing you that
it can’t be selected.
You need to join all these separate paths together to make one solid shape that you
can color. Use the Direct Selection tool (A) to draw a marquee around the
bottom two points to select them:
raywenderlich.com 778
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Select Object\Path\Join (Command-J) to join the two points. This joins the left
and right paths, creating a filled path that’s almost the size of cat’s head, but still
open at the top:
Use the Direct Selection tool (A) to select the top-left point of the path. Shift-
click the left point of the curve that forms the top of the cat’s head:
Press Command-J to join the points. Your cat’s head should now be completely
filled. The path is still open at the top right, under the ear, but that’s OK. You're
going to merge it with the ear shapes next.
raywenderlich.com 779
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Switch to the Selection tool (V) and Shift-click each of the ears to add them to
the current selection. In the Pathfinder palette (select Window\Pathfinder if
you don’t see it), click the Unite button. This merges all three paths into a single
shape.
With the fill shape selected, it's time to give it a better color. In the Swatches
palette (select Window\Swatches if you don’t see it), select a color for your cat:
Note: If the stroke color changes instead of the fill, press Command-Z to
undo, press X to make the fill color the active swatch, and then select your
color again.
Double-click outside of your shape to exit Isolation Mode. You've finished your
first filled shape!
Why have you colored the head separately? It’s best to create a separate fill for
each basic shape so that when you get to the shading stage, you can layer shading
objects behind objects in front.
For example, you'll layer the shading for the body behind the shape of the legs. If
raywenderlich.com 780
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
the fill for the body and legs were the same object, it wouldn't be possible to layer
them without doing some extra trimming of the shaded area—this will become clear
shortly.
Use the same method to create filled shapes for the legs, body and tail. Remember
the basic steps:
1. Use the Selection tool (V) to select the paths that create the outline of the
area you’re trying to fill.
6. As necessary, combine the paths with the Direct Selection tool (A) and
Command-J, or via the Unite button in Pathfinder.
Once you’re done, use the Selection tool (V) to select the lines and fill for the tail
and send them to the back by pressing Command-Shift-[.
raywenderlich.com 781
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Select the outer, egg-shaped ovals for both eyes with the Selection tool (V). Copy
the paths (Command-C) and paste behind (Command-B). Swap the stroke and
fill colors (Shift-X) to create a filled path with no stroke. Click the white swatch in
the Swatches palette to change the fill color to white.
1. With the Selection tool (V), click the top arc of the iris. Press Command-C to
copy it, then Command-B to paste behind.
4. This shape isn’t big enough for the iris. To make it the right shape, use the Pen
tool (P) and click the end point of the path.
5. Then click once for each point shown here, covering the lower part of the eye
with the new shape. Double-click outside the shape to leave Isolation Mode.
raywenderlich.com 782
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
6. You need a duplicate of the white eye shape to use for the Pathfinder operation.
Use the Selection tool (V) to click it. Press Command-C to copy and
Command-F to paste in front.
7. Still using the Selection tool, hold Shift and click the shape you just created
for the iris.
9. The shapes are combined, leaving just the shape of the iris.
10. With the iris shape still selected, click a green color in the Colors palette.
11. Click empty space on your document to deselect. Click the black swatch in the
Swatches palette. Then use the Ellipse tool (L) to draw a black circle for the
pupil.
12. Now you need a duplicate of the iris shape to use for the Pathfinder operation.
Use the Selection tool (V) to select it. Press Command-C and then
Command-F to create the duplicate.
raywenderlich.com 783
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
13. Hold Shift and click the pupil to select both shapes. In the Pathfinder palette,
click the Intersect button.
14. You’re left with the iris and pupil looking very nice.
15. Using the Ellipse tool (L), draw a white circle to use for a highlight on the eye.
Repeat this entire process for the other eye.
16. Select the curves for the tops of the irises and change them to a dark green.
raywenderlich.com 784
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
You need a shape that's white with no stroke. Press D to set the default fill (white)
and stroke (black). Make sure the stroke swatch is on top of the fill swatch, as
shown here:
If it isn’t, press X to switch them, bringing the stroke to the front. Press the
forward slash key (/) to remove the stroke:
1. Using the Pen tool, draw a curved line across the tail, near the top. The curve
should extend out past the edges of the tail, as shown. You’ll trim it down later.
2. Continue the path around the end of the tail by clicking once for each point.
Make sure you cover the end of the tail completely. The shape doesn’t need to
be closed—the Pathfinder tools work just as well with open paths.
3. Use the Selection tool (V) to select the tail, press Command-C to copy it,
then press Command-F to paste in front. You’ll now use this copy of the tail
shape with the Pathfinder to make the white path match the shape of the tail.
4. Hold the Shift key and click the white path so it's also selected. For the
Pathfinder to work, it’s important that both the tail shape and the white shape
you just created are selected, that they are the only shapes selected, and that
raywenderlich.com 785
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
6. Illustrator trims away the parts of each shape that are not overlapping. What
remains is a single shape for the white end of the tail.
Look at your Layers palette. Currently, there are two layers: one for your sketch,
Layer 1, and one for your vector artwork, Layer 2. Click the disclosure triangle next
to Layer 2:
raywenderlich.com 786
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
This reveals all the objects that are within that layer:
An object is a single path or shape. A single layer can hold any number of objects.
As you can see, you’ve created a lot of objects in Layer 2 already! The Layers
palette shows the objects in order of their layering. Objects at the top of the list
appear onscreen in front of objects farther down the list.
Getting back to your cat’s tail, the white shape at the tip of the tail is currently
sitting in front of the black outlines for the tail. You could press Command-[ until
it’s behind the outlines, but that could get tedious in a document with many
objects.
Instead, use the Selection tool (V) to select the white shape for the end of the
tail, press Command-X to cut it, and then select the object you want it to
raywenderlich.com 787
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
appear in front of—in this case, the color fill shape for the entire tail—and press
Command-F to paste in front. This is the fastest way to get an object exactly
where you want it in the layer stack.
Use this method to paste the white tip of the tail in front of the fill for the tail. It
should now show up behind the stroke:
Follow the same steps to create a white fill for the front paws. As a reminder, here
are the basic steps:
1. Use the Pen tool (P) to draw a shape that colors the area in the paws you want
filled, overlapping the area outside the paws.
2. Use the Selection tool (V) to select the paws, press Command-C to copy
them, then press Command-F to paste in front.
3. Hold the Shift key and click the white path so it's also selected.
The last piece to add is an oval around the cat’s mouth and nose. Use the Ellipse
tool (L) to create one:
raywenderlich.com 788
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
To get the ellipse into the right place in the layer stack, use the paste-in-front
technique to cut the white ellipse and paste it in front of the color fill for the head.
This will place it behind all the strokes for the face.
Checkpoint! Press Command-S to save. If you wish, you can start from here
using the file cat-05-tail-and-toes.ai in the Resources folder.
With the light shining from the top right, shadows will fall on the lower-left areas of
any given object:
raywenderlich.com 789
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
To create shadows like this in Illustrator, you’ll use almost the same Pathfinder
technique you used to create the white areas for the tail and paws.
Start by using the Selection tool (V) to select the fill for the body. Then, follow
these instructions:
1. Recall that two shapes are required for a Pathfinder operation. Press
Command-C to copy the fill for the body. Then press Command-F twice.
2. Drag the currently selected shape (the topmost one) up and to the right a bit,
shown here in black so it’s easier to see. I also dragged the bottom-right resize
handle up and to the left a bit to make it smaller.
3. Hold Shift and click the first copy you made of the body shape, so that they're
both selected.
raywenderlich.com 790
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
4. In the Pathfinder palette, click the Subtract button. Illustrator subtracts the
front shape from the back shape.
5. What remains will work well for a shadow on the body. If you haven’t already
changed the color to black, click the Fill box in the toolbar on the left to bring it
forward, then click the black swatch in the Swatches palette.
7. Remember to rearrange the layers. Cut the shadow shape, select the body fill
shape, and then paste in front (Command-F), as described earlier.
Use these steps to create shaded areas for the front legs, head and tail. Here are
reminders of the steps to follow:
1. Select the area you want to shade with the Selection tool (V).
2. Create two copies of the shape. Press Command-C to copy it, then press
Command-F twice.
3. Move one of the copies up and to the right a bit, optionally resizing it.
raywenderlich.com 791
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
5. Set the color of the shadow area to black and set the transparency to 30%.
Remember: Copy the base shape, paste twice, drag the top shape up and to the
right and use the Pathfinder to subtract it from the first copy. Then, change the
opacity and make sure it’s layered correctly.
Create these by drawing a black path with the Pen tool (P) and then setting its
transparency to 30%. Here are the abbreviated steps:
raywenderlich.com 792
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
raywenderlich.com 793
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
In the same way, add depth to the tufts of hair at the end of the tail. Imagine
extending the inner line down further into the tail, then curving back up to the tip,
and you have the shape for your tail shadows:
Save! Press Command-S to save your progress. A version of the file at this
point is in the Resources folder and is called cat-06-shaded.ai.
Start by using the Selection tool (V) to select any one of the black outlines. In the
menu bar, choose Select\Same\Stroke Color. This will select all of the black lines
in the document.
raywenderlich.com 794
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Have a look at the swatches and make sure the stroke swatch is on top of the fill:
If it’s not, click on it to bring it forward or press X to swap them. Click on a slightly
darker version of your fill color in the Swatches palette to change your stroke
color.
Some of your strokes can stay black (the nose) or be dark grey (the whiskers and
mouth). Use the Selection tool (V) to select the strokes you want to change and
then click the color in the Swatches palette that you want to use. This is mostly a
matter of preference, so play around with the outlines until they look just the way
you want.
You've finished your cat! Don’t forget to save. Press Command-S to do so now.
raywenderlich.com 795
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Before you export the cat image, you need to shrink down the Artboard to match
the size of the cat itself.
Press Command-A to select all the art on the canvas. Select Object\Artboards
\Fit to Selected Art to make the Artboard match the size of the art.
raywenderlich.com 796
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
That was easy! Select File\Save for Web… At the top right, choose the PNG-24
preset and click Save. PNG-24 will save a full-quality PNG file with transparency,
and is the preferred graphic format for all iOS apps.
raywenderlich.com 797
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Next, create the 1x (non-Retina) version of the graphic, which is scaled down by
50%. Select File\Save for Web… again. In the Image Size panel, set Percent to
50 and click Save to create the non-Retina version of the graphic. Make sure you
use the same filename as the previous step, without @2x—that is, cat.png.
raywenderlich.com 798
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Finally, create the 3x version of the graphic, which is scaled up to 150%. Select File
\Save for Web… again. In the Image Size panel, set Percent to 150 and click
Save to create the 3x version of the graphic. Make sure you use the same filename
as the previous step, with @3x—that is, [email protected].
Congratulations! You’ve taken a blank piece of paper and turned it into a resolution-
independent piece of game art, ready to use in your next project.
Challenges
Want to polish your artistic skills even more? Here are two more characters you can
use as guides to practice the techniques you’ve learned in this chapter.
As always, if you get stuck, you can find the solutions in the resources for this
chapter—but give it your best shot first!
raywenderlich.com 799
2D iOS & tvOS Games by Tutorials Chapter 29: 2D Art for Programmers
Challenge 1: Squeak!
Your first challenge is to draw one of the simplest possible characters: a lowly
mouse. The shape is nice and simple, so you can quickly practice going through the
steps you’ve learned in this chapter, all on your own.
Challenge 2: Woof!
For a greater challenge, how about making a dog? This one involves more
complicated shapes and more shadows to match.
Be sure to keep experimenting and most importantly, have fun. The key to
developing your skills as an artist and a programmer is to practice, practice,
practice.
If you'd like to share any of the artwork you create after following this chapter, we’d
love to see it. Please stop by the book forums for show and tell!
raywenderlich.com 800
C Conclusion
We hope that you have enjoyed your adventure through this book. If you followed
along the entire way, you have made five complete iOS and tvOS games with Sprite
Kit and Swift from scratch – spanning everything from zombies to cats to
rampaging dinosaurs.
You now have all the knowledge it takes to make a hit game, so why not go for it?
Come up with a great idea, prototype a game, get people to play it, watch them for
feedback and keep iterating and polishing your game based on all you have
learned. Be sure to set aside time in your schedule to add juice to your game, and
make sure you have killer art and sound effects, following the advice in the book.
We can’t wait to see what you come up with! Be sure to stop by our forums and
share your progress at www.raywenderlich.com/forums.
You might also be interested to know that we have a monthly blog post where we
review games written by fellow readers like you. If you’d like to be considered for
this column, please visit this page after you release your game:
www.raywenderlich.com/reviews
We have one final question for you: Did we succeed in our goal to write the best
book on game programming you’ve ever read? Please email us anytime at
[email protected] to let us know either way.
Thank you again for purchasing this book. Your continued support is what makes
the tutorials, books and other things we do at raywenderlich.com possible. We truly
appreciate it.
— Mike, Michael, Ali, Neil, Toby, Rod, Marin, Tammy, B.C., Vinnie, Ray and Vicki
raywenderlich.com 801