CCButton – My Button Subclass of CCSprite

For those who don’t know, I’ve been working on a game for iOS known as Empous. Empous, which resembles RISK, is being implemented using Cocos2d.

Check out Cocos2d if you want to make a 2D game for iOS. Cocos2d is very intuitive and highly recommended by me. There are some drawbacks. The biggest one being the lack of seemingly obvious classes. Cocos2d does not assume anything about anyone’s development style. It gives you all the basic tools but you’re going to have to write the bigger ones yourself. Over the development of Empous, I have made a few classes I’m proud of. The most recent of these is a subclass of CCSprite.

To display an image using Cocos2d, a developer would rely on the CCSprite class. It’s a very simple class and handles all the terrible openGL code needed to display the image. It even caches the textures in case they’re needed again.

CCSprite only handles images. It doesn’t even respond to touches. A programmer can check if a touch and a sprite coincide, but that has to be done manually. A perfect extension of CCSprite would be some sort of button class. Cocos2d has no such class because they don’t want to force programmers to follow patterns unless absolutely necessary.

I however use buttons everywhere, so I created the CCButton class. The rest of this post will explain how it works. If you want the code now feel free to check out the HurleyProg cocos2dUtilities repo. The repository will always have the most up-to-date code. It is likely that this post will not be updated as that code changes.

How CCButton Works

The ideal button should know when it’s been touched and should be able to fire any callback when touched. CCbutton can do this. In addition, CCButton allows a programmer to specify both pressed and unpressed images for the button. CCButton will automatically change the image as necessary. Finally, CCButton allows a programmer to optionally scale the touch area of the button. I used this to improve the user experience when the button images were quite small.

The header for CCButton is as follows (comments have been removed for brevity):

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

@interface CCButton : CCSprite 
{
    SEL _selector;
    id _target;
    NSString* _spriteFile;
    NSString* _pressedSpriteFile;
    float _touchRectScale;
}

@property (nonatomic, assign) id target;
@property (nonatomic) SEL selector;
@property (nonatomic, copy) NSString* spriteFile;
@property (nonatomic, copy) NSString* pressedSpriteFile;
@property (nonatomic, assign) float touchRectScale;

+(id)spriteWithFile:(NSString *)filename withPressedFile:(NSString*)pressedFilename target:(id)object function:(SEL)callback;
+(id)spriteWithFile:(NSString *)filename withPressedFile:(NSString*)pressedFilename touchAreaScale:(float)scale target:(id)object function:(SEL)callback;

@end
}

The class is not terribly complicated. The main thing to point out is the two methods that return id. The first creates a button where the touch area is equal to the bounding box of the sprite. The second allows a programmer to specify how much much the touch area should be scaled. In mathematical terms, the touch area = boundingbox * scale. Using a scale of “1.0” would be equivalent to the first spriteWithFile method. The two methods both return autoreleased objects which is the custom of Cocos2d.

CCbutton.m will be covered in chunks. This first chunk shows how the two id methods in the header return autoreleased objects. It also shows how the init method calls constructor for CCSprite and then stores the arguments using the properties defined in the header. Finally, the dealloc method cleans up the NSString references.

#import "CCButton.h"

@implementation CCButton

@synthesize target = _target;
@synthesize selector = _selector;
@synthesize spriteFile = _spriteFile;
@synthesize pressedSpriteFile = _pressedSpriteFile;
@synthesize touchRectScale = _touchRectScale;

+(id)spriteWithFile:(NSString *)filename withPressedFile:(NSString*)pressedFilename target:(id)object function:(SEL)callback
{
    return [[[self alloc]initWithWithFile:filename withPressedFile:pressedFilename touchAreaScale:1.0 target:object method:callback] autorelease];
}

+(id)spriteWithFile:(NSString *)filename withPressedFile:(NSString*)pressedFilename touchAreaScale:(float)scale target:(id)object function:(SEL)callback;
{
    return [[[self alloc]initWithWithFile:filename withPressedFile:pressedFilename touchAreaScale:scale target:object method:callback] autorelease];
}

-(id)initWithWithFile:(NSString *)filename withPressedFile:(NSString*)pressedFilename touchAreaScale:(float)scale target:(id)object method:(SEL)callback
{
    self = [super initWithFile:filename];
    if(self)
    {
        self.target = object;
        self.selector = callback;
        self.spriteFile = filename;
        self.pressedSpriteFile = pressedFilename;
        self.touchRectScale = scale;
    }
    return self;
}

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

The following code registers the CCButton instance with the touch dispatcher. It will also remove itself from the touch dispatcher when it is either removed from a layer or the scene changes.

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

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

The next method is the most important method in the class. It will check to see if a touch occurred within the sprite. It does this by first getting the bounding box from the button. If a scale other than 1.0 has been set, it will create a new rectangle by scaling the bounding box and then readjusting the origin to center the new touch area around the button. It finally does the check using CGRectContainsPoint. This code assumes that the button’s parent is the size of the iPhone screen. Nested sprites probably won’t work.

- (BOOL)containsTouchLocation:(UITouch *)touch
{
    CGPoint touchLocation = [self.parent convertTouchToNodeSpace:touch];

    CGRect spriteArea = self.boundingBox;
    if (self.touchRectScale != 1.0)
    {
        float newWidth = spriteArea.size.width * self.touchRectScale;
        float newHeight = spriteArea.size.height * self.touchRectScale;
        
        float differenceInWidth = newWidth - spriteArea.size.width;
        float differenceInHeight = newHeight - spriteArea.size.height;
        
        spriteArea = CGRectMake(spriteArea.origin.x - (0.5 * differenceInWidth),
                              spriteArea.origin.y - (0.5 * differenceInHeight),
                              spriteArea.size.width * self.touchRectScale,
                              spriteArea.size.height * self.touchRectScale);
    }
    return CGRectContainsPoint(spriteArea, touchLocation);
}

Finally, the button needs to respond to touches. Besides calling the above method, these two methods will swap out the button images as necessary.

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    BOOL isSpriteTouched = [self containsTouchLocation:touch];
    if(isSpriteTouched)
    {
        [self setTexture:[[CCTextureCache sharedTextureCache] addImage:self.pressedSpriteFile]];
    }
    return isSpriteTouched;
}

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    [self setTexture:[[CCTextureCache sharedTextureCache] addImage:self.spriteFile]];
    
    if(![self containsTouchLocation:touch])
        return;
    [self.target performSelector:self.selector withObject:self];
}

@end

That’s it! Works great for me. I remind you that you should get the code from the repository. If I have made any bug fixes, they will be in the repo and may not be in this post.