Custom Dialog Boxes Using Cocos2d on iPhone

Custom dialog boxes are a must for any game. iPhone’s standard dialog boxes are fine for a business or productivity app, but in a game they look out of place. Users play games to escape from their everyday lives. Showing them the same exact graphic design that they associate with work and ordinary routine is a great way to subtly interrupt the immersiveness of their gameplay experience. We don’t want that, so we need to customize.

iPhone UIAlertView to custom dialog box example. Doesn't the custom one look much better?

Cocos2d makes it easy to convert your drab standard UIAlertViews into glorious customized color dialog boxes. Above is an actual UIAlertView / custom dialog box transition from my upcoming iPhone game. Here’s how I did it.

Implementing a Custom Dialog Box in Cocos2d

Cocos2d Scene Structure

Cocos2d uses a scene graph to hold everything being displayed at any given point in a game. Informally, this data structure is a hierarchy of objects, each of which might display something onscreen, contain other objects to be displayed onscreen, or both. In Cocos2d, these objects are descendants of the CCNode class.

Related objects can be placed into a single container object, which allows them all to be manipulated as a unit when necessary. The root, or lowest-level, container at any given time is an instance of the CCScene class. A CCScene contains CCLayer objects, which themselves contain other objects such as menu labels or sprites to be displayed. Layers, as the name implies, are displayed superimposed upon each other onscreen, which makes a CCLayer perfect for our custom dialog box.

Custom CCLayer

Here’s the code for DialogLayer.h and DialogLayer.m. These implement a basic but perfectly servicable custom iPhone dialog box. It can be used as-is or expanded in any number of ways for particular situations. Take a look through the finished product, and then we’ll go over it below.

DialogLayer.h

//
//  DialogLayer.h
//  concentrate
//
//  Created by Paul Legato on 12/4/10.
//  Copyright 2010 Paul Legato. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface DialogLayer : CCLayer {
  NSInvocation *callback;
}

-(id) initWithHeader:(NSString *)header andLine1:(NSString *)line1 andLine2:(NSString *)line2 andLine3:(NSString *)line3 target:(id)callbackObj selector:(SEL)selector;
-(void) okButtonPressed:(id) sender;

@end

DialogLayer.m

//
//  DialogLayer.m
//  concentrate
//
//  Created by Paul Legato on 12/4/10.
//  Copyright 2010 Paul Legato. All rights reserved.
//

#import "DialogLayer.h"
#import "GameSoundManager.h"

#define DIALOG_FONT @"bicho.fnt"

@implementation DialogLayer
-(id) initWithHeader:(NSString *)header andLine1:(NSString *)line1 andLine2:(NSString *)line2 andLine3:(NSString *)line3 target:(id)callbackObj selector:(SEL)selector
 {
   if((self=[super init])) {

     NSMethodSignature *sig = [[callbackObj class] instanceMethodSignatureForSelector:selector];
     callback = [NSInvocation invocationWithMethodSignature:sig];
     [callback setTarget:callbackObj];
     [callback setSelector:selector];
     [callback retain];

     CGSize screenSize = [CCDirector sharedDirector].winSize;
     
     CCSprite *background = [CCSprite node];
     [background initWithFile:@"Dialog.png"];
     [background setPosition:ccp(screenSize.width / 2, screenSize.height / 2)];
     [self addChild:background z:-1];


     CCLabelBMFont *headerShadow = [CCLabelBMFont labelWithString:header fntFile:DIALOG_FONT];
     headerShadow.color = ccGRAY;
     headerShadow.opacity = 127;
     [headerShadow setPosition:ccp(243, 262)];
     [self addChild:headerShadow];

     CCLabelBMFont *headerLabel = [CCLabelBMFont labelWithString:header fntFile:DIALOG_FONT];
     headerLabel.color = ccBLACK;
     [headerLabel setPosition:ccp(240, 265)];
     [self addChild:headerLabel];

     //////////////////

     CCLabelBMFont *line1Label = [CCLabelBMFont labelWithString:line1 fntFile:DIALOG_FONT];
     line1Label.color = ccBLACK;
     line1Label.scale = 0.84f;
     [line1Label setPosition:ccp(240, 200)];
     [self addChild:line1Label];

     CCLabelBMFont *line2Label = [CCLabelBMFont labelWithString:line2 fntFile:DIALOG_FONT];
     line2Label.color = ccBLACK;
     line2Label.scale = 0.84f;
     [line2Label setPosition:ccp(240, 160)];
     [self addChild:line2Label];

     CCLabelBMFont *line3Label = [CCLabelBMFont labelWithString:line3 fntFile:DIALOG_FONT];
     line3Label.color = ccBLACK;
     line3Label.scale = 0.84f;
     [line3Label setPosition:ccp(240, 120)];
     [self addChild:line3Label];

     CCMenuItemImage *okButton = [CCMenuItemImage itemFromNormalImage:@"OKButton.png" selectedImage:@"OKButtonSelected.png" target:self selector:@selector(okButtonPressed:)];
     [okButton setPosition:ccp(0, 60)];

     CCMenu *menu = [CCMenu menuWithItems: okButton, nil];
     menu.position = ccp(240,0);
     [self addChild:menu];
   }
   return self;
 }

-(void) okButtonPressed:(id) sender
 {
   [[GameSoundManager sharedManager].soundEngine playEffect:@"OK.caf"
                                    pitch:1.0f
                                    pan:0.0f
                                    gain:1.0f];

   [self removeFromParentAndCleanup:YES];
   [callback invoke];
 }


