Cocos2D Tutorial for iOS: How To Drag and Drop Sprites

A Cocos2D tutorial on how to drag and drop sprites via touch gestures. By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Moving Sprites and the Layer based on Touches

Time to make these animals move! The basic idea is you'll implement the ccTouchMoved callback, 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 layer in Cocos2D.

Start by taking a look at the image below:

Scrolling layers with Cocos2D

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 Cocos2D layer 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 = [CCDirector sharedDirector].winSize;
    CGPoint retval = newPos;
    retval.x = MIN(retval.x, 0);
    retval.x = MAX(retval.x, -background.contentSize.width+winSize.width); 
    retval.y = self.position.y;
    return retval;
}

- (void)panForTranslation:(CGPoint)translation {    
    if (selSprite) {
        CGPoint newPos = ccpAdd(selSprite.position, translation);
        selSprite.position = newPos;
    } else {
        CGPoint newPos = ccpAdd(self.position, translation);
        self.position = [self boundLayerPos:newPos];      
    }  
}

- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {       
    CGPoint touchLocation = [self convertTouchToNodeSpace:touch];
    
    CGPoint oldTouchLocation = [touch previousLocationInView:touch.view];
    oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation];
    oldTouchLocation = [self convertToNodeSpace:oldTouchLocation];
           
    CGPoint translation = ccpSub(touchLocation, oldTouchLocation);    
    [self panForTranslation:translation];    
}

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) moves the sprite (if there's one selected) based on a passed-in translation, and if not moves the layer itself. This is done by setting the position for the sprite or layer.

The final method (ccTouchMoved) is the callback you get when the user drags their finger. As you did earlier, you convert the touch to layer coordinates, and then you need to get the information about the previous touch as well. There is no helper moethod such as convertTouchToNodeSpace for the previous touch, so here you have to do the steps to convert the touch coordinates manually.

Then it figures out the amount the touch moved by subtracting the current location from the last location, and calls the panForTranslation method you wrote above.

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!

Dragging sprites with touch with Cocos2D

How to Use Gesture Recognizers with Cocos2D

There's another way to accomplish what you just did with Cocos2D touch handling - use gesture recognizers instead!

Gesture recognizers are a relatively new addition to the iOS SDK (introduced in iOS SDK 3.2). And let me tell you, they are awesome.

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 Cocos2D with no troubles. Let's see how that works.

First, go to your init method and comment out the touch registration, since you will be using a different method now:

//[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];

Then, go to DragDropAppDelegate.m and inside applicationDidFinishLaunching, replace the last line in the method with the following:

CCScene *scene = [HelloWorld scene];
HelloWorld *layer = (HelloWorld *) [scene.children objectAtIndex:0];
UIPanGestureRecognizer *gestureRecognizer = [[[UIPanGestureRecognizer alloc] initWithTarget:layer action:@selector(handlePanFrom:)] autorelease];
[viewController.view addGestureRecognizer:gestureRecognizer];

[[CCDirector sharedDirector] runWithScene:scene];	

This code gets a reference to the HelloWorld layer (it knows that it's the only child of the HelloWorld scene), and then creates a UIPanGestureRecognizer. Note that to create a gesture recognizer, you just have to initialize it and pass in where the callback should go to - in this case the handlePanFrom method in the layer.

After creating the gesture recognizer, it adds it to the OpenGL view (viewController.view).

Next, add the following to the bottom of your HelloWorldScene.m:

- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {
    
    if (recognizer.state == UIGestureRecognizerStateBegan) {    
        
        CGPoint touchLocation = [recognizer locationInView:recognizer.view];
        touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
        touchLocation = [self convertToNodeSpace:touchLocation];                
        [self selectSpriteForTouch:touchLocation];
        
    } else if (recognizer.state == UIGestureRecognizerStateChanged) {    
        
        CGPoint translation = [recognizer translationInView:recognizer.view];
        translation = ccp(translation.x, -translation.y);
        [self panForTranslation:translation];
        [recognizer setTranslation:CGPointZero inView:recognizer.view];    
        
    } else if (recognizer.state == UIGestureRecognizerStateEnded) {
        
        if (!selSprite) {         
            float scrollDuration = 0.2;
            CGPoint velocity = [recognizer velocityInView:recognizer.view];
            CGPoint newPos = ccpAdd(self.position, ccpMult(velocity, scrollDuration));
            newPos = [self boundLayerPos:newPos];
            
            [self stopAllActions];
            CCMoveTo *moveTo = [CCMoveTo actionWithDuration:scrollDuration position:newPos];            
            [self runAction:[CCEaseOut actionWithAction:moveTo rate:1]];            
        }        
        
    }        
}

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 approprite 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 selectSpriteForTouch 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 OpenGL 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 layer to slide a bit, so the user can flick quickly to get the layer 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 CCMoveTo action (with CCEaseOut to make it feel a bit better) for a neat effect.

Compile and run your code, and you should now be able to slide and move around, all with gesture recognizers!

Moving sprites with UIGestureRecognizers with Cocos2D