How To Drag and Drop Sprites with Cocos2D

How To Drag and Drop Sprites with Cocos2D

Drag and drop these cute pets with Cocos2D!

I’ve received several requests to write a tutorial on how to move sprites in Cocos2D by dragging them with touches. You asked for it, you got it!

In this 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 mylovely wife, on a background made by gwebstock.

This 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 donwload the images you’ll need for this 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! Compile and run your code, and you should see a couple cute animals sitting there, just begging 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!

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:

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!

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, nd 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!

Where To Go From Here?

Here is a sample project with all of the code from this 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 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! :]

Category: iPad, iPhone

Tags: cocos2D, iOS, iPhone, sample code, tutorial

//

http://www.raywenderlich.com/2343/how-to-drag-and-drop-sprites-with-cocos2d

你可能感兴趣的:(Game,Cocos2D,iOS)