-(void) dealloc
 {
   [callback release];
   [super dealloc];
 }
@end

Custom dialog box code analysis

Overview

The dialog box is implemented as a subclass of CCLayer. An initWithHeader method is provided to initialize the layer with a given message and a specified callback method that will be invoked when the dialog’s “OK” button is pressed. It also plays a confirmation sound and switches the “OK” button’s image to a “pressed button” when the user taps it.

DialogLayer.m requires a few external supports, which must be available as project resources. Dialog.png is the dialog box’s background image. bicho.fnt is an AngelCode format bitmapped font, used for the labels, and can be substituted with any font you like. OKButton.png and OKButtonSelected.png are the images to be used for the button in unpressed and pressed forms. Finally, OK.caf is the audio file to be played when the user taps the button.

Analysis

Storing the callback

     NSMethodSignature *sig = [[callbackObj class] instanceMethodSignatureForSelector:selector];
     callback = [NSInvocation invocationWithMethodSignature:sig];
     [callback setTarget:callbackObj];
     [callback setSelector:selector];
     [callback retain];

This code is Objective C’s (massively redundant) way of storing the callback to be activated when the dialog’s button is pressed. It’s based on the callback code in Cocos2d itself. (We could also have used Objective C’s new closure data type, called “blocks”, but that syntax grates my Lispy sensibilities even more than the overly verbose Javaish mess here, besides not working out of the box on older versions of iOS.)

The initWithHeader method is given a target object and a method selector. It looks up the class of the target object and then looks up the selector on that class at line 1, resulting in a NSMethodSignature object which encapsulates the class-method combination. Line 2 transforms that class-method signature into an “invocation,” which represents the act of actually calling that method on some as yet unspecified object. Line 3 associates the invocation with the particular target object we were given, and line 4 associates the selector (which is utterly odd, since the desired method is encoded in the signature object already.) Finally, we retain the callback so that we can save it for later (to be activated when the user actually presses the “OK” button) and it won’t be deallocated at the end of the present scope.

Setting up the background and labels

The remainder of the initWithHeader method, lines 25 to 68 in the main listing above, is pretty straightforward. A CCSprite object is created to store the background image and added to self, that is, to the new custom layer being created. Its position is set to be the center of the screen, and its level on the Z axis is set to -1 so that it will appear below anything else added in the layer.

CCLabelBMFonts are then created for the given text and added to the new layer. A shadow effect is created for the header by writing the same thing in grey at a slight offset underneath the main text. All lines except the header are then scaled down to 0.84 of their natural size, so as to enhance the prominence of the header.

A CCMenuItemImage is finally created to be the “OK” button. It’s added to a CCMenu, which handles the logic of button presses for us, and the CCMenu is then added to the new layer. It will take a little experimentation to get the exact placement of the OK button correct (lines 64 and 67) relative to your background image.

Note that the OK button itself takes callback arguments of exactly the same type that our initWithHeader method does (line 63). We might be tempted to forego the construction of our own callback object and just pass the given target and selector on to the OK button, simplifying the design of our DialogLayer a bit, but then we wouldn’t have the chance to play a sound when the OK buton is pressed. More seriously, we would have no way to destroy the dialog layer and remove it from the screen. So, we give the OK button a target of self and tell it to call the okButtonPressed method when OK is pressed.

The okButtonPressed callback

The CCMenuItemImage will invoke this method when the user presses “OK”. This means that it’s time to remove the DialogLayer from the scene (easy enough, line 80). Here, we also play a little click-type sound to make the game seem more interactive; also easy with CocosDenshion, lines 75-78. If you don’t want to play a sound, you can remove those lines and the GameSoundManager.h include file.

dealloc

Nothing fancy here; we just release our retained callback object and then defer to super.

Using the custom DialogLayer

Adding a custom dialog layer to your game scene is very easy:

   CCLayer *dialogLayer = [[[DialogLayer alloc] 
                            initWithHeader:@"Level 1" 
                            andLine1:@"Tilt The Phone" 
                            andLine2:@"To Balance" 
                            andLine3:@"The Ball!"
                            target:self
                            selector:@selector(beginLevelOne)] autorelease];
   [self addChild:dialogLayer z:200];

Future Expansions

DialogLayer is a simple modal dialog box that can only accept one action from the user. It can easily be enhanced in many ways, depending on your needs. The most obvious enhancement is to add multiple buttons to allow the user to make a choice rather than simply dismissing the box. It could be modified to accept an arbitrary amount of text and divide it up automatically into an aesthetically pleasing configuration.

One thing that DialogLayer does not do is pause the game. I decided that that is not necessarily desirable in all cases; many game scenarios need to present information to the user without stopping the action. It’s easy enough to pause manually when activating the dialog.

Little enhancements like this are critical as ‘polish’ for your game.

7 Responses to Custom Dialog Boxes Using Cocos2d on iPhone

  1. Thank you for your tutorial, it helps me to understand the UIAlertView!

    I’ll use this technique to create my pop up screen in my next project!

  2. NSMethodSignature *sig = [[callbackObj class] instanceMethodSignatureForSelector:selector];
    should be written as
    NSMethodSignature *sig = [callbackObj methodSignatureForSelector:selector];

  3. thanks for your tutorial, that’s just what i need.
    and I found that:
    the -(void) okButtonPressed:(id) sender should be:
    [callback invoke];
    [self removeFromParentAndCleanup:YES];

    the [callback invoke] should put before remove, or there would be error message like:
    *** -[NSInvocation invoke]: message sent to deallocated instance 0xba019d0