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;
}