11
votes

Hey all. I just started looking into the cocos2d library. I've heard that it's an easy library to get into if you're used to programming in ActionScript and I've found that a lot of the concepts are indeed similar.

I started looking through sample projects (the sample games linked here were especially helpful) and I saw that handling of touches usually isn't done in a CCSprite. Rather, the CCLayer that instantiates CCSprites reacts to a touch event and iterates through the sprites it created to detect which CCSprite was touched (if any).

I want CCSprites to handle whether they've been touched themselves, and call up to notify that it's been touched (if needed). The Paddle class found under /tests/TouchesTest does just this - it handles touches by itself.

So, the question I have is: What's considered best practice for this? Is it better to have touches handled in a central location and iterate through children to see what's been touched? Or should each child handle its own touch events? Or does it not matter?

I'd prefer each child handling its own touch events but I'd like to follow best practices on this (if they exist). Thanks!

3
I guess I should add about best practices in general when it comes to something like this (i.e., should we be calling schedule in the parent or have each child handle its own).donkim

3 Answers

17
votes

I think it is a matter of preference but I like to have the sprite detect if it was touched by subclassing CCSprite. I make a getter method in my CCSprite subclass that retrieves the state variable from the subclass and then the main program can act accordingly.

Here is an example header file for my CCSprite subclass "spuButton":

    #import "cocos2d.h"

    typedef enum tagButtonState {
        kButtonStatePressed,
        kButtonStateNotPressed
    } ButtonState;

    typedef enum tagButtonStatus {
        kButtonStatusEnabled,
        kButtonStatusDisabled
    } ButtonStatus;

    @interface spuButton : CCSprite <CCTargetedTouchDelegate> {
    @private
        ButtonState state;
        CCTexture2D *buttonNormal;
        CCTexture2D *buttonLit;
        ButtonStatus buttonStatus;

    }

    @property(nonatomic, readonly) CGRect rect;

    + (id)spuButtonWithTexture:(CCTexture2D *)normalTexture;

    - (void)setNormalTexture:(CCTexture2D *)normalTexture;
    - (void)setLitTexture:(CCTexture2D *)litTexture;
    - (BOOL)isPressed;
    - (BOOL)isNotPressed;

    @end

