Programming iPhone Games with Cocos2D Part 2

The huge Library of cocos2D gives you some really good start for nearly everything you want to program. But sometimes it still might not fit your needs as you wanted. In this post I will show you how to enhance the menu class of cocos2d to function as radio menu.

A Menu in cocos2d grabs the touch events and delegates them to the menu items. You can hover over the menu items and when releasing the touch the current menu item under your finger will be executed. In cocos2D it is the same if you want to create a menu or a single button. If you just need one button you have to create a menu with one button. Keep in mind that you cannot slide from one menu into another, if the user does not release the touch the touch will be still captured by the first menu. But you can compose diffrent button types to one menu and also place them where you wanted. The menu class does have some align functions but you dont need to use them.

Ok, lets start with that what cocos2D have for you. First you have to create some menu items. In cocos2d they are called CCMenuItem (sounds reasonable). There a plenty of them but I will introduce here just one : CCMenuItemImage is an image button with diffrent images for each state: normal, selected and disabled. If you dont need disabled you dont have to set it up. Menu items support targets, so if you press a button a function will be called. Here is an example of how to create a CCMenuItemImage:

#import "MyScene.h"
@implementation MyScene
-(void) onButton1Pressed:(id) sender
{
	// TODO: something phenomenal 
}
 
-(void) onButton2Pressed:(id) sender
{
	// TODO: something even more phenomenal 
}
 
-(void) setupMenu
{
	CCMenuItemImage * button1 = [CCMenuItemImage itemFromNormalImage:@"b1_up.png" selectedImage:@"b1_down.png" target:self selector:@selector(onButton1Pressed:)];
 
	CCMenuItemImage * button2 = [CCMenuItemImage itemFromNormalImage:@"b2_up.png" selectedImage:@"b2_down.png" disabledImage:"b2_disabled.png" target:self selector:@selector(onButton2Pressed:)];
}

To actually create the menu you have to put all your buttons into a CCMenu class. One really strange thing is you have to create the CCMenu with items, you are able to add more items (childs) later but you have to create it with at least one item. So here is the complete code including alignment of all buttons:

#import "MyScene.h"
@implementation MyScene
-(void) onButton1Pressed:(id) sender
{
	// TODO: something phenomenal 
}
 
-(void) onButton2Pressed:(id) sender
{
	// TODO: something even more phenomenal 
}
 
-(void) setupMenu
{
	CCMenuItemImage * button1 = [CCMenuItemImage itemFromNormalImage:@"b1_up.png" selectedImage:@"b1_down.png" target:self selector:@selector(onButton1Pressed:)];
 
	CCMenuItemImage * button2 = [CCMenuItemImage itemFromNormalImage:@"b2_up.png" selectedImage:@"b2_down.png" disabledImage:"b2_disabled.png" target:self selector:@selector(onButton2Pressed:)];
 
	CCMenu * menu = [CCMenue menueWithItems:button1 , button2, nil]; // you have to terminate the list with nil
 
	menu.position = ccp ( 160, 240 );
	[menu alignItemsHorizontallyWithPadding:10.0f];
	[self addChild: menu];
}

Now when you just want to have a radio button menu instead, so that one item always is selected and if you select another you want to unselect the previous one. When hovering over the items you want to show both – the current selected one and the one you are targeting. Also if you cancel your hovering (like if the touch gets outside the screen) you want to unselect the one you are targetting and fallback to the one that was previously selected. All in all you need to enhance the CCMenu class. Lets start with the easy part, the header file of our new class called CCRadioMenu :

//
//  CCRadioMenu.h
//  Cocos2d Version used: 0.9.0 alpha
//  Created by Hans Hamm on 24.11.09.  
//
 
#import "cocos2d.h"
 
 
@interface CCRadioMenu : CCMenu {
	int selectedItemIndex; // will be the current targeted item 
	int fallBackItemIndex; // will be our fall back item (previously selected one)
}
 
@property int selectedItemIndex;
 
-(CCMenuItem *) itemForTouch: (UITouch *) touch; // will retrieve the current item for the touch
 
@end

Ok, there are several ways to implement such a menu, and probably this is not the best one. If someone has an improved version, feel free to post it. My working code sounds like :

//
//  CCRadioMenu.m
// 
//  Cocos2d Version used: 0.9.0 alpha
//  Created by Hans Hamm on 24.11.09.  
//
 
#import "CCRadioMenu.h"
 
@implementation CCRadioMenu
 
@synthesize selectedItemIndex;
 
