Adding UIGestureRecognizer support in cocos2d

Forums Programming cocos2d support (graphics engine) Adding UIGestureRecognizer support in cocos2d

This topic contains 88 replies, has 35 voices, and was last updated by  jaanicreations 1 year, 2 months ago.

Viewing 25 posts - 1 through 25 (of 89 total)
Author Posts
Author Posts
August 22, 2010 at 8:19 pm #224182

xemus
@xemus

I wanted to share some code I’ve written to allow using UIGestureRecognizer classes with cocos2d. This code has saved me a tremendous amount of time handling touch events, so hopefully it will help some others out I can get some feedback on them in the process. The main goal was to be able to get the gesture recognizers to act as close to original design as possible without a lot of setup work to use them.

This major design change I did to accomplish this was to move touches from CCLayer based to CCNode based, this way can be seen as each node being a view. Handling touches this way gives you a lot of flexibility and control to test for touches on individual objects of a scene or a layer like a sprite instead of just knowing the layer was touched somewhere. This change was only for gesture recognizers, I left the way cocos handles individual touch events intact. My other goal was the gesture recognizers should handle touches across nodes, but a touch on a particular node can only be handled by itself and it’s children, this mimics the way gesture recognizers normally work with subviews.

Note:

Not all code changes are in this post, I made some changes to CCNode such as moving the isTouchEnabled from CCLayer and the code to retain its gesture recognizers and handle the actual attachment to the view. If this post generates enough interest I will post a full patch.

Example initialization:

CCGestureRecognizer* recognizer = [CCGestureRecognizer CCRecognizerWithRecognizerTargetAction:[[[UIRotationGestureRecognizer alloc]init] autorelease] target:self action:@selector(rotate:node:)];

Example usage:

Usage is very straightforward, the callback function that occurs once a gesture is recognized just needs to take a UIGestureRecognizer and a CCNode as a parameter. Normally you can tell what view was touched from the gesture recognizer, but since we only have one view the CCNode that was touched gets passed to the callback function as well.

- (void) rotate:(UIGestureRecognizer*)recognizer node:(CCNode*)node
{
UIRotationGestureRecognizer* rotate = (UIRotationGestureRecognizer*)recognizer;
float r = node.rotation;
node.rotation += CC_RADIANS_TO_DEGREES(rotate.rotation) -r;
}

Here is the source:

CCGestureRecognizer.h

#ifndef __CCGestureRecognizer_H__
#define __CCGestureRecognizer_H__

#import "ccTypes.h"
#import "CCNode.h"
#import <UIKit/UIKit.h>

@class CCNode;

@interface CCGestureRecognizer : NSObject <UIGestureRecognizerDelegate>
{
UIGestureRecognizer* m_gestureRecognizer;
CCNode* m_node;

id<UIGestureRecognizerDelegate> m_delegate;

id m_target;
SEL m_callback;
}

@property(nonatomic,readonly) UIGestureRecognizer* gestureRecognizer;
@property(nonatomic,assign) CCNode* node;
@property(nonatomic,assign) id<UIGestureRecognizerDelegate> delegate;
@property(nonatomic,assign) id target;
@property(nonatomic,assign) SEL callback;

- (id) initWithRecognizerTargetAction:(UIGestureRecognizer*)gestureRecognizer target:(id)target action:(SEL)action;
+ (id) CCRecognizerWithRecognizerTargetAction:(UIGestureRecognizer*)gestureRecognizer target:(id)target action:(SEL)action;

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

// this is the function the gesture recognizer will callback and we will add our CCNode onto it
- (void) callback:(UIGestureRecognizer*)recognizer;
@end

#endif // end of __CCGestureRecognizer_H__

CCGestureRecognizer.m

It looks m_node isn’t retained or released, but when you attach a gesture recognizer to a CCNode the node sets this value and also unsets this value when it is released. Since CCNode keeps track of gesture recognizers attached to it doing a retain would mean both objects would never get released.

#import "CCGestureRecognizer.h"
#import "CCDirector.h"
#import "ccMacros.h"
#import "CGPointExtension.h"

@implementation CCGestureRecognizer

-(void)dealloc
{
[m_delegate release];
[super dealloc];
}

