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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Pairing the iCade with Your iOS Device
On your iPad (or iPhone – this all works there as well), go into your Settings app and choose General, then Bluetooth. Switch Bluetooth on if it’s not already. Your iPad will say Searching… under the devices list. Now we’ll go to the iCade.
Press the bottom four buttons and the top white button (far right) and hold them down at the same time. This puts the iCade into pairing mode. The coin slot will start to blink if this is done right.
On your iPad, an alertview will pop up and give you a four-digit pin to enter into the iCade. This is done with the buttons and the joystick. There’s a graphic that illustrates the mapping between the numbers and the buttons.
Once you’ve put in the pin and pressed enter (either white button), the iPad should have a device in the list named ‘ION iCade Game Controller,’ and the status should be connected. If it’s not connected, choose the device from the devices list on the iPad, and iPad will search for it.
Also, the iCade may have turned itself off. Press any button on the iCade to turn it back on. You’ll know it’s on because the coin slot will be lit. That should do it – you are now paired with your iCade.
All of these pairing instructions are written on the underside of the top of the iCade cabinet, along with the graphic that shows the numbers mapping for the pin entry.
Keep in mind that this is a Bluetooth keyboard, so any text input will no longer bring up the onscreen keyboard. In order to get it back, you’ll have to disconnect from the iCade, so download any games or anything that requires you type before you do all of this. The easiest way (that I’ve found) to disconnect the iCade is to turn Bluetooth off.
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:
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:
- 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.
- 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.