Sprite Kit Tutorial: How To Drag and Drop Sprites
Learn how to drag and drop sprites (with or without gesture recognizers) in this Sprite Kit tutorial for beginners! By Jean-Pierre Distler.
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: How To 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.
Before you add any code though, let's take a minute to discuss 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).
So 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 spot.
Now that you're armed with this background information, let's see what it looks like in code! Add the following new methods to the bottom of your file:
- (CGPoint)boundLayerPos:(CGPoint)newPos {
CGSize winSize = self.size;
CGPoint retval = newPos;
retval.x = MIN(retval.x, 0);
retval.x = MAX(retval.x, -[_background size].width+ winSize.width);
retval.y = [self position].y;
return retval;
}
- (void)panForTranslation:(CGPoint)translation {
CGPoint position = [_selectedNode position];
if([[_selectedNode name] isEqualToString:kAnimalNodeName]) {
[_selectedNode setPosition:CGPointMake(position.x + translation.x, position.y + translation.y)];
} else {
CGPoint newPos = CGPointMake(position.x + translation.x, position.y + translation.y);
[_background setPosition:[self boundLayerPos:newPos]];
}
}
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 where you’d like to move the layer, and it modifies what you pass in to make sure you don’t scroll too far.
If you have any troubles understanding what’s going on here, consult the picture above and draw it out on paper.
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 set‘s also the position but calls boundLayerPos: to make sure that you cannot scroll to far to the left or right.
Now you can implement touchesMoved:withEvent:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
CGPoint previousPosition = [touch previousLocationInNode:self];
CGPoint translation = CGPointMake(positionInScene.x - previousPosition.x, positionInScene.y - previousPosition.y);
[self panForTranslation:translation];
}
Like you did in touchesBegan:withEvent: you get the touch and convert it‘s position to the position in your scene. To calculate the translation, thus how far you're finger moved on the screen. You need 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.
Give it a shot - compile and run your code, and you should now be able to move the sprites (and the layer!) around 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.
Basically, instead of having to write a bunch of crazy looking code to detect the difference between taps, double taps, swipes, pans, or pinches, you simply create a gesture recognizer object for what you want to detect, and add it to the view. It will then give you a callback when that occurs!
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.
Then add the following method to your scene:
- (void)didMoveToView:(SKView *)view {
UIPanGestureRecognizer *gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget: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 target and handlePanFrom:
as callback. Next you add the gesture recognizer to your scenes presenting view.
Note: You may ask yourself why recognizer is added here and not in the init method of your scene. The answer is simple. SKScene has a property view that holds the SKView that is presenting the scene, but unfortunately this property is first set when the scene is presented on the screen. So the property is nil when your init method gets called. didMoveToView:
is like viewDidAppear:
from UIKit and gets called after your scene was presented.
Note: You may ask yourself why recognizer is added here and not in the init method of your scene. The answer is simple. SKScene has a property view that holds the SKView that is presenting the scene, but unfortunately this property is first set when the scene is presented on the screen. So the property is nil when your init method gets called. didMoveToView:
is like viewDidAppear:
from UIKit and gets called after your scene was presented.
Next, add the following to the bottom of your MyScene.m:
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [self convertPointFromView:touchLocation];
[self selectNodeForTouch:touchLocation];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(translation.x, -translation.y);
[self panForTranslation:translation];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
if (![[_selectedNode name] isEqualToString:kAnimalNodeName]) {
float scrollDuration = 0.2;
CGPoint velocity = [recognizer velocityInView:recognizer.view];
CGPoint pos = [_selectedNode position];
CGPoint p = mult(velocity, scrollDuration);
CGPoint newPos = CGPointMake(pos.x + p.x, pos.y + p.y);
newPos = [self boundLayerPos:newPos];
[_selectedNode removeAllActions];
SKAction *moveTo = [SKAction moveTo:newPos duration:scrollDuration];
[moveTo setTimingMode:SKActionTimingEaseOut];
[_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.
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 table views.
So this section contains a bit of code to calculate a point to move based on the velocity, and running a moveTo action (with SKActionTimingEaseOut to make it feel a bit better) for a neat effect.
Before you can Build & Run your code you have to add a last helper function. Add the following function to the bottom:
CGPoint mult(const CGPoint v, const CGFloat s) {
return CGPointMake(v.x*s, v.y*s);
}
This function just multiplies your velocity with the scroll duration.
Build & Run your code, and you should now be able to slide and move around, all with gesture recognizers!