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.

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

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.

No controls

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):

IMG_2726

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");
  }
}
  1. 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.
  2. Then, you return an NSURL form of the path string you just created.
  3. 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.
  4. Finally, you call writeToURL on the snapshots array (a method that saved an array to disk in plist form). This method returns a BOOL 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;
}
  1. The first thing to do is check whether the shouldRecordSnapshot boolean is YES.
  2. Next, you create the NSData representation of the snapshot. You create a snapshot by calling saveSnapshot on the gamepad profile object. In this case the extendedGamepad profile. The GCExtendedGamepadSnapshot that’s created by calling saveSnapshot 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 calling snapshotData.
  3. 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.
  4. Finally, you add that dictionary to the snapshots array.
  5. 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!

IMG_2771

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:

Screen Shot 2014-02-24 at 9.46.33 AM

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.

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.