Adding iCade Support to Your Game

This is a blog post by iOS Tutorial Team member Jacob Gundersen, an indie game developer who runs the Indie Ambitions blog. Check out his latest app – Factor Samurai! The iCade is a miniature arcade cabinet for your iPad. It communicates with the iPad over Bluetooth, and allows you to play iCade-compatible games with […] By Jake Gundersen.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Testing the iCade Library

Next, open up the iCadeTest project you downloaded from Github and run it on your iPad in debug mode.

You should see a UI representation of the iCade controller on the screen like this:

iCadeTest program

As you move your joystick controller, you should see the UI update appropriately. For example, in the screenshot above I have the upper right white button pressed down.

If this works, you’re finally ready to integrate iCade into our simple game!

Adding a Touchscreen Controller

If you haven’t already, download the starter project and open it up in Xcode.

The first thing you’re going to do is remove the touch responder methods from ActionLayer.mm. Go ahead and comment out touchesBegan, touchesMoved, touchesEnded, and touchesCancelled. You can also remove self.isTouchEnabled = YES; from – (id)initWithHUD:(HUDLayer *).

Later we’ll be adding new methods that move and jump the player, but for now we’re moving the touch responder to the HUD layer.

You’re now going to add the buttons to the HUD layer. Open HUDLayer.h and add the following instance variables so your code looks like this:

@interface HUDLayer : CCLayer {
    CCLabelBMFont * _statusLabel;
    CCSprite *leftButton;
    CCSprite *rightButton;
    CCSprite *jumpButton;
    
    NSArray *buttons;
}

- (void)showRestartMenu:(BOOL)won;
- (void)setStatusString:(NSString *)string;

@end

Then change init in HUDLayer.mm to the following:

- (id)init {
 	if ((self = [super init])) {
		self.isTouchEnabled = YES;
        
        leftButton = [CCSprite spriteWithFile:@"leftButton.png"];
        rightButton = [CCSprite spriteWithFile:@"rightButton.png"];
        jumpButton = [CCSprite spriteWithFile:@"jumpButton.png"];
        
        buttons = [[NSArray alloc ] initWithObjects:leftButton, rightButton, jumpButton, nil];
        
        for (CCSprite *s in buttons) {
            s.opacity = 127;
        }
         
        CGSize winSize = [CCDirector sharedDirector].winSize;
        
        _statusLabel = [CCLabelBMFont labelWithString:@"" fntFile:@"Arial.fnt"];
        leftButton.position = ccp(50, 50);
        rightButton.position = ccp(150, 50);
        jumpButton.position = ccp(440, 50);
            
        leftButton.scale = 0.5;
        rightButton.scale = 0.5;
        jumpButton.scale = 0.5;
       
        
        [self addChild:jumpButton];
        [self addChild:leftButton];
        [self addChild:rightButton];
        
        _statusLabel.position = ccp(winSize.width* 0.85, winSize.height * 0.9);
        [self addChild:_statusLabel]; 
	 }
    return self;
}

Nothing earth shattering here: just adding the buttons. You’re setting the opacity to half (127) and you’ll set it back to full (255) when a button is pressed. Also, you’re adding the buttons to an array, just to make it easier to process touches later on.

If you look at the images in a photo editor, you’ll see that they’re surrounded by a bunch of transparent space. This is to make it easier to press the buttons.

Build and run now. You should have a screen that looks like this:

If you try to touch buttons, nothing happens. To fix that, add the following touch code methods, starting with the two simpler ones, touchesBegan and touchesEnded:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {    
    for (UITouch *t in touches) {
        
        CGPoint touchLocation = [self convertTouchToNodeSpace:t];
        
        for (CCSprite *s in buttons) {
            if (CGRectContainsPoint(s.boundingBox, touchLocation)) {
                s.opacity = 255;
                int buttIndex = [buttons indexOfObject:s];
                if (buttIndex == 2) {
                    [delegate heroJump];
                } else if (buttIndex == 1) {
                    [delegate heroMove:kDirectionRight];
                } else if (buttIndex == 0) {
                    [delegate heroMove:kDirectionLeft];
                }
                
            } 
        }
    }
}

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    
    for (UITouch *t in touches) {
        
        CGPoint touchLocation = [self convertTouchToNodeSpace:t];
        
        for (CCSprite *s in buttons) {
            if (CGRectContainsPoint(s.boundingBox, touchLocation)) {
                s.opacity = 127;
                int buttIndex = [buttons indexOfObject:s];
                
                if (buttIndex == 1 || buttIndex == 0) {
                    [delegate heroMove:kDirectionNone];
                } 
            }
        }
    }
} 

These two methods are mirror images of each other. You are using multitouch, iterating through the whole set of touches. You’ll need to be able to hit a direction and jump at the same time. You are also iterating through your buttons and looking at whether your touch begins or ends inside of a button sprite.

