Sprite Kit Tutorial: Drag and Drop Sprites
Learn how to drag and drop sprites, both with and without gesture recognizers, in this Sprite Kit tutorial for beginners! By Riccardo D'Antoni.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sprite Kit Tutorial: Drag and Drop Sprites
20 mins
Moving Sprites and the Layer based on Touches
Time to make these animals move! The basic idea is you'll implement touchesMoved(_:withEvent:)
, and figure out how much the touch has moved since last time. If an animal is selected, it will move the animal by that amount. If an animal is not selected, it will move the entire layer instead, so that the user can scroll the layer from left to right.
To understand how you can scroll a node in Sprite Kit, start by taking a look at the image below:
As you can see, you've set up the background so the anchor point (the lower left) is at (0, 0), and the rest extends off to the right. The black area indicates the current visible area (the size of the window).
If you want to scroll the image 100 points to the right, you can do that by moving the entire Sprite Kit node 100 points to the left, as you can see in the second image.
You also want to make sure you don't scroll too far. For example, you shouldn't be able to move the layer to the right, since there would be a blank space where the background doesn't cover.
Now that you're armed with this background information, let's see what it looks like in code! Still in GameScene.swift, add the following new methods to the class:
func boundLayerPos(aNewPosition: CGPoint) -> CGPoint {
let winSize = self.size
var retval = aNewPosition
retval.x = CGFloat(min(retval.x, 0))
retval.x = CGFloat(max(retval.x, -(background.size.width) + winSize.width))
retval.y = self.position.y
return retval
}
func panForTranslation(translation: CGPoint) {
let position = selectedNode.position
if selectedNode.name! == kAnimalNodeName {
selectedNode.position = CGPoint(x: position.x + translation.x, y: position.y + translation.y)
} else {
let aNewPosition = CGPoint(x: position.x + translation.x, y: position.y + translation.y)
background.position = self.boundLayerPos(aNewPosition)
}
}
The first method boundLayerPos(_:)
is used for making sure you don’t scroll the layer beyond the bounds of the background image. You pass in the coordinates of where you’d like to move the layer, and it returns a possibly modified point to make sure you don’t scroll too far.
The next method panForTranslation(_:)
first checks if selectedNode
is an animal node and sets the position based on a passed-in translation. If the selected node is the background layer it sets calls boundLayerPos(_:)
in addition to setting the position to make sure that you cannot scroll too far to the left or right.
Now you can implement touchesMoved(_:withEvent:)
to start handling pans:
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch
let positionInScene = touch.locationInNode(self)
let previousPosition = touch.previousLocationInNode(self)
let translation = CGPoint(x: positionInScene.x - previousPosition.x, y: positionInScene.y - previousPosition.y)
panForTranslation(translation)
}
Like you did in touchesBegan(_:withEvent:)
you first get the touch and convert its position to the position in your scene. To calculate the translation, or how far you've dragged your finger on the screen, you need to start with the previous location of the touch.
With the current and previous location you create the translation by subtracting the current location from the last one. Finally you call panForTransaltion(_:)
with the calculated translation to handle the scrolling.
Give it a shot - build and run your code, and you should now be able to move the sprites and the layer by dragging!
How to Use Gesture Recognizers with Sprite Kit
There's another way to accomplish what you just did with Sprite Kit touch handling - use gesture recognizers instead!
Gesture recognizers are a great way to detect different gestures like taps, double taps, swipes or pans. Instead of implementing the touch-handling methods yourself and try to distinguish between taps, double taps, swipes, pans and pinches, you simply create a gesture recognizer object for what you want to detect, and add it to the view.
They are extremely easy to use, and you can use them with Sprite Kit with no troubles. Let's see how that works.
First, comment out the touch handling methods, touchesBegan(_:withEvent:)
and touchesMoved(_:withEvent:)
since you will be using a different method now.
Next, add the following method to the class:
override func didMoveToView(view: SKView) {
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("handlePanFrom:"))
self.view!.addGestureRecognizer(gestureRecognizer)
}
This method gets called when the scene is first presented. Here, you create a pan gesture recognizer and initialize it with your scene as the target and handlePanFrom:
as the callback method. Finally, you add the gesture recognizer to your scene's presenting view.
Note: You may ask yourself why recognizer is added here and not in the initializer. SKScene has a view
property that holds the SKView that is presenting the scene, but unfortunately this property is first set when the scene is presented on the screen. That means the property is nil while the object is initializing. didMoveToView(_:)
is like viewDidAppear(_:)
in UIKit views, and gets called after your scene is presented.
Note: You may ask yourself why recognizer is added here and not in the initializer. SKScene has a view
property that holds the SKView that is presenting the scene, but unfortunately this property is first set when the scene is presented on the screen. That means the property is nil while the object is initializing. didMoveToView(_:)
is like viewDidAppear(_:)
in UIKit views, and gets called after your scene is presented.
Next, add the following to the class:
func handlePanFrom(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .Began {
var touchLocation = recognizer.locationInView(recognizer.view)
touchLocation = self.convertPointFromView(touchLocation)
self.selectNodeForTouch(touchLocation)
} else if recognizer.state == .Changed {
var translation = recognizer.translationInView(recognizer.view!)
translation = CGPoint(x: translation.x, y: -translation.y)
self.panForTranslation(translation)
recognizer.setTranslation(CGPointZero, inView: recognizer.view)
} else if recognizer.state == .Ended {
if selectedNode.name != kAnimalNodeName {
let scrollDuration = 0.2
let velocity = recognizer.velocityInView(recognizer.view)
let pos = selectedNode.position
// This just multiplies your velocity with the scroll duration.
let p = CGPoint(x: velocity.x * CGFloat(scrollDuration), y: velocity.y * CGFloat(scrollDuration))
var newPos = CGPoint(x: pos.x + p.x, y: pos.y + p.y)
newPos = self.boundLayerPos(newPos)
selectedNode.removeAllActions()
let moveTo = SKAction.moveTo(newPos, duration: scrollDuration)
moveTo.timingMode = .EaseOut
selectedNode.runAction(moveTo)
}
}
}
This callback gets called when the pan gesture begins, changes (i.e the user continues to drag), and ends. The method switches on each case, and does the appropriate action.
When the gesture begins, it converts the coordinates to node coordinates (note it has to do it the long way because there's no shortcut method), and calls the selectNodeForTouch(_:)
helper you wrote earlier to select a node.
When the gesture changes, it needs to figure out the amount the gesture moved. One of the nice things about gesture recognizers it actually stores for you the cumulative translation for the gesture so far! However, you have to reverse the y coordinate to take into effect the difference between UIKit coordinates and Sprite Kit coordinates.
After panning for the translation, it resets the translation on the recognizer to zero, because otherwise the translation is cumulative, and you just want the difference each time.
When the gesture ends, there’s some new and interesting code in here! Another cool thing a UIPanGestureRecognizer gives you is the velocity of the pan movement. You can use this to animate the background node to slide a bit, so the user can flick quickly to get the background to slide a bit, like you’re used to seeing in scroll views. Based on the velocity, you run a move action with easing to make it feel more natural.
Build and run the project, and you should now be able to slide and move around, all with gesture recognizers!
Remember to try the velocity-powered movement for the background – try a small flick-like swipe from left to right on the background and you'll see it continue to scroll a bit after you lift your finger.