In the previous article in this series, we looked using actions to create complex sprite movements with very few lines of code. As this series progresses, we’ll work our way towards building complete games.
In this article, we’ll look at responding to user’s touches on the screen.
Set up a new project
It’s time to do the File → New → Project… dance again! Open up Xcode and create a new Game project:
When it comes time to name the project, give it an appropriate name like RespondingToTouches (that’s the name I gave my project).
Once again, we’ll set the game to that it’s landscape-only. The simplest way to restrict our app’s orientation to landscape is by checking and unchecking the appropriate Device Orientation checkboxes in the app’s properties. Make sure that:
- Portrait is unchecked
- Landscape Left and Landscape Right are checked
You can confirm that the app is landscape-only by running it. It should look like the video below, with spinning spaceships appearing wherever you tap the screen. It shouldn’t switch to portrait mode when you rotate the phone to a portrait orientation:
Making the spaceship go to where you touch the screen
It’s time to make our own interactivity. Let’s make an app that puts the spaceship sprite in the center of the screen, and then has the spaceship sprite immediately go to wherever the user touches the screen.
Open Gamescene.swift and replace its code with this:
import SpriteKit class GameScene: SKScene { override func didMoveToView(view: SKView) { } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { } }
First task: drawing the spaceship sprite in the center of the screen. Before you look at the code, see if you can do it yourself. There’s enough information in the previous article to help you do this, or you can follow these hints:
- A sprite is an instance of the
SKSpriteNode
class, and given the name of an image, you can instantiate one by usingSKSpriteNode
‘sinit(imageNamed:)
method. - A game scene’s
didMoveToView
method is called when the scene is first presented, and it’s the perfect place to do things like setting sprites in their initial locations. - A sprite’s location onscreen can be set using its
position
property, which is of typeCGPoint
, astruct
that holds 2Float
s: one for the x-coordinate, and another for the y-coordinate. - The
frame
of a scene is the rectangle — aCGRect
— that determines its bounds. You can get itswidth
andheight
through the properties bearing those names. - You might want to scale down the spaceship sprite to one-third of its size. The
setScale
method will do that. - To display a sprite in a scene, you need to use the
addChild
method.
Your code should look something like this…
import SpriteKit class GameScene: SKScene { let spaceshipSprite = SKSpriteNode(imageNamed: "Spaceship") override func didMoveToView(view: SKView) { spaceshipSprite.position = CGPoint(x: frame.width / 2, y: frame.height / 2) spaceshipSprite.setScale(0.33) addChild(spaceshipSprite) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { } }
…and when you run it, it should look like this:
If you were to touch the screen, nothing would happen. That’s because there’s no code to respond to touches on the screen…yet! We’ll start by adding code to the touchesBegan
method.
touchesBegan
is called when the user puts a finger on the screen. It provides information about that touch action in its touches<c/ode> argument, a
Set
of touch objects, which are instances of the UITouch
class. One of the properties of UITouch
is
locationInNode
, which is a CGPoint
. Given that sprites have a position
property that is also a CGPoint
, you've probably already guessed what we're about to code.
The default setting for multitouch in an app is “off”. We want to keep things simple for now, so we’ll just go with the default, which means that the app will register only one touch at a time, which in turn means that touchesBegan
‘s touches argument will only contain a single touch object. We’ll retrieve that object from touches
by using Set
‘s first
property.
Try coding it yourself before looking at the code below:
import SpriteKit class GameScene: SKScene { let spaceshipSprite = SKSpriteNode(imageNamed: "Spaceship") override func didMoveToView(view: SKView) { spaceshipSprite.position = CGPoint(x: frame.width / 2, y: frame.height / 2) spaceshipSprite.setScale(0.33) addChild(spaceshipSprite) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first! spaceshipSprite.position = touch.locationInNode(self) } }
If you run the app, you’ll see that the spaceship jumps to wherever you put down your finger:
Making the spaceship go to where you released your finger
You’ll notice that if you hold your finger on the screen and move it about, the ship doesn’t follow your finger. It just stays where you first touched the screen and doesn’t move until you remove your finger and touch some other point.
The name touchesBegan
implies that it has a counterpart method for when the user releases his/her finger from the screen. That method is called touchesEnded
, and it takes the same arguments as touchesBegan
.
Start typing touchesEnded into GameScene
, and Xcode’s autocompletion should make it simple for you to set up a touchesEnded
method. Cut the code from touchesBegan
and paste it into touchesEnded
, like so:
import SpriteKit class GameScene: SKScene { let spaceshipSprite = SKSpriteNode(imageNamed: "Spaceship") override func didMoveToView(view: SKView) { spaceshipSprite.position = CGPoint(x: frame.width / 2, y: frame.height / 2) spaceshipSprite.setScale(0.33) addChild(spaceshipSprite) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first! spaceshipSprite.position = touch.locationInNode(self) } }
Note what happens: the spaceship doesn’t move when you touch the screen until you remove your finger. Only when you release your finger does it move, and it jumps to the location where you released your finger from the screen. Try touching the screen, moving your finger to another location on the screen, and then releasing your finger.
What if we put the same code into both the touchesBegan
and touchesEnded
methods, like this?
import SpriteKit class GameScene: SKScene { let spaceshipSprite = SKSpriteNode(imageNamed: "Spaceship") override func didMoveToView(view: SKView) { spaceshipSprite.position = CGPoint(x: frame.width / 2, y: frame.height / 2) spaceshipSprite.setScale(0.33) addChild(spaceshipSprite) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first! spaceshipSprite.position = touch.locationInNode(self) } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first! spaceshipSprite.position = touch.locationInNode(self) } }
If you run the app, the spaceship now jumps to the location where you made initial contact with the screen, and again to the location where you released your finger.
Making the spaceship follow your finger as you move it
You’re probably wondering if there’s a method that responds to changes that occur between the touchesBegan
and touchesEnded
events, something that lets you track the movements of the user’s finger as it moves on the screen. There is, and it’s called touchesMoved
. It takes the same arguments as touchesBegan
and touchesEnded
, which means that it should respond to the code that we put into those two methods. Copy and paste the code from either touchesBegan
and touchesEnded
and paste it into touchesMoved
. Your code should look like the following:
import SpriteKit class GameScene: SKScene { let spaceshipSprite = SKSpriteNode(imageNamed: "Spaceship") override func didMoveToView(view: SKView) { spaceshipSprite.position = CGPoint(x: frame.width / 2, y: frame.height / 2) spaceshipSprite.setScale(0.33) addChild(spaceshipSprite) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first! spaceshipSprite.position = touch.locationInNode(self) } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first! spaceshipSprite.position = touch.locationInNode(self) } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first! spaceshipSprite.position = touch.locationInNode(self) } }
Run the app. It should look like this:
You’ll find that the spaceship jumps to the location where you touched the screen and as long as you keep your finger on the screen, you can drag it around. When you release your finger from the screen, the ship stays at the location where you released your finger.
Let’s refactor the duplicate code in touchesBegan
, touchesMoved
, and touchesEnded
into its own method, handleTouch
. The code will now look like this:
import SpriteKit class GameScene: SKScene { let spaceshipSprite = SKSpriteNode(imageNamed: "Spaceship") override func didMoveToView(view: SKView) { spaceshipSprite.position = CGPoint(x: frame.width / 2, y: frame.height / 2) spaceshipSprite.setScale(0.33) addChild(spaceshipSprite) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { handleTouch(touches.first!) } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { handleTouch(touches.first!) } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { handleTouch(touches.first!) } func handleTouch(touch: UITouch) { spaceshipSprite.position = touch.locationInNode(self) } }
Experimenting with actions
Let’s combine this article’s lessons with the ones from the last one. Can you create some actions that make the spaceship pulsate as long as the user presses on the screen, as shown below?
Before you look at my solution below, see if you can’t code it yourself first:
import SpriteKit class GameScene: SKScene { let spaceshipSprite = SKSpriteNode(imageNamed: "Spaceship") override func didMoveToView(view: SKView) { spaceshipSprite.position = CGPoint(x: frame.width / 2, y: frame.height / 2) spaceshipSprite.setScale(0.33) addChild(spaceshipSprite) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let expandAction = SKAction.scaleTo(0.5, duration: 0.33) let contractAction = SKAction.scaleTo(0.33, duration: 0.33) let pulsateAction = SKAction.repeatActionForever( SKAction.sequence([expandAction, contractAction])) spaceshipSprite.runAction(pulsateAction) handleTouch(touches.first!) } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { handleTouch(touches.first!) } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { handleTouch(touches.first!) spaceshipSprite.removeAllActions() let restoreScaleAction = SKAction.scaleTo(0.33, duration: 0.1) spaceshipSprite.runAction(restoreScaleAction) } func handleTouch(touch: UITouch) { spaceshipSprite.position = touch.locationInNode(self) } }
You may have noticed that in the touchesEnded
method, the spaceship sprite is restored to its normal scale with an action rather than by simply setting its scale using the setScale
method. I found that simply setting the spaceship back to its normal scale with setScale
a little too abrupt; restoreScaleAction
makes the transition very smooth.
What we just covered
In this article, we covered:
- A recap of:
- Starting a new Game project in Xcode
- Restricting an app’s orientation to landscape-only
- Drawing sprites at a specified location on the screen
- Responding to the event where the user touches the screen with the
touchesBegan
method - Responding to the event where the user stops touching the screen with the
touchesEnded
method - Responding to the event where the user moves his/her finger while touching the screen with the
touchesMoved
method - Combining actions with touch events for interesting visual effects.
In the next article in this series, we’ll look at moving player characters around the screen.