/**
 * will be called if a touch started.
 */
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
	if( state != kMenuStateWaiting ) return NO; // do not track events if menu is busy
 
	selectedItem = [self itemForTouch:touch]; // any of our items was selected?
 
	if( selectedItem ) // if one of our items was selected
	{
		[selectedItem selected]; // make it be selected
		state = kMenuStateTrackingTouch; // mark menu as busy
		return YES; // say : "yes, we want more events from this touch"
	}
	return NO; // none of our items was selected, so we dont want to hear from this touch event anymore
}
 
/**
 * will be called if a touch event ended.
 */
-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
	NSAssert(state == kMenuStateTrackingTouch, @"[Menu ccTouchEnded] -- invalid state");
 
	for( CCMenuItem* item in children ) // unselect all items
	{
		[item unselected];	
	}
 
	if (selectedItem) // if we have selected an item:
	{
		[selectedItem selected]; // make it selected
		[selectedItem activate]; // an execute it
		fallBackItemIndex = selectedItemIndex; // the new fall back item is the current target
	}
	else // there is none selected
	{
		self.selectedItemIndex = fallBackItemIndex; // so just fall back to the old one
	}	
 
	state = kMenuStateWaiting; // we are not busy anymore
}
 
/**
 * will be called if a touch event was canceled. Normaly the user dont want
 * to execute the last selected one, so just fall back to the old one here.
 */
-(void) ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event
{
	NSAssert(state == kMenuStateTrackingTouch, @"[Menu ccTouchCancelled] -- invalid state");
 
	for( CCMenuItem* item in children ) // unselect all items
	{
		[item unselected];	
	}
	// on cancel just fall back to the old one :
	self.selectedItemIndex = fallBackItemIndex;
 
	// we are not busy anymore
	state = kMenuStateWaiting;
}
 
/**
 * will be called if a touch event moved.
 */
-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
	NSAssert(state == kMenuStateTrackingTouch, @"[Menu ccTouchMoved] -- invalid state");
 
	// still one of our items selected?
	CCMenuItem * currentItem = [self itemForTouch:touch]; 
 
	// if not, what was the fallback item again?
	CCMenuItem * fallBackItem = (CCMenuItem *) [children objectAtIndex:fallBackItemIndex];
 
	// if the new selected item changed and is not our fallback item
	if (currentItem != selectedItem && currentItem != fallBackItem)
	{				
		[selectedItem unselected]; // unselect the previous one
		selectedItem = currentItem; // this is our new targeted one ( could be nil ! ) 
 
		if (selectedItem) // if there is a new one ( touch could be outside the menu )
		{		
			[selectedItem selected]; // select this new one
		}
		else // there is no new one, so just fall back to the old one
		{
			[[children objectAtIndex:fallBackItemIndex]selected];
		}
 
	}
}
 
/**
 * Returns the first menu item hit by a touch event.
 * Also we could update the selectedItemIndex here.
 */
-(CCMenuItem *) itemForTouch: (UITouch *) touch
{
	CGPoint touchLocation = [touch locationInView: [touch view]]; // touch location
	touchLocation = [[CCDirector sharedDirector] convertToGL: touchLocation]; // in gl coordinates
	int idx = -1; // helper variable to determine the index of the item ( 0 = first item )
 
	for( CCMenuItem* item in children ) 
	{
		idx++; // in first step this would be zero ( 0 )
 
		// convert the touch to the local coordinates of the current item
		// and check if the location is inside the rect of it
 
		CGPoint local = [item convertToNodeSpace:touchLocation]; 		
		CGRect r = [item rect];  
		r.origin = CGPointZero;
 
		if( CGRectContainsPoint( r, local ) ) // if touch is inside it
		{
			selectedItemIndex = idx;  // save the itemIndex and..
			return item;	 // .. return the item
		}
	}
 
	// we found not item hit by that touch:
	return nil;
}
 
/**
 * returns the current selected Item Index.
 */
-(int) selectedItemIndex
{
	return selectedItemIndex;
}
 
/**
 *  sets the current Selected Item Index ( but does not activate it )
 */
- (void)setSelectedItemIndex:(int) value 
{
	for( CCMenuItem* item in children )  // unselect all
	{
		[item unselected];	
	}
 
	selectedItemIndex = value;	// store new index
	selectedItem = [children objectAtIndex:selectedItemIndex]; // get the item for that index	
	fallBackItemIndex = selectedItemIndex; // store as fallBackIndex, too.	
	[selectedItem selected]; // mark as selected.
}
 
@end

So how to change your first “CCMenu-Code” to fit our CCRadioMenu thing ? All you have to do is to import your class, change CCMenu to CCRadioMenu and thats it. May be just one thing more. If you want a specific item be initially selected, you have to set selectedItemIndex once the menu is created:

