iOS 7 Game Controller Tutorial
Learn how to add control your games with a joystick in this iOS 7 game controller tutorial! 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
iOS 7 Game Controller Tutorial
45 mins
- Supported Game Controller Types
- Game Controller Design Considerations
- Getting Started
- Connecting Hardware Controllers
- Connecting the Thumb Stick
- Connecting in the Middle of a Game
- Supporting the Pause Button
- Serializing Controller Inputs
- Playing Back Serialized Controller Data
- Setting the LED Player Indicator
- Where To Go From Here?
Connecting the Thumb Stick
The next step is to connect the thumb stick to the controller. You already have a custom getter method that accesses the state of the on screen thumbstick, so you’ll be adding a little more code and reading (instead of using a change handler) the hardware left thumbstick input instead.
Change the xJoystickVelocity
method to the following:
- (float)xJoystickVelocity {
//1
if (self.controller.extendedGamepad) {
return self.controller.extendedGamepad.leftThumbstick.xAxis.value;
//2
} else if (self.controller.gamepad) {
return self.controller.gamepad.dpad.xAxis.value;
}
//3
return self.joystick.velocity.x / 60.0;
}
- You first check for the extended gamepad profile, if it exists, you return the value of its left thumbstick. Apple requires that controller readouts be standardized and have appropriate dead zones, so you don’t have to do any preprocessing of the values coming back from the controller. By the time you retrieve the value, you can just use it.
- If you don’t find an extended controller, you check for a gamepad profile. If it exists, you return the value of the dpad. All buttons are pressure sensitive, so the dpad will also return a value between -1.0 and 1.0 instead of just booleans for left/right.
- If you don’t have either a gamepad or extended gamepad, you revert to accessing the on screen HUD joystick class to retrieve a value. This code already existed in the method.
Go ahead and build and run now. You should now have full control over your Koala.
Not bad for just a few lines of code, eh? In my opinion this also makes the game much more fun than the on-screen controls as well.
Connecting in the Middle of a Game
The next step is to make your app respond to controller being connected and disconnected during gameplay. This is done with a system notification. First add a couple properties to keep track of the observers and remove them when the class is deallocated. Add this to HUDNode.m, @interface
section:
@property (nonatomic, strong) id connectObserver;
@property (nonatomic, strong) id disconnectObserver;
Next, add the following code in the initWithSize method of the HUD at the ‘//Add observers here’ comment:
__weak typeof(self) weakself = self;
self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
if ([[GCController controllers] count] == 1) {
[weakself toggleHardwareController:YES];
}
}];
self.disconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
if (![[GCController controllers] count]) {
[weakself toggleHardwareController:NO];
}
}];
The GCControllerDidConnectNotification and GCControllerDidDisconnectNotification notifications are fired when a new controller is connected or disconnected. If a new controller is connected and the count of controllers is 1, you can conclude that there were no connected controllers before the notification fired and you call your routine that hides the HUD and sets up the controller. When the disconnect notification is fired, you check to see whether there are any controllers left, and if there are none, you call that method to reveal the HUD and remove the GCController.
Pretty straight forward. Finally, when using the block methods for NSNotificationCenter you need to remove those observers in the dealloc
method or they will cause a leak (they’ll retain the HUDNode object).
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
}
Build and run now. You should be able to start the app without a controller, then watch the HUD disappear when you connect and reappear when you disconnect your controller. This is much easier if you have a bluetooth connected controller, but will work in either case.
Supporting the Pause Button
The last thing you must do in order to support gamepads – and this is a requirement from Apple – is add support for the pause button.
I’m going to use NSNotificationCenter to communicate between the HUDNode and the SKView/SKScene. A delegate protocol would work as well, but this kind of event seems more suited to a notification to me. Add the following line of code to HUDNode.h before the @interface
line:
extern NSString * const kGameTogglePauseNotification;
Then add this line before @interface in HUDNode.m:
NSString * const kGameTogglePauseNotification = @"GameTogglePauseNotification";
This is just the NSString name of the notification. Using const
like this just makes it easier not to make a mistake typing (and copying/pasting) the string into multiple places. Then switch to the ViewController.m and add a new property to keep track of the observer for removal:
@property (nonatomic, strong) id pauseToggleObserver;
You need to #import
the HUDNode.h file to get access to the notification NSString const that you just created:
#import "HUDNode.h"
Then, add this to the end of viewDidAppear:
__weak typeof(self) weakself = self;
self.pauseToggleObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kGameTogglePauseNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[weakself togglePause];
}];
This just creates the new NSNotification observer that will fire when the notification is fired. Next, create a dealloc
method to remove that observer:
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self.pauseToggleObserver];
}
Now, in that same file, add the pause method:
- (void)togglePause {
SKView *view = (SKView *)self.view;
view.paused = (view.paused) ? NO : YES;
if (!view.paused) {
self.pauseView.hidden = YES;
} else {
self.pauseView.hidden = NO;
}
}
I’ve added a hidden UIImageView with a pause image to the storyboard already. It’s called pauseView
.
I would prefer to use an SKLabelNode or SKSpriteNode to add a pause label or button the the scene, but once you set self.view.paused = YES
, nothing renders in the SKView after that. So you never see the new node or label that you’ve added. There are ways around this issue, but to keep it simple I just used UIKit.
Now, you need to add the code that sends the notification. In HUDNode, in toggleHardwareController
, there’s a comment line ‘//Add controller pause handler here’, replace that comment with this code:
[self.controller setControllerPausedHandler:^(GCController *controller) {
[[NSNotificationCenter defaultCenter] postNotificationName:kGameTogglePauseNotification object:nil];
}];
Build and run now. Press pause button. If everything is in place, you should see something like this (and the game should be paused):
Serializing Controller Inputs
The Game Controller framework has one more capability that you are going to explore. You can serialize (convert to NSData to be saved in a plist or sent over the network) the state of the controller.
This ability can be used in different ways. For example, you could use this feature to send the controller state across the network to another player, or you can save the entire history of inputs to a file. In this tutorial, you’ll be recording and playing back the sequences of input you use to progress through the level.
The first step is to add a boolean to indicate to the HUD class that it is in snapshot recording mode. Add the following in the @interface
section of HUDNode.h:
@property (nonatomic, assign) BOOL shouldRecordSnapshots;
Now, add a new NSMutableArray that will contain the snapshots to HUDNode.m @interface
:
@property (nonatomic, strong) NSMutableArray *snapShots;
The next step is to designate a file that the snapshots can be saved to. Open HUDNode.m and add the following two methods (end of the file):
- (NSURL *)snapShotDataPath {
//1
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *filePath = paths[0];
filePath = [filePath stringByAppendingPathComponent:@"snapshotData.plist"];
//2
return [NSURL fileURLWithPath:filePath];
}
- (void)saveSnapshotsToDisk:(NSArray *)snapShots {
//3
if (![snapShots count]) return;
//4
if (![snapShots writeToURL:[self snapShotDataPath] atomically:YES]) {
NSLog(@"Couldn't save snapshots array to file");
}
}
- These two methods make it easier to work with the file representation of the snapshots that you’ll create. The first method just retrieves the NSURL that you’ll use to save and load data to and from. You’ll be referring to this URL location more than once, so it’s better to give it its own method. First, you get the file path that you’ll use to store the plist. You are putting the file into the app’s documents directory.
- Then, you return an NSURL form of the path string you just created.
- In the second method, you will be saving the array of NSData snapshots that you create. In this first part, you don’t want to save the array if it’s empty, so you check for that and return if there aren’t any snapshots.
- Finally, you call
writeToURL
on the snapshots array (a method that saved an array to disk in plist form). This method returns aBOOL
indicating whether the operation was successful or not. If it fails, you want to let yourself know so you can do further investigation.
Now, you are ready to write the code that creates the snapshots and adds them to the array. You need to generate one snapshot per frame. You want to find a place in your code that is called once per frame, every frame. The touch methods aren’t going to work, because they are called when the touches change, so there would be many frames where no touch methods would fire.
I chose to use the xJoystickVelocity
method in HUDNode.m. Every frame, the player’s update method calls this method (once per frame) to get the state of the joystick. Change the following block of code:
//1
if (self.controller.extendedGamepad) {
return self.controller.extendedGamepad.leftThumbstick.xAxis.value;
//2
} else if (self.controller.gamepad) {
return self.controller.gamepad.dpad.xAxis.value;
}
To this:
if (self.controller.extendedGamepad) {
//1
if (self.shouldRecordSnapshots) {
//2
NSData *snapShot = [[self.controller.extendedGamepad saveSnapshot] snapshotData];
//3
NSDictionary *snapshotDict = @{@"type": @"extended", @"data":snapShot};
//4
[self.snapShots addObject:snapshotDict];
}
return self.controller.extendedGamepad.leftThumbstick.xAxis.value;
} else if (self.controller.gamepad) {
//5
if (self.shouldRecordSnapshots) {
NSData *snapShot = [[self.controller.gamepad saveSnapshot] snapshotData];
NSDictionary *snapshotDict = @{@"type": @"gamepad", @"data":snapShot};
[self.snapShots addObject:snapshotDict];
}
return self.controller.gamepad.dpad.xAxis.value;
}
- The first thing to do is check whether the
shouldRecordSnapshot
boolean isYES
. - Next, you create the NSData representation of the snapshot. You create a snapshot by calling
saveSnapshot
on the gamepad profile object. In this case theextendedGamepad
profile. The GCExtendedGamepadSnapshot that’s created by callingsaveSnapshot
is an object that you can query the snapshot buttons the same way to do the controller profile object. More on that in a bit. Once you have the snapshot, you need to convert it to NSData so it can be saved to a plist. You do that by callingsnapshotData
. - Then, you create a dictionary to contain the NSData. When you load these snapshots to read them back later on, you will need to know what kind of snapshot it is in order to convert it from NSData back into a snapshot object (either a GCGamepadSnapshot or a GCExtendedGamepadSnapshot). You won’t know just by looking at the NSData which type it is. So, you create a dictionary that contains a “type” key so you can determine which you need to initialize with the NSData.
- Finally, you add that dictionary to the snapshots array.
- This second block is identical to the one I just covered, except that you are saving a GCGamepadSnapshot, so the “type” is “gamepad”.
You need a method that starts the recording process. Add this to HUDNode.m (at the end):
- (BOOL)recordSnapshots {
if (!self.controller || self.shouldRecordSnapshots) return NO;
self.shouldRecordSnapshots = YES;
self.snapShots = [NSMutableArray array];
return YES;
}
Here, you are checking that there’s a controller connected. You don’t want to enable snapshot recording without a controller. Also, if you are already in recording mode, you don’t want to enable it again, or you’ll erase all the snapshots you’ve collected up to that point.
Then you set shouldRecordSnapshots
to YES
and create a new array for the snapshots object.
You are returning a BOOL
from this method. This is a way to tell if the recording mode successfully started.
I’m not going to be creating UI to start/stop the recording mode or the playback mode. I’ll have you do all that in code. In a real game, you’d want buttons or a settings pane to enable these options. Returning a boolean from this method makes it easier to change the state of that UI (like turning a recording button to a YES
state).
Now, add this line to the very end of HUDNode.m, initWithSize:
[self recordSnapshots];
That is all you need to do in order to retrieve snapshots and store then in an array. However, you still need to call the method that saves the snapshots to a plist on the disk. For simplicity, you’re going to be calling that when the player wins the game (if he dies before reaching the end of the level, that run won’t be saved).
Find this line in GameLevelScene.m, gameOver:
gameText = @"You Won!";
Add these lines immediately after it:
if (self.hud.shouldRecordSnapshots) {
[self.hud saveSnapshots];
}
There’s one last thing you must do, create the saveSnapshots
method. You already have a saveSnapshotsToDisk
method, but that one must be called internally (to have access to the private snapshots array).
Add this method declaration to HUDNode.h:
- (void)saveSnapshots;
Now, add this method to the end of HUDNode.m:
- (void)saveSnapshots {
[self saveSnapshotsToDisk:self.snapShots];
}
That’s it. You can now build and run. Make sure when you start the game, the controller is already connected, or the recordSnapshots
method with return without enabling the function. Play through the level and make sure you win!
When you are done, you’ll have a new plist inside the app’s bundle on the device. In order to validate that this worked correctly, you’ll need to use a program that allows you to browse all the contents of your iPhone, not just the pictures. I use iExplorer. Navigate to the app, find the Documents folder, and you should have a snapshotsData.plist file that looks like this:
If you don’t have iExplorer or a program that can navigate the device’s file system, you can still proceed. The next build and run step will validate whether or not you’ve got the recording part working right.