- (UIGestureRecognizer*)gestureRecognizer
{
return m_gestureRecognizer;
}

- (CCNode*)node
{
return m_node;
}

- (void)setNode:(CCNode*)node
{
// we can't retain the node, otherwise a node will never get destroyed since it contains a
// ref to this. if node gets unrefed it will destroy this so all should be good
m_node = node;
}

- (id<UIGestureRecognizerDelegate>)delegate
{
return m_delegate;
}

- (void) setDelegate:(id<UIGestureRecognizerDelegate>)delegate
{
[m_delegate release];
m_delegate = delegate;
[m_delegate retain];
}

- (id)target
{
return m_target;
}

- (void)setTarget:(id)target
{
[m_target release];
m_target = target;
[m_target retain];
}

- (SEL)callback
{
return m_callback;
}

- (void)setCallback:(SEL)callback
{
m_callback = callback;
}

- (id)initWithRecognizerTargetAction:(UIGestureRecognizer*)gestureRecognizer target:(id)target action:(SEL)action
{
if( (self=[super init]) )
{
m_gestureRecognizer = gestureRecognizer;
[m_gestureRecognizer retain];
[m_gestureRecognizer addTarget:self action:@selector(callback:)];

// setup our new delegate
m_delegate = m_gestureRecognizer.delegate;
m_gestureRecognizer.delegate = self;

m_target = target;
[m_target retain];
m_callback = action;
}
return self;
}