menu.selectedItemIndex = 0; // first Item should be initially selected

so the code should now look like this :

#import "MyScene.h"
#import "CCRadioMenu.h"
 
@implementation MyScene
-(void) onButton1Pressed:(id) sender
{
	// TODO: something phenomenal 
}
 
-(void) onButton2Pressed:(id) sender
{
	// TODO: something even more phenomenal 
}
 
-(void) setupMenu
{
	CCMenuItemImage * button1 = [CCMenuItemImage itemFromNormalImage:@"b1_up.png" selectedImage:@"b1_down.png" target:self selector:@selector(onButton1Pressed:)];
 
	CCMenuItemImage * button2 = [CCMenuItemImage itemFromNormalImage:@"b2_up.png" selectedImage:@"b2_down.png" disabledImage:"b2_disabled.png" target:self selector:@selector(onButton2Pressed:)];
 
	CCRadioMenu * menu = [CCRadioMenu menueWithItems:button1 , button2, nil]; // you have to terminate the list with nil
 
	menu.selectedItemIndex = 0; // first Item should be initially selected
 
	menu.position = ccp ( 160, 240 );
	[menu alignItemsHorizontallyWithPadding:10.0f];
	[self addChild: menu];
}

Update to 0.99

Since they changed some ivar names in 0.99 you have to use “children_” instead “children”. So if you are using 0.99 or later you have to use my updated code for CCRadioMenu class.

Updated code of CCRadioMenu class for cocos2d 0.99 stable and later :

#import "CCRadioMenu.h"
 
@implementation CCRadioMenu
 
@synthesize selectedItemIndex;
 
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
	if( state != kMenuStateWaiting ) return NO;
 
	selectedItem = [self itemForTouch:touch];
 
	if( selectedItem ) 
	{
		[selectedItem selected];
		state = kMenuStateTrackingTouch;
		return YES;
	}
	return NO;
}
 
-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
	NSAssert(state == kMenuStateTrackingTouch, @"[Menu ccTouchEnded] -- invalid state");
 
	for( CCMenuItem* item in children_ ) 
	{
		[item unselected];	
	}
 
	if (selectedItem)
	{
		[selectedItem selected];
		[selectedItem activate];
		fallBackItemIndex = selectedItemIndex;
	}
	else
	{
		self.selectedItemIndex = fallBackItemIndex;
	}	
 
	state = kMenuStateWaiting;
}
 
-(void) ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event
{
	NSAssert(state == kMenuStateTrackingTouch, @"[Menu ccTouchCancelled] -- invalid state");
 
	for( CCMenuItem* item in children_ ) 
	{
		[item unselected];	
	}
 
	self.selectedItemIndex = fallBackItemIndex;
 
	state = kMenuStateWaiting;
}
 
-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
	NSAssert(state == kMenuStateTrackingTouch, @"[Menu ccTouchMoved] -- invalid state");
 
	CCMenuItem * currentItem = [self itemForTouch:touch];
	CCMenuItem * fallBackItem = (CCMenuItem *) [children_ objectAtIndex:fallBackItemIndex];
	if (currentItem != selectedItem && currentItem != fallBackItem) 
	{				
		[selectedItem unselected];
		selectedItem = currentItem;
 
		if (selectedItem)
		{		
			[selectedItem selected];
		}
		else
		{
			[[children_ objectAtIndex:fallBackItemIndex]selected];
		}
 
	}
}
 
-(CCMenuItem *) itemForTouch: (UITouch *) touch
{
	CGPoint touchLocation = [touch locationInView: [touch view]];
	touchLocation = [[CCDirector sharedDirector] convertToGL: touchLocation];
	int idx = -1;
 
	for( CCMenuItem* item in children_ ) 
	{
		idx++;
		CGPoint local = [item convertToNodeSpace:touchLocation];
 
		CGRect r = [item rect];
		r.origin = CGPointZero;
 
		if( CGRectContainsPoint( r, local ) )
		{
			selectedItemIndex = idx;
			return item;
		}
	}
 
	return nil;
}
 
-(int) selectedItemIndex
{
	return selectedItemIndex;
}
 
- (void)setSelectedItemIndex:(int) value 
{
	for( CCMenuItem* item in children_ ) 
	{
		[item unselected];	
	}
 
	selectedItemIndex = value;	
	selectedItem = [children_ objectAtIndex:selectedItemIndex];
 
	fallBackItemIndex = selectedItemIndex;
 
	[selectedItem selected];      
}
 
@end

About this entry