Cocos2d Menus and arguments

I was toying with the idea of writing a post about why I went independent, but I figure that wasn't appropriate for the wider audience of iDevBlogADay.com. Check my blog for those posts. So I decided to post a more technical piece regarding cocos2d menus and how to pass arguments through the Cocos2d menu system. But I was facing two issues. One I hated rewriting similar code over and over again. Second to make menu items more reusable, I needed to be able to pass arguments to the selector of the menu, which wasn't readily apparent how to do this. Also, checking the cocos2d forums and other places I saw that many others have had this issue.

So my goal is to create a reusable button factory which allows for the passing of arguments to the selector (read check this tutorial to get the basics for Button and Text Menu Items in Cocos2d).

For starters, here is a very typical pattern for adding a text menu item:

-(void) addMenu {
    //Create the label for the menu
    CCLabelTTF *label = [CCLabelTTF labelWithString:@"Button Text fontName:@"Arial" fontSize:12];

    //Add label to a menu item.
    CCMenuItemLabel *menuItem = [CCMenuItemLabel itemWithLabel:label target:self 
                                                      selector:(menuItemPressed:)]; 
    //Add menu item to a menu.   
    CCMenu *menu = [CCMenu menuWithItems:menuItem, nil];
    menu.position = ccp(0,0);

    //Add menu to a layer.
    [self addChild:menu];
}

//Method to receive button pressed action
-(void) menuItemPressed: (id)sender {
    [self doSomething];
}

As you might have noticed, the argument for the menu select is automatically supplied as a CCMenuItem of some sort or another, but generically it is just an id object. The question is how do you forward some information to the menuItemPressed method, from the menu item you created, and to no have to go through the effort of creating this sequence of code over and over again.

Example, this is ugly.

//Method to receive button1 pressed action
-(void) menuItemPressed1: (id)sender {
    [self doSomething: 1];
}
//Method to receive button2 pressed action
-(void) menuItemPressed2: (id)sender {
    [self doSomething: 2];
}
//Method to receive button3 pressed action
-(void) menuItemPressed3: (id)sender {
    [self doSomething: 3];
}

I wanted something like this.

-(void) menuItemPressed: (id)sender {
    [self doSomething: sender.passedVariable];
}

Gladly, all the CCMenuItem classes and subclasses all have a userData property that you can use to assign data to like this:

menuItem.userData = someObj;

To use the userData, and to make sure there weren't warnings or error in XCode, I explicitly cast the object to what is should, and assign it to a temp variable.

Update the code a bit and it becomes:

-(void) addMenu {
    //Create the label for the menu
    CCLabelTTF *label = [CCLabelTTF labelWithString:@"Button Text fontName:@"Arial" fontSize:12];

    //Add label to a menu item.
    CCMenuItemLabel *menuItem = [CCMenuItemLabel itemWithLabel:label target:self 
                                                      selector:(menuItemPressed:)]; 
    //Add menu item to a menu.   
    CCMenu *menu = [CCMenu menuWithItems:menuItem, nil];
    menuItem.userData = someObj;  //Of type SomeObj.
    menu.position = ccp(0,0);

    //Add menu to a layer.
    [self addChild:menu];
}

//Method to receive button pressed action
-(void) menuItemPressed: (id)sender {
    (SomeObj*) tempObj = (SomeObj*) sender.userData; 
    [self doSomething: sender.userData];
}

So now, we have a passed variable being used in a receiver of a pressed menu item. Great. Now write this code over and over again. OR, create a button factory that does the heavy lifting for you. Since I wanted to pass the position, and have a general layer to use, this method returns a CCLayer, but you could just return a CCMenu instead.

Here is my final button factory code, and an example usage (subject to improvements in the future, I will try to keep this article up to date).

-(CCLayer*) actionTrayButtonFactory: (NSString*) text withPosition: (CGPoint) pos withUserData: (id) userData selector: (SEL) selector {
    
    CCLayer *layer = [CCLayer node];
    
    CCLabelTTF *label = [CCLabelTTF labelWithString:text fontName:@"Arial" fontSize:13];
    label.color = ccc3(255, 255, 255); //Set your own color.
    
    CCMenuItemLabel *menuItem = [CCMenuItemLabel itemWithLabel:label target:self 
                                                      selector:selector];
    menuItem.userData = [userData retain];
    
    CCMenu *menu = [CCMenu menuWithItems:menuItem, nil];
    menu.position = pos;

    [layer addChild:menu];
    
    return layer;
}

Usage:

        CCLayer *layer = [self actionTrayButtonFactory: @"Button Name" withPosition:ccp(100, 200) withUserData:[NSNumber numberWithInt:1] selector:@selector(menuItemPressed:)];
        [self addChild: layer];

I would use this inside a loop where I am incrementing the button name, position height and the userData passed in.

Next, I will be making a similar button factory that takes in images and will create CCMenuItemImages instead. The same format will work. Also, a further extension would be to add in another argument for button type incase you wanted to adjust the size/shape/color of the manufactured menu item.

Also, if you are making a localized app, the button factory method can update the 'labelWithString:text' portion to 'labelWithString:NSLocalizedString(text, nil)', which is what I am doing in my game.

I am not sure if I needed to use objects in the userData property, instead of just integers or floats, but that is what was need when I got it to start working. I also at times needed to place a 'retain' on the object to maintain the data. Not exactly sure why. Any help from my readers on why that is the case will help clean up this code long term.

This code will show up in the Skejo Studios Outbreak project. Check out the game development diaries. This post is a part of the iDevBlogADay series. Check out some other recent posts including Who is an Indie Dev and What's my motivation.

I suggest to look into a

I suggest to look into a better code formatting plugin to enhance the readability of the code. It's not indented, and after a few lines the font changes. Makes it hard to read.

Good point. I have updated it

Good point. I have updated it slightly. I hope that helps and thanks for the reminder.