If you get a hit, you first change the opacity of the button. Next, you test which button you are currently working with by its index in the array, and then you send the appropriate message to your delegate based on whether you’re hitting left, right, jump, or releasing any of these buttons.

We’ll get to your delegate protocol in a minute.

These methods take care of touches that start and end on a single button. To deal with the case where a touch starts on one button and ends on another, we need to add a ccTouchMoved callback.

This callback needs to deal with two situations:

  1. What if the player touches the right button, and then slides onto the left button? In this case, we want the right button to turn off, and the left button to turn on.
  2. What if the player slides onto the jump button? In this case (and this is a design decision – you could choose to do otherwise), there shouldn’t be another jump. The player needs to release the screen and start a new tap to trigger a second jump.

This behavior better mirrors the interface of a physical controller: sliding between directional buttons changes direction, but a new jump action requires a release and press.

So to accomplish this, add the ccTouchesMoved callback like so:

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *t in touches) {
        
        CGPoint touchLocation = [self convertTouchToNodeSpace:t];
        
        //get previous touch and convert it to node space
        CGPoint previousTouchLocation = [t previousLocationInView:[t view]];
        CGSize screenSize = [[CCDirector sharedDirector] winSize];
        previousTouchLocation = ccp(previousTouchLocation.x, screenSize.height - previousTouchLocation.y);
        
        for (CCSprite *s in buttons) {
            if (CGRectContainsPoint(s.boundingBox, previousTouchLocation) && 
                !CGRectContainsPoint(s.boundingBox, touchLocation)) {
              
                s.opacity = 127;
                int buttIndex = [buttons indexOfObject:s];
                
                if (buttIndex == 1 || buttIndex == 0) {
                    [delegate heroMove:kDirectionNone];
                } 
            }
        }
        
        for (CCSprite *s in buttons) {
            if (!CGRectContainsPoint(s.boundingBox, previousTouchLocation) && 
                CGRectContainsPoint(s.boundingBox, touchLocation)) {
                
                s.opacity = 255;
                int buttIndex = [buttons indexOfObject:s];
                
                //We don't get another jump on a slide on, we want the player to let go of the button for another jump
                if (buttIndex == 1) {
                    [delegate heroMove:kDirectionRight];
                } else if (buttIndex == 0) {
                    [delegate heroMove:kDirectionLeft];
                }
                
            } 
        } 
    }
}

This method is a hybrid of the two previous ones. One difference is that there are two touch locations. You’re getting the previous touch location along with the current one.

The first part tests when the player slides off a button they were previously touching. If that’s the case, we want to turn that button back to half opacity and send a message to the delegate that the player is no longer touching a direction button.

There’s no need to know which direction button has been released, so the one message will do. (We’re assuming that pressing the new direction happens after releasing the old one. If this isn’t true, we might accidentally turn off the new direction press).

You don’t need to send a message that the player is no longer pressing the jump button, because you’re applying an impulse on the first touch of the jump button, and not sending another until the button has been released and touched again.

The second block of code tests whether the player has pressed a new button. In this case, you do need to know which button is being pressed, so you find the position in the array and send the appropriate message.

For the touch methods to work properly, you need to turn on multitouch. If you don’t, the player can only touch one button at a time.

Turning on multitouch is done in AppDelegate.mm. Find the line that runs the first scene, [[CCDirector sharedDirector] runWithScene: [ActionLayer scene]]; and add the following line before it:

[glView setMultipleTouchEnabled:YES];

One last thing you need to do in order to run this code (otherwise you’ll get an error) is add your delegate protocol. Change HUDLayer.h to the following:

#import "cocos2d.h"

typedef enum {
    kDirectionLeft,
    kDirectionRight,
    kDirectionNone
} ControlDirection;

@protocol ControlsDelegate

-(void)heroMove:(ControlDirection)direction;
-(void)heroJump;

@end

@interface HUDLayer : CCLayer {
    CCLabelBMFont * _statusLabel;
    
    CCSprite *leftButton;
    CCSprite *rightButton;
    CCSprite *jumpButton;
    
    NSArray *buttons;
    
    id <ControlsDelegate> delegate;
}

- (void)showRestartMenu:(BOOL)won;
- (void)setStatusString:(NSString *)string;

@property (assign) id <ControlsDelegate> delegate;

@end

In the above code, first you add an enum that will be used to send the direction of the button to the delegate. Then you create the protocol.

You add two methods to the protocol, one for jump and one for direction. Then you add an instance variable for your delegate, and a property so it can be set by its parent.

The only other thing you must do is add @synthesize to the HUDLayer.mm:

@synthesize delegate;

You can now build and run. Your buttons won’t yet make the hero move, because you still need to implement the logic on the delegate end, but they should now respond to touch by changing opacity.

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.