+ (id)CCRecognizerWithRecognizerTargetAction:(UIGestureRecognizer*)gestureRecognizer target:(id)target action:(SEL)action
{
[[[self alloc] initWithRecognizerTargetAction:gestureRecognizer target:target action:action] autorelease];
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
CGPoint pt = CCDirector sharedDirector] convertToGL:[touch locationInView: [touch view];

BOOL rslt = [m_node isPointInArea:pt];

if( rslt )
{
// still ok, now check children of parents after this node
CCNode* node = m_node;
CCNode* parent = m_node.parent;
while( node != nil && rslt)
{
CCNode* child;
BOOL nodeFound = NO;
CCARRAY_FOREACH(parent.children, child)
{
if( !nodeFound )
{
if( !nodeFound && node == child )
nodeFound = YES; // we need to keep track of until we hit our node, any past it have a higher z value
continue;
}

if( [child isNodeInTreeTouched:pt] )
{
rslt = NO;
break;
}
}

node = parent;
parent = node.parent;
}
}

if( rslt && m_delegate )
rslt = [m_delegate gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch];

return rslt;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if( !m_delegate )
return YES;
return [m_delegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if( !m_delegate )
return YES;
return [m_delegate gestureRecognizerShouldBegin:gestureRecognizer];
}

- (void)callback:(UIGestureRecognizer*)recognizer
{
if( m_target )
[m_target performSelector:m_callback withObject:recognizer withObject:m_node];
}
@end

Here are some of the changes to CCNode

-(BOOL) isPointInArea:(CGPoint)pt
{
if( !visible_ )
return NO;

/* convert the point to the nodes local coordinate system to make it
easier to compare against the area the node occupies*/
pt = [self convertToNodeSpace:pt];

// we have to take the anchor point into account for checking
CGRect rect;
/* we should be able to use touchableArea here, even if a node doesn't set
this, it will return the contentArea. */
rect.size = self.touchableArea;
CGPoint anchor = anchorPoint_;

// we pretty much need to undo the anchor to get our rect to start at the lower left
anchor.x = 0.5f - anchor.x;
anchor.y = 0.5f - anchor.y;

rect.origin = CGPointMake( -(rect.size.width*anchor.x), -(rect.size.height*anchor.y) );

if( CGRectContainsPoint(rect,pt) )
return YES;
return NO;
}

-(BOOL) isNodeInTreeTouched:(CGPoint)pt
{
if( [self isPointInArea:pt] )
return YES;

BOOL rslt = NO;
CCNode* child;
CCARRAY_FOREACH(children_, child )
{
if( [child isNodeInTreeTouched:pt] )
{
rslt = YES;
break;
}
}
return rslt;
}

-(CGSize) touchableArea
{
// we use content size if touchable area is 0
if( touchableArea_.width != 0.0f &&
touchableArea_.height != 0.0f )
return touchableArea_;
else
return contentSize_;
}

-(void) setTouchableArea:(CGSize)area
{
touchableArea_ = area;
}

August 23, 2010 at 3:26 am #295459

riq
Keymaster
@admin

thanks for sharing it.

August 24, 2010 at 2:47 am #295460

landasia
@landasia

thanks xemus. do you mind posting the full patch? I also am going to try to use gesturerecognizers for my input on my game.

August 24, 2010 at 4:52 am #295461

JonKean
@jonkean

Might consider throwing it on github, as it’s slightly more public and far easier to deal with maintenance

August 24, 2010 at 8:24 am #295462

JamesK
@jamesk

@xemus that’s very cool, thanks for posting this

August 24, 2010 at 2:10 pm #295463

xemus
@xemus

My morning is looking pretty packed, but I will post a full the full patch to github later today.

August 25, 2010 at 5:00 pm #295464

xemus
@xemus

I posted a sample project on github that shows the use of some basic gesture recognizers pinch, tap, pan, rotate. Since CCGestureRecognizer is really a wrapper for UIGestureRecognizer it’s no extra work to use it for long press, swipe or a custom recognizer if you wanted to. If you want to add the functionality to your project the base directory has a patch file called cocos2d.patch that can apply the needed changes to cocos2d. The patch has been tested to work on v0.99.4 and 0v.99.5b and instructions to apply the patch can be found in the README. Here is the github link http://github.com/xemus/cocos2d-GestureRecognizers/archives/master

August 25, 2010 at 6:10 pm #295465

eshirt
Participant
@eshirt

xemus – this is fantastic as I was looking to do something just like this!

However, I’m still hesitant to cut out iOS 3.1.x users so I was wondering if what you’ve put together would work with conditional statements to allow 3.1 touch events to work if UIGestureRecognizer is not available?

Or do your changes to CCNode override the use of ccTouchesBegan:, et al?

August 25, 2010 at 7:17 pm #295466

xemus
@xemus

Conditional checks for them shouldn’t be an issue. I was able to mix the use of my code with ccTouchBegan, ccTouchEnded and everything functioned as expected.

September 9, 2010 at 7:16 pm #295467

csayers112
Participant
@csayers112

How about recognizing Swipe Left and Right? I keep getting an error with the direction property. Anyone having the same problem??

September 9, 2010 at 9:20 pm #295468

xemus
@xemus

Can you post a sample of your gesture recognizer setup code and I can take a look at it later.

September 10, 2010 at 12:31 am #295469

csayers112
Participant
@csayers112

Thanks so much!! This is really awesome, thanks for posting!!!

Here is gesture recognizer setup,

//Setup

CCGestureRecognizer* recognizer;

recognizer = [CCGestureRecognizer CCRecognizerWithRecognizerTargetAction:[[[UISwipeGestureRecognizer alloc]init] autorelease] target:self action:@selector(swipe:node:)];
recognizer.direction = UISwipeGestureRecognizerDirectionLeft;
[node addGestureRecognizer:recognizer];

//Method

- (void) swipe:(UISwipeGestureRecognizer*)recognizer node:(CCNode*)node
{
if (recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
NSLog(@"Left Swipe");
}
else {
NSLog(@"Right Swipe");
}
}

I got how to use the direction property from here,

http://developer.apple.com/library/ios/#samplecode/SimpleGestureRecognizers/Introduction/Intro.html%23//apple_ref/doc/uid/DTS40009460

Any help that you can provide would be awesome. Thanks again!

September 12, 2010 at 4:07 am #295470

xemus
@xemus

CCGestureRecognizer doesn’t inherit from any of the UIGestureRecognizer classes, so you would need to use your direction setting code on the recognizer returned by the gestuerRecognizer property of the CCGestureRecognizer class. This setup code should get you what you need.

CCGestureRecognizer* recognizer;
recognizer = [CCGestureRecognizer CCRecognizerWithRecognizerTargetAction:[[[UISwipeGestureRecognizer alloc]init] autorelease] target:self action:@selector(swipe:node:)];
((UISwipeGestureRecognizer*)recognizer.gestureRecognizer).direction = UISwipeGestureRecognizerDirectionLeft;
[node addGestureRecognizer:recognizer];

September 16, 2010 at 6:47 pm #295471

csayers112
Participant
@csayers112

That did it. You the man!!

September 22, 2010 at 1:01 pm #295472

WWWeb-ster
@wwweb-ster

I installed the patch following the readme, but now I have hundreds of ‘duplicate declaration’ type errors? Has anyone else had this? Has anyone solved it please?

September 22, 2010 at 2:03 pm #295473

WWWeb-ster
@wwweb-ster

Solution to my problem:

I’m unsure if it’s an error in the patch script, but I ended up with several copies of the CCGestureRecognizer interface in the .h.

I had to change the permissions on the file and remove the extra copies.

The file I got was downloaded from the provided link and I selected the .zip as a matter of interest.

September 28, 2010 at 1:58 pm #295474

fringley
Participant
@fringley

Is there a patch available for the 0.99.5-beta3 ?

September 29, 2010 at 11:02 am #295475

fringley
Participant
@fringley

Got this working with 0.99.5-beta3, Gestures are working really well (much more reliably than my old code!) however, I’m now having difficulties having a CCMenu absorbing clicks, i.e. my TapGesture is firing before the click on the menu item meaning that I can never select the menu item.

September 29, 2010 at 1:31 pm #295476

xemus
@xemus

I tried to closely follow the way apple had them working so it should be that a parent and child can receive the same touch, but not 2 different children. If your tap gesture recognizer is on the main layer, then both it and the menu would receive the touch. I think what you can do to get around this is make sure the menu has touches enabled and also give it a touchableArea. If your touch ends up being in that touchable area the nodes that are behind that point should not hit pickup the touch.

September 29, 2010 at 1:42 pm #295477

xemus
@xemus

@WWWeb-ster

I tried running the patch a few times and the only time I had the issue you’re describing was when I accidentally applied the patch after it had already been applied before.

September 29, 2010 at 1:46 pm #295478

fringley
Participant
@fringley

Righto. My menu does have touches enabled (i believe), but how would I go about setting up a touchable area? Would this be around the whole menu – effectively absorbing all the touches within that area?

September 29, 2010 at 2:17 pm #295479

xemus
@xemus

Yes, just set up a touchable area around the entire menu, this should keep anything behind it from getting those touches. It should just be like this..

menu.touchableArea = CGSizeMake(75.0f,100.0f);

September 29, 2010 at 2:26 pm #295480

fringley
Participant
@fringley

Just gave that a go – no joy :( Any other ideas?

Is there a way to to stop touch events being sent to the gesture recogniser – i.e. if a CCMenuItem responds to a touch, there’s no reason to propagate the touch to the recogniser maybe?

Maybe something in the CCTouchDispatcher, the MenuItem correctly receives the touch and “claims” it – dont want the recogniser to do anything if the touch is claimed?

September 29, 2010 at 2:43 pm #295481

cjkeeme
Participant
@cjkeeme

I would like to get touch gestures to work so I can turn pages in a book with left and right swipe gestures. Each page in the book is a scene. Can someone help me out with the how the setup and the method code will look like?

September 29, 2010 at 3:02 pm #295482

fringley
Participant
@fringley

OK, I might be grasping at straws here.

I’m looking at gestureRecognizer:shouldReceiveTouch: in CCGestureRecognizer and it never gets to line 117 where [child isNodeInTreeTouched:pt] should return true for my node that should absorb the touch event, meaning that the gesture recogniser on line 130 is *always* called.

EDIT: I’ve tried playing with z-orders. If I have a menu layer and a swipe layer, the swipes only work if it is the top layer. So, I think it is better if there is one layer with the gestures applied to the layer and the menu added as child of the layer – but then I have the problem mentioned above.

EDIT: Maybe the code that iterates over the children is having difficulty with my swipe gestures being applied to the layer, where as the “children” of the layer is only the menu. Do my swipe gestures need to be applied to a child of the same layer??

Viewing 25 posts - 1 through 25 (of 89 total)

You must be logged in to reply to this topic.