and here is an example of the .m file:

    #import "spuButton.h"
    #import "cocos2d.h"

    @implementation spuButton

    - (CGRect)rect
    {
        CGSize s = [self.texture contentSize];
        return CGRectMake(-s.width / 2, -s.height / 2, s.width, s.height);
    }

    + (id)spuButtonWithTexture:(CCTexture2D *)normalTexture
    {
        return [[[self alloc] initWithTexture:normalTexture] autorelease];
    }

    - (void)setNormalTexture:(CCTexture2D *)normalTexture {
        buttonNormal = normalTexture;
    }
    - (void)setLitTexture:(CCTexture2D *)litTexture {
        buttonLit = litTexture;
    }

    - (BOOL)isPressed {
        if (state == kButtonStateNotPressed) return NO;
        if (state == kButtonStatePressed) return YES;
        return NO;
    }

    - (BOOL)isNotPressed {
        if (state == kButtonStateNotPressed) return YES;
        if (state == kButtonStatePressed) return NO;
        return YES;
    }

    - (id)initWithTexture:(CCTexture2D *)aTexture
    {
        if ((self = [super initWithTexture:aTexture]) ) {

            state = kButtonStateNotPressed;
        }

        return self;
    }

    - (void)onEnter
    {
        [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
        [super onEnter];
    }

    - (void)onExit
    {
        [[CCTouchDispatcher sharedDispatcher] removeDelegate:self];
        [super onExit];
    }   

    - (BOOL)containsTouchLocation:(UITouch *)touch
    {
        return CGRectContainsPoint(self.rect, [self convertTouchToNodeSpaceAR:touch]);
    }

    - (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
    {
        if (state == kButtonStatePressed) return NO;
        if ( ![self containsTouchLocation:touch] ) return NO;
        if (buttonStatus == kButtonStatusDisabled) return NO;

        state = kButtonStatePressed;
        [self setTexture:buttonLit];

        return YES;
    }

    - (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
    {
        // If it weren't for the TouchDispatcher, you would need to keep a reference
        // to the touch from touchBegan and check that the current touch is the same
        // as that one.
        // Actually, it would be even more complicated since in the Cocos dispatcher
        // you get NSSets instead of 1 UITouch, so you'd need to loop through the set
        // in each touchXXX method.

        if ([self containsTouchLocation:touch]) return;
        //if (buttonStatus == kButtonStatusDisabled) return NO;

        state = kButtonStateNotPressed;
        [self setTexture:buttonNormal];

    }

    - (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
    {

        state = kButtonStateNotPressed;
        [self setTexture:buttonNormal];


    }

@end

Hope this helps and Happy coding!

ADDITION of tick method and explanation (for Stephan's question below):

To check the status of the buttons I have a tick: method that basically fires every frame and checks the status of all my buttons.

    -(void)tick:(ccTime)dt {

do my button checks here....

}

I check the status of my buttons by calling a isPressed or isNotPressed function that is part of my spuButton class.

for (spuButton *aButton in _fourButtonsArray) {
     if ([aButton isNotPressed]) continue; //this button is not pressed
     .....otherwise record that it is pressed.....
}

Then I do the same kind of check to see if it has been released and respond accordingly. I do it this way because I want to be able to react to multiple button press combos plus I want to do something when it gets pressed down and then something else when it gets released. I use the ccTouchBegan and ccTouchEnded to change the textures(the sprite image) and to change the state variable accordingly.

0
votes

just adding to this thread. Mark also provides an examples how to instantiate the spuButton which is helpful:

Problem with cocos2d and orientation changes, textures are deformed

you can also go in modify this example to pass in both normal and lit button images like so:

+ (id)spuButtonWithTexture:(CCTexture2D *)normalTexture lit:(CCTexture2D *)litTexture

and then do the same to:

- (id)initWithTexture:(CCTexture2D *)normalTexture lit:(CCTexture2D *)litTexture

and within this method, you can set both textures:

[self setNormalTexture:normalTexture];
[self setLitTexture:litTexture];
0
votes

Here is my solution, based on CCSprite hope it will be useful for somebody

this is protocol for controlled object (like player or something):

@class AGSensitiveButton;

@protocol AGSensitiveButtonControlledObjectProtocol <NSObject>
@required
- (void)sensitiveButtonTouchDown:(AGSensitiveButton *)sButton;
- (void)sensitiveButtonTouchUp:(AGSensitiveButton *)sButton;
@optional
- (void)sensitiveTouchButtonKeepPressed:(AGSensitiveButton *)sButton forTime:(ccTime)pressTime;
@end

.h file:

#import "CCSprite.h"
#import "cocos2d.h"
#import "AGSensitiveButtonControlledObjectProtocol.h"

typedef enum {
    AGSensitiveButtonStateNormal = 0,
    AGSensitiveButtonStateHighlighted,
    AGSensitiveButtonStateDisabled
} AGSensitiveButtonState;

@interface AGSensitiveButton : CCSprite <CCTargetedTouchDelegate>

@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
@property (nonatomic, assign) ccTime maximumTouchDuration;
@property (nonatomic, weak) id <AGSensitiveButtonControlledObjectProtocol> controlledObject;
@property (nonatomic, copy) void (^touchDownHandler)();
@property (nonatomic, copy) void (^touchUpHandler)();

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture;

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
              disabledtexture:(CCTexture2D *)disabledTexture;

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
             controllerObject:(id <AGSensitiveButtonControlledObjectProtocol>)controlledObject;

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
              disabledtexture:(CCTexture2D *)disabledTexture
             controlledObject:(id <AGSensitiveButtonControlledObjectProtocol>)controlledObject;

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
              disabledtexture:(CCTexture2D *)disabledTexture
             controlledObject:(id <AGSensitiveButtonControlledObjectProtocol>)controlledObject
             touchDownHandler:(void(^)(void))touchDownHandler
               touchUpHandler:(void(^)(void))touchUpHandler;

- (void)setTexture:(CCTexture2D *)texture forState:(AGSensitiveButtonState)state;

- (BOOL)isHighlighted;

@end

implementation .m file:

#import "AGSensitiveButton.h"

@interface AGSensitiveButton ()
@property (nonatomic, assign) AGSensitiveButtonState state;
@property (nonatomic, strong) NSDictionary *stateTextures;
@property (nonatomic, assign) ccTime currentTouchTime;
@end

@implementation AGSensitiveButton

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture {
    return [self buttonWithNormalTexture:normalTexture
                      highlightedTexture:highTexture
                        controllerObject:nil];
}

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
              disabledtexture:(CCTexture2D *)disabledTexture {
    return [self buttonWithNormalTexture:normalTexture
                      highlightedTexture:highTexture
                         disabledtexture:disabledTexture
                        controlledObject:nil];
}

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
             controllerObject:(id <AGSensitiveButtonControlledObjectProtocol>)controlledObject {
    return [self buttonWithNormalTexture:normalTexture
                      highlightedTexture:highTexture
                         disabledtexture:nil
                        controlledObject:controlledObject];
}

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
              disabledtexture:(CCTexture2D *)disabledTexture
             controlledObject:(id <AGSensitiveButtonControlledObjectProtocol>)controlledObject {
    return [self buttonWithNormalTexture:normalTexture
                      highlightedTexture:highTexture
                         disabledtexture:disabledTexture
                        controlledObject:controlledObject
                        touchDownHandler:NULL
                          touchUpHandler:NULL];
}

+ (id)buttonWithNormalTexture:(CCTexture2D *)normalTexture
           highlightedTexture:(CCTexture2D *)highTexture
              disabledtexture:(CCTexture2D *)disabledTexture
             controlledObject:(id <AGSensitiveButtonControlledObjectProtocol>)controlledObject
             touchDownHandler:(void(^)(void))touchDownHandler
               touchUpHandler:(void(^)(void))touchUpHandler {
    AGSensitiveButton *button = [[self alloc] initWithTexture:normalTexture
                                                         rect:CGRectMake(0.0, 0.0, normalTexture.contentSize.width, normalTexture.contentSize.height)];
    [button setTexture:normalTexture forState:AGSensitiveButtonStateNormal];
    [button setTexture:highTexture forState:AGSensitiveButtonStateHighlighted];
    [button setTexture:disabledTexture forState:AGSensitiveButtonStateDisabled];
    button.controlledObject = controlledObject;
    button.touchDownHandler = touchDownHandler;
    button.touchUpHandler = touchUpHandler;
    return button;
}

- (void)setEnabled:(BOOL)enabled {
    [self setupNewState:enabled ? AGSensitiveButtonStateNormal : AGSensitiveButtonStateDisabled];
}

- (BOOL)isEnabled {
    return (self.state != AGSensitiveButtonStateDisabled);
}

- (BOOL)isHighlighted {
    return (self.state == AGSensitiveButtonStateHighlighted);
}

- (void)toggleTextureForCurrentState {
    CCTexture2D *textureToSet = [self.stateTextures objectForKey:[NSNumber numberWithInteger:self.state]];
    if (textureToSet) {
        self.texture = textureToSet;
        self.textureRect = CGRectMake(0.0, 0.0, textureToSet.contentSize.width, textureToSet.contentSize.height);
    }
}

- (void)setTexture:(CCTexture2D *)texture forState:(AGSensitiveButtonState)state {
    NSMutableDictionary *newStates = self.stateTextures.mutableCopy;
    if (texture) {
        [newStates setObject:texture forKey:[NSNumber numberWithInteger:state]];
    } else {
        [newStates removeObjectForKey:[NSNumber numberWithInteger:state]];
    }
    self.stateTextures = newStates.copy;
}

- (NSDictionary *)stateTextures {
    if (!_stateTextures) {
        _stateTextures = [[NSDictionary alloc] init];
    }
    return _stateTextures;
}

- (void)onEnter {
    [super onEnter];
    [self toggleTextureForCurrentState];
    [[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
    [self scheduleUpdate];
}

- (void)onExit {
    [super onExit];
    [self unscheduleUpdate];
    [[[CCDirector sharedDirector] touchDispatcher] removeDelegate:self];
}

- (void)update:(ccTime)dt {
    if ((self.state == AGSensitiveButtonStateHighlighted) && (self.maximumTouchDuration)) {
        self.currentTouchTime+=dt;
        if (self.currentTouchTime >= self.maximumTouchDuration) {
            [self ccTouchEnded:nil withEvent:nil];
    } else {
        if ([self.controlledObject respondsToSelector:@selector(sensitiveTouchButtonKeepPressed:forTime:)]) {
            [self.controlledObject sensitiveTouchButtonKeepPressed:self forTime:self.currentTouchTime];
        }
    }
    }
}

- (CGRect)rectForTouches {
    return CGRectMake(-self.contentSize.width/2, -self.contentSize.height/2,
                      self.contentSize.width, self.contentSize.height);
}

- (void)forwardTouchDownEventIntoHandlers {
    if ([self.controlledObject respondsToSelector:@selector(sensitiveButtonTouchDown:)]) {
        [self.controlledObject sensitiveButtonTouchDown:self];
    }
    if (self.touchDownHandler) {
        self.touchDownHandler();
    }
}

- (void)forwardTouchUpEventIntoHandlers {
    if ([self.controlledObject respondsToSelector:@selector(sensitiveButtonTouchUp:)]) {
        [self.controlledObject sensitiveButtonTouchUp:self];
    }
    if (self.touchUpHandler) {
        self.touchUpHandler();
    }
}

- (void)setupNewState:(AGSensitiveButtonState)state {
    if (self.state != state) {
        switch (state) {
            case AGSensitiveButtonStateHighlighted: {
                [self forwardTouchDownEventIntoHandlers];
                break;
            }
            default: {
                if (self.state == AGSensitiveButtonStateHighlighted) {
                    [self forwardTouchUpEventIntoHandlers];
                }
                break;
            }
        }
        self.state = state;
        [self toggleTextureForCurrentState];
    }
}

#pragma mark - CCTargetedTouchDelegate

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    if ((self.state != AGSensitiveButtonStateNormal) || (!CGRectContainsPoint([self rectForTouches], [self convertTouchToNodeSpaceAR:touch]))) {
        return NO;
    }
    self.currentTouchTime = 0.0;
    [self setupNewState:AGSensitiveButtonStateHighlighted];
    return YES;
}

- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
    if (self.state == AGSensitiveButtonStateHighlighted) {
        if (!CGRectContainsPoint([self rectForTouches], [self convertTouchToNodeSpaceAR:touch])) {
            [self ccTouchEnded:touch withEvent:event];
        }
    }
}

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    if (self.state == AGSensitiveButtonStateHighlighted) {
        [self setupNewState:AGSensitiveButtonStateNormal];
    }
}

@end