The One Day Help System, in Cocos2d for iOS

Since we are developing a strategy game for the iPad/iPhone at Skejo Studios, we were in need of an in game help system to quickly tutor the player with the different controls and game options. We had been putting off this mechanism for a while, since it was figured it might take a while, but surprisingly it was rather easy in Cocos2d to accomplish.

Game Layer Code

I am assuming you know how to add the correct definitions and import class headers, so I am not showing that part. Placing the below code in your init method for the Cocos2d scene or layer setups up the layer to receive touches (though I assume almost any game is receiving touches already) and adds the help layer (blank for now).

-(id) init
{
    
	if( (self=[super init])) {

        //Setup delagate for processing touches
        [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:-1 swallowsTouches:YES];
	...

    helpSystemOn = NO;
    helpLayer = [HelpLayer  node];   
    [helpLayer setGamePlayLayer: self]; //Only needed if you need to know something from your gaming layer.
    [playingLayer addChild:helpLayer z:10];

	...
}

To accept the touches, you need at least the ccTouchBegan method (required for the CCTouchDispatch call):

#pragma mark Touch Processing

-(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event
{	
    CGPoint touchLocation = [self convertTouchToNodeSpace:touch];

    if (receivingTouches == YES) {
        [self selectSpriteForTouch:touchLocation];
        
        return YES;
    } else if (helpSystemOn == YES) {
        [self selectSpriteForHelp:touchLocation];
        return YES;

    } else {
        return NO;
    }
}

- (BOOL)selectSpriteForHelp:(CGPoint)touchLocation {
    BOOL processedTouch = NO;
    processedTouch = [helpLayer processTouch: touchLocation];
    return processedTouch;

}

In our game, we don't always want to be receiving touches, so we have a receivingTouches flag that we check for. If so, then we forward the touch to the correct method. We now added the check for the Help System by checking for the helpSystemOn flag and forward to the selectSpriteForHelp method which simple passes the touch to the HelpClass instance.

NOTE: The 'processedTouch' return value is use if you have multiple TouchDispatchers going at once. They will be queried in order of priority until a dispatcher returns a TRUE value.

Before moving on to the HelpLayer class, we need to add the toggle switch for the help system. Normally such a toggle should be placed with other high level game controls like 'End Turn', 'Return to Menu' or 'Restart Turn' buttons. We added the showInfo menu button with the other buttons and assign it the showInfoPressed method when pressed.

- (void) addGameControls {
	...  //Other menu items.
    
	CCMenuItem *showInfo = [[CCMenuItemImage 
                              itemFromNormalImage:@"info.png" selectedImage:@"info_pressed.png" 
                              target:self selector:@selector(showInfoPressed:)];
    
    showInfo.position = ccp(60, 300);
    
    CCMenu *controlsMenu = [CCMenu menuWithItems:pauseGameItem, showManual, restartTurn, showInfo, endTurn, nil];
    
    //Place in upper left corner
    [controlsMenu alignItemsHorizontallyWithPadding:10.0];
    controlsMenu.position = ccp(10, 700);
    [self addChild:controlsMenu z:10];

}


-(void) showInfoPressed: (CCMenuItemFont*)itemPassedIn {
    
    if (helpSystemOn == YES) {
        //Turning Off
        helpSystemOn = NO;
        [self rebuildActions]; //Used to re-enable action menu    
	} else {
        //Turning On
        [self cancelMoves]; //Used to disable any current actions
        [self rebuildActionsWithBlanks]; //Used to disable the action menu
        helpSystemOn = YES;
    }
    
    [helpLayer toggleHelp];
}

Any appropriate measures should be taken so that the game is essentially paused when the information system is on. We did this by disabling the action menu and canceling any in-progress moves.

So that is all the code that is needed inside the game play class, now we need to move to the HelpLayer class.

HelpLayer Class

//
//  HelpLayer.h
//
//  Created by Greg Holsclaw on 12/12/11.
//  Skejo Studios, www.skejo.com
//

#import 
#import "cocos2d.h"
#import "GamePlayLayer.h"
#import "Constants.h"

@class GamePlayLayer;

@interface HelpLayer : CCLayer {
    CCLayer *infoLayer;
    GamePlayLayer *gameLayer;
}

@property (nonatomic, strong) CCLayer *infoLayer;

- (void) toggleHelp;
- (BOOL) processTouch: (CGPoint)touchLocation;
- (void) setGamePlayLayer: (GamePlayLayer*) gamePlay;

@end
//
//  HelpLayer.m
//
//  Created by Greg Holsclaw on 12/12/11.
//  Skejo Studios, www.skejo.com
//

#import "HelpLayer.h"

@implementation HelpLayer
@synthesize infoLayer;

- (id) init
{
    self = [super init];
    if (self != nil)
    {
        
        infoLayer = [CCLayer node];
        CCSprite *helpBG = [CCSprite spriteWithFile:@"overlay-70.png"]; //A 1x1 pixel semi-transparent black overlay.
        [helpBG setScaleX:1024]; //Scale transparent background to appropriate size.
        [helpBG setScaleY:768];  //Scale transparent background to appropriate size.
        [self addChild:helpBG];
        [self setVisible:NO];    //Initially made invisible, only shown when info system turned on.
        
        CGSize screenSize = [CCDirector sharedDirector].winSize;
        self.position = ccp(screenSize.width/2, screenSize.height/2);
        
        [self addChild:infoLayer];
        
	}
    return self;
    
}	

-(void) setGamePlayLayer: (GamePlayLayer*) gamePlay {
    gameLayer = gamePlay;
}

- (void) toggleHelp {
    if (self.visible == YES) {
        //Remove Help system

        [infoLayer removeAllChildrenWithCleanup:YES];
        self.visible = NO;
    } else {
        //Add Help system

        self.visible = YES;        
        CCLabelTTF *helpLabel = [CCLabelTTF labelWithString:@"Info Mode On" fontName:kBaseFont fontSize:24];

        [helpLabel runAction:[CCSequence actions:[CCDelayTime actionWithDuration:2.2], [CCFadeOut actionWithDuration:1], nil]];
        
        [self addChild:helpLabel];
    }
}


-(void) dealloc {
    gameLayer = nil;
}

@end

When the toggleHelp method is called by the main game layer it makes visible the transparent overlay, and adds a disappearing message. Now we just need to implement the processTouch methods that was used when receiving touches.

Before diving into the code, I need to mention that a couple of design considerations went into this code. There are always at least two consideration in coding, ease of coding and maintainability. Usually the super easily coded solution isn't the best long term solution. We followed an easy to code method, which I admit *might* become a time sink later. But if the UI doesn't change much, then later will never come. We try not to let the perfect kill the good here.

If your game consists of items that are moving, and those items need to present information when pressed, then the code gets a bit more complicated, and the proposed solution won't work at all. See the 'Where to Go From Here section'.

For now, we simply wanted to add information for the control buttons, which are statically placed. Thus all we have to layout a set of coordinate boxes, and trigger the correct message if the touch is inside the boxes.

- (BOOL) processTouch: (CGPoint)touchLocation {
    BOOL processed = NO;
    BOOL found = NO;
    NSString *imageStr;
    NSString *text;
    CGRect boundBox;
    
         
    boundBox = CGRectMake(10, 230, 50, 50);  //Location of Move Button
    if (CGRectContainsPoint(boundBox, touchLocation)) {   
        found = YES;
        imageStr = @"help-move.png";
        text = @"HELPMOVE";
    }

    boundBox = CGRectMake(10, 280, 150, 50);  //Location of Fight Button
    if (CGRectContainsPoint(boundBox, touchLocation)) {   
        found = YES;
        imageStr = @"help-fight.png";
        text = @"HELPFIGHT";
    }

    boundBox = CGRectMake(60, 230, 50, 50);  //Location of Trade Button
    if (CGRectContainsPoint(boundBox, touchLocation)) {   
        found = YES;
        imageStr = @"help-trade.png";
        text = @"HELPTRADE";
    }

    boundBox = CGRectMake(10, 180, 50, 50);  //Location of Cure Button
    if (CGRectContainsPoint(boundBox, touchLocation)) {   
        found = YES;
        imageStr = @"help-cure.png";
        text = @"HELPCURE";
    }

	... //Other stuff if more needed. 

    [infoLayer removeAllChildrenWithCleanup:YES]; //Remove any prior message.

    if (found == YES) {
        processed = YES;
    	CCSprite *bg = [CCSprite spriteWithFile:@"bg_info_message.png"];
    	CCSprite *image = [CCSprite spriteWithFile:imageStr];
    	CCLabelTTF *label = [CCLabelTTF labelWithString: text fontName:@"Arial" fontSize:12];
    
    	label.position = ccp(0,100);
    
    	[infoLayer addChild:bg z:-1];
    	[infoLayer addChild:image z:1];
    	[infoLayer addChild:label z:2];
    }
    
    return processed;
}

Finished product

Of course, the actual help text, images and backgrounds, and their positions need to be adjusted to your use, but this should be adequate to get you very near a tailored solution.

Our solution seen in a couple of images:

Where to go from here

If the help system needs to be used on sprites/objects that move on the game surface, then above solution will not work in the slightest. You will need to interact with your game play layer and reference all the actual sprites you want to give information about.

To do this, you must assign all the 'interesting' sprites to an array and go through the array of sprites checking for collisions between the touch and the sprite's bounding box.

In some init function add the sprites to a help array:

-(id) init {
	... //regular init code
	helpSprites = [NSMutableArray arrayWithCapacity:2];
	[helpSprites addObject:…….]; //Repeat for all objects that will need help information
	...
}
 

Then in the 'processTouch' method for the HelpLayer class you would loop through the sprites looking for a collision:

	id foundSprite;
	for (id *sprite in gameLayer.helpSprites) {        
        
        //CGPoint boundOrigin = 
        CGPoint worldCoord = [sprite.infectionOverlay convertToWorldSpace: sprite.position];

        CGRect boundBox = CGRectMake(worldCoord.x, worldCoord.y, sprite.boundingBox.size.width, sprite.boundingBox.size.height);
        
        if (CGRectContainsPoint(boundBox, touchLocation)) {   
            foundSprite = sprite;
            break;
        }
    }
 
	//Determine which kind of sprite it is and display the appropriate help message.

A tailored mechanism like this will allow you to quickly add objects to the help system, and detect them anywhere on the game layer they might exist. Such code is much more maintainable because if you move any UI items later in the game design, the code isn't broken (unlike the original implementation which forces you to sync the boundBox to the location of items, which might be changed later). But this system is much more complex to setup.

Gladly, this entire process took less than a day to implement (minus cool help graphics to add), so no game should be w/o at least a rudimentary help system).

Comment below, or with Twitter,

Screenshot taken from our Eradicate project (view all posts here). Also visit Skejo Studios to see all we are doing there.