Cocos2D Tutorial for iOS: How To Drag and Drop Sprites

Ray Wenderlich

This post is also available in: Chinese (Simplified), Japanese

Drag and drop these cute pets with Cocos2D!

Drag and drop these cute pets with Cocos2D!

I’ve received several requests to write a Cocos2D tutorial on how to drag and drop sprites in Cocos2D by touch gestures. You asked for it, you got it!

In this Cocos2D tutorial, you’re going to learn:

  • The basics of dragging and dropping sprites with touches
  • How to scroll the view itself via touches
  • How to keep coordinates straight in your head
  • How to use gesture recognizers with Cocos2D for even more cool effects!

To make things fun, you’ll be moving some cute animals around the scene drawn by my lovely wife, on a background made by gwebstock.

This Cocos2D tutorial assumes you have at least basic knowledge of Cocos2D and have the Cocos2D templates already installed. If you are new to Cocos2D, you may wish to try out the How To Make A Simple iPhone Game with Cocos2D tutorial first.

So without further ado, drag your fingers over to the keyboard and let’s get started!

Getting Started

Before you implement the touch handling, first you’ll create a basic Cocos2D scene displaying the sprites and artwork.

Open up XCode, go to File\New Project, choose User Templates\cocos2d X.X.X\cocos2d Application, and click “Choose…“. Name the project “DragDrop“, and click Save.

Next, go ahead and download the images you’ll need for this Cocos2D tutorial. Once you download and unzip the file, drag all of the images into the Resources group in your project. Verify that “Copy items into destination group’s folder (if needed)” is checked, and click Add.

Once you’ve added the images to your project, open the Classes group in XCode, and select HelloWorld.h. Inside the @interface declaration, declare three instance variables you’ll need like the following:

CCSprite * background;
CCSprite * selSprite;
NSMutableArray * movableSprites;

You’ll be using these later on to keep track of the background image, the currently selected sprite, and the list of sprites that should be moved with touches.

Now switch to HelloWorldScene.m. Find the init method, and replace it with the following:

-(id) init {
    if((self = [super init])) {		
        CGSize winSize = [CCDirector sharedDirector].winSize;
 
        [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGB565];
        background = [CCSprite spriteWithFile:@"blue-shooting-stars.png"];
        background.anchorPoint = ccp(0,0);
        [self addChild:background];
        [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_Default];
 
        movableSprites = [[NSMutableArray alloc] init];
        NSArray *images = [NSArray arrayWithObjects:@"bird.png", @"cat.png", @"dog.png", @"turtle.png", nil];       
        for(int i = 0; i < images.count; ++i) {
            NSString *image = [images objectAtIndex:i];
            CCSprite *sprite = [CCSprite spriteWithFile:image];
            float offsetFraction = ((float)(i+1))/(images.count+1);
            sprite.position = ccp(winSize.width*offsetFraction, winSize.height/2);
            [self addChild:sprite];
            [movableSprites addObject:sprite];
        }     		
    }
    return self;
}

There’s some new stuff in here, so let’s go over it step by step.

Loading the Background

The first part of the method loads the background image for the scene (blue-shooting-stars.png). Note that it sets the anchor point of the image to the lower left of the image (0, 0).

In Cocos2D, when you set the posiiton of a sprite, you are actually setting where the anchor point of the image is. By default, the anchor point is set to the exact center of the image. However, by setting the anchor point to the lower left corner, when you set the position of the sprite, you are now setting where the lower left corner is.

The method doesn’t set the position of the background anywhere, so the position of the background defaults to (0,0). Hence, the lower left corner of time image is located at (0,0), and so the image (which is about 800 points long) extends off screen to the right.

Another thing to note about loading the background image is it switches the pixel format before it loads the image. By default, when you load images with Cocos2D, they are loaded as 32-bit images. This means each pixel takes up 4 bytes in memory. This is good when you want very high quality artwork, but the tradeoff is it takes a large amount of memory to load the images – a premium on mobile devices.

When you’re loading large images (such as background images), it’s often good practice to load the background images as 16-bit images instead – basically sacrificing quality for lower memory usage. There are several different texture formats available in Cocos2D – here you choose the best possible level of quality for 16-bit images that don’t need any transparency effects.

Loading the Images

The next part of the method loops through the list of images to load and creates and places a sprite for each. The sprites are distributed along the length of the screen, to have a nice initial layout. It also puts a reference to each sprite in the array of movable sprites, which will be used later.

Speaking of which, one last step – find your dealloc method and add the following lines to release the array:

[movableSprites release];
movableSprites = nil;

That’s it! Build and run your code, and you should see a couple cute animals sitting there, just begging to be touched!

Sprites just waiting to be touched!

Selecting Sprites based on Touches

Now you’ll write the code to determine which sprite is selected based on the user’s current touch.

The first step is to enable your HelloWorldLayer to receive touches. Add the following line to the end of your init method to do so:

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

Note that this is a relatively new way to enable touches on your layer – the old way was to set the isTouchEnabled property to yes and implement ccTouchesBegan. If you are curious as to the advantages of this new method over the old method, check out the How To Make a Tile Based Game with Cocos2D Tutorial for more information.

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

- (void)selectSpriteForTouch:(CGPoint)touchLocation {
    CCSprite * newSprite = nil;
    for (CCSprite *sprite in movableSprites) {
        if (CGRectContainsPoint(sprite.boundingBox, touchLocation)) {            
            newSprite = sprite;
            break;
        }
    }    
    if (newSprite != selSprite) {
        [selSprite stopAllActions];
        [selSprite runAction:[CCRotateTo actionWithDuration:0.1 angle:0]];
        CCRotateTo * rotLeft = [CCRotateBy actionWithDuration:0.1 angle:-4.0];
        CCRotateTo * rotCenter = [CCRotateBy actionWithDuration:0.1 angle:0.0];
        CCRotateTo * rotRight = [CCRotateBy actionWithDuration:0.1 angle:4.0];
        CCSequence * rotSeq = [CCSequence actions:rotLeft, rotCenter, rotRight, rotCenter, nil];
        [newSprite runAction:[CCRepeatForever actionWithAction:rotSeq]];            
        selSprite = newSprite;
    }
}
 
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {    
    CGPoint touchLocation = [self convertTouchToNodeSpace:touch];
    [self selectSpriteForTouch:touchLocation];      
    return TRUE;    
}

The first method (selectSpriteForTouch) is a helper method that loops through all of the sprites in the movableSprites array, looking for the first sprite that the touch intersects.

Note that CCNode has a helper property called boundingBox that returns the bounding box around the sprite. This is generally better to use than computing the rectangle yourself, because a) it’s easier, and b) it takes the transform of the sprite into consideration.

After it finds a matching sprite (if any), it runs a little animation on the sprite so the user knows which they have selected. It first cancels any animation running on the previously selected sprite, then uses a sequence of CCActions to make the sprite appear to “wiggle” back and forth (much like the iOS home screen when rearranging/deleting apps).

Finally, the ccTouchBegan method calls the above method based on the user’s touch. Note that it has to convert the touch location from UIView coordinates to layer (node space) coordinates. To do this, it calls a helper method in CCNode called convertTouchToNodeSpace. This method does three things:

  1. Figures out the location of the touch in the touch’s view (via locationInView)
  2. Converts the touch coordinates to OpenGL coordinates (via convertToGL)
  3. Converts the Open GL world coordinates to coordinates in a specific node (this layer, via convertToNodeSpace)

This is a common conversion to do, so using this method saves some time.

Compile and run your code, and you should now be able to click on the animals, and when you tap them they should wiggle in a particularly cute way to show that they are selected!

Sprites wiggling indicating selection

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

Where To Go From Here?

Here is a sample project with all of the code from this Cocos2D tutorial.

At this point you should know how to move sprites using touches in your Cocos2D apps and games, and should know the basics of using gesture recognizers with Cocos2D.

From here, you could try extending this project with other gesture recognizers, such as perhaps pinch or rotate gesture recognizers. Maybe you can make the cat grow!

Thank you to all of those who suggested making this Cocos2D tutorial, hopefully this is useful for you guys. The more requests I see on the topic, the more likely I am to write it, so keep the requests coming! :]

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

96 Comments

[ 1 , 2 , 3 , 4 , 5 , 6 , 7 ]
  • Hi Ray,

    I know your Cocos2D drag and drop tutorial was created a while back, but I'd be overjoyed if you'd take a crack at this challenge.

    How do you drag and drop transparent sprites (so it ignores the transparent part) . You'd think this would be an easy one, but I have yet to find anyone online anywhere who has created a simple tutorial that clearly shows how to deal with transparency and cocos2d.

    How would you take your drag and drop example and update it to make it work for the new version of Cocos2D for iOS6 or higher with transparency. If you were able to shed some light or update your example to tackle this, in my opinion, you'd be a true hero!

    Thanks for all your great work!
    rhart
  • Hi rhart,

    I've used Ray's tutorial and have modified it for use on cocos2d v2.x and iOS 6. Can you please provide more detail on "drag and drop transparent sprites (so it ignores the transparent part)"? Are the sprites transparent .pngs or are you using the opacity property? Can you post the code that you are using to create these transparent sprites ?
    CocosKat
  • Hi CocosKat,

    I have a PNG image with transparency that I put on the layer by creating it using spriteWithFile and then doing and addChild to my layer.

    I then have code in ccTouchesBegan and ccTouchesMoved that allow me to drag the image around the screen. I have all this working no problem.

    The problem I'm having is that I don't want to initiate a drag or selection of a sprite if I'm clicking on the transparent part of the sprite.

    I did quite a bit of research on ccrendertecture and glreadpixels and I think this is part of the solution, but I just can seem to get this to work. In iOS 6 they updated how glreadpixels works and the examples I found online don't seem to work now either.

    I've been trying things like
    http://www.cocos2d-iphone.org/forums/to ... f-a-point/
    http://stackoverflow.com/questions/1088 ... rs-sprites
    http://www.cocos2d-iphone.org/forums/to ... age-pixel/

    I have a background image and several other draggable sprites on the layer too. This is also a universal app, so iPhones, iPad and retina and non retina. Not sure if scale factor issues my be an issue here too.

    It's hard to believe detecting the transparency of a particular part of the sprite at a touch point is so hard, but apparently it's quite tricky.

    Any help or advice you can give would be greatly appreciated. Thanks!
    rhart2
  • there are a lot of litle things which change with cocos2d 2.2 and xcode 4.6, which make some issues, however you can fixe them.
    in part "How to Use Gesture Recognizers with Cocos2D"
    instead of go in DragDropAppDelegate.m, you must go in introLayer.m, and replace onEnter method by this:

    -(void) onEnter
    {
    [super onEnter];
    CCScene *scene = [HelloWorldLayer scene];
    HelloWorldLayer *layer = (HelloWorldLayer*)[scene.children objectAtIndex:0];

    UIPanGestureRecognizer *gestureRecognizer = [[[UIPanGestureRecognizer alloc] initWithTarget:layer action:@selector(handlePanFrom:)] autorelease];
    [[CCDirector sharedDirector].view addGestureRecognizer:gestureRecognizer];

    [[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0 scene:scene]];
    }
    anis
  • Ok, rhart2 I think I understand what you're trying to do. I'm wondering if you can achieve that by manipulating the sprite's bounding box instead. For example, in my implementation for drag/drop I use this statement:
    Code: Select all
    (CGRectContainsPoint(sprite.boundingBox, touchLocation))

    If you trim your sprite's bounding box to be only the NON-transparent section of the .png then perhaps you could effectively initiate a drag of the sprite on only the NON-transparent part of the sprite. Hope this helps.
    CocosKat
  • Hello everyone. I'm fairly new with cocos2d and objectiveC. I know this tutorial was done a long while ago, but was trying to learn the concepts in it. I'm having trouble making the background scroll. I tried both methods (the programmed one and the gesture one) and both don't work in the same manner - the background disappears when trying to scroll it. I'm using the newer cocos2d that has introlayer. Is that what may be causing the issue? Also using xcode4. Any thoughts would be greatly appreciated. Thanks!!

    And thanks a ton Ray for the tutorials!
    aigo
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Dani Arnaout

... 50 total!

Update Team

... 14 total!

Editorial Team

  • John Clem

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Zihan Xu
  • Miguel Angel

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!