Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4396

iOS 7 Game Controller Tutorial

$
0
0
Learn how to add hardware game controller support into your iOS games!

Learn how to add hardware game controller support into your iOS games!

iOS 7 introduced official support for hardware game controllers. This is great because some games are particularly well suited for hardware game controllers – imagine Mario with a touch interface!

It’s also great because now that it’s official, you can support a variety of game controllers just by implementing a single API.

On top of that, Apple has provided prospective controller manufacturers with specific requirements in order to be compatible with iOS. Apple isn’t just letting just any yahoo make a hardware controller for iOS!

In this tutorial, you will add hardware game controller support into a simple platformer game like Mario, made with Sprite Kit.

This tutorial has the following prerequisites:

  • Some basic Sprite Kit experience. If you are new to Sprite Kit, I recommend checking out our Sprite Kit Tutorial for Beginners first.
  • A hardware iOS game controller. There’s no way to test game controllers in the simulator, so you’ll need a hardware iOS game controller for testing. If you’re wondering which one to get, I recommend picking up the Stratus SteelSeries Wireless Gaming Controller.
  • Review starter game (optional). As I mentioned, you will be adding game controller support into a simple Sprite Kit platformer game like Mario. If you’d like to learn how to make this game, we have a tutorial for that.

Grab your game pad and let’s get started!

Supported Game Controller Types

There are exactly three game controller types supported by the Game Controller framework, as specified in Game Controller Programming Guide:

1) A standard form-fitting controller

The form-fitting controller encases the iPhone within the controller, connecting via the lightning connector.

At the time of writing this article, there is only one of these types of controllers available – Logitech’s Powershell controller.

Logitech Powershell

This type of controller supports the “standard” controller profile, which means it has the following inputs:

  • Direction Pad (always on the left)
  • 4 buttons A, B, X, Y (always on the right>
  • 2 shoulder buttons, one on the left and one on the right
  • Pause button

2) An extended form-fitting controller

The second type of controller is also form-fitting, but it contains some extra buttons so is considered “extended.”

At the time of writing this article, there is only one of these types of controllers available – MOGA’s ACE Power controller.

MOGA

Note that an extended controller like this one contains some additional buttons:

  • Two thumbsticks, one left and one right
  • Additional pair of shoulder buttons/triggers

3) An extended wireless controller

The final controller type is the external/bluetooth controller, such as Stratus’s SteelSeries controller.

This supports the extended gamepad (I believe that external controllers are all required to support the extended profile, that’s my impression from the WWDC videos in any case).

SteelSeries

The external controllers connect via bluetooth and must first be paired to the device in order work. Once paired, any time the controller is powered on it will connect via bluetooth to the same device.

The full collection

Here’s an image of my current hardware collection:

Controllers

In this image:

  • Upper-middle: The iCade: A legacy game controller that came out before the iOS 7 game controller API was released.
  • Upper-left: Stratus SteelSeries A extended wireless controller.
  • Upper right: Logitech Powershell: A standard form-fitting controller.
  • Lower left: MOGA’s ACE Power controller: An extended form-fitting controller.
  • Lower right: Xbox 360 controller: Don’t get excited, these don’t work on iOS! :] I just included it in the picture here for size comparison purposes.

Common characteristics

Beyond specifying the controller types, Apple specifies certain characteristics that all controllers must satisfy. These are intended to provide a consistent, high quality experience when using a hardware controller with an iOS device.

If you would like to have a complete picture of the benefits of Apple’s requirements, I suggest watching the video on hardware controllers from WWDC 2013. But, let me just mention a few things:

  1. All buttons are pressure sensitive
  2. Thumb sticks must have no dead zones or drift
  3. Controllers must have LED indicators that indicate player number
  4. A, B, X, and Y buttons always have the same color

Game Controller Design Considerations

Apple has a few requirements that you must satisfy in order to submit an app that supports a hardware controller. The first, and most important, is that you must not require a hardware controller. Your game must be playable with standard touch/tilt controls.

This means that you have to put thought into how you are going to implement your control scheme. An Extended Gamepad has a total of 13 controls, good luck fitting all of those controls on a touch screen!

Don't do this!

Don’t do this!

Because of these issues, many games provide completely different controls schemes on a hardware controller versus the touch screen.

For example, LEGO Star Wars uses the hardware controller to move around the player in 3D space, but if you use the touch screen, you simply touch on the screen where you want the player to go, and he will move to that spot.

You will have to be creative to make a game that takes advantage of the hardware controller, but still plays well with touch screen controls. You don’t want to design for the hardware and then add touch screen controls as an after thought. Realistically, most of your users will be playing your game via the touch screen, so you want both experiences to yield praise and good reviews.

Getting Started

As I mentioned earlier, in this tutorial you will be adding game controller support to the Super Koalio game from our Sprite Kit platformer game tutorial.

In the original tutorial, Super Koalio has a very simple control scheme. Tapping on the left side of the screen walks forward (forward only) and tapping on the right side of the screen jumps.

However, I have created a modified version of the starter project that contains a more complete set of controls and on screen buttons and joystick.

Download the starter project, build and run, and try out the new control scheme.

Final Controls

The new control scheme uses an analog stick (provided on github by the sneaky narwal – you can get that code here), and two buttons:

  • Dash button: This enables Koalio to reach a much higher top speed. You won’t need this in the hardware controller because the analog stick will enable you to move Koalio at a much wider variety of speeds, however you need a solution for touch players, hence this button. Pressing dash makes the Koala change size/shape and shades his image darker to give visual feedback of his state.
  • Jump button: The jump buttons is simple, but often you want to hold dash and press jump and the same time. Because a touch point is a single point rather than the entire surface area of your thumb, you need something else in order to use both buttons. In my scheme, if you press the B button, it stays pressed until you release that touch. That way, you can start on B and slide onto and off of the A button and the B button stays pressed.

Note that the control layer is a separate class called HUDNode. The hardware controller will communicate with the HUD.

Connecting Hardware Controllers

First, make sure your controller is on and paired to your device, following the instructions that came with your controller.

Next, you’ll add some code to connect your game with the controller. Start by opening HUDNode.m and importing the GameController framework:

@import GameController;

Then, add a new GCController property to your private interface:

@property (nonatomic, strong) GCController *controller;

Now you can add the code that interacts with the hardware. Add the following method to HUDNode.m:

- (void)toggleHardwareController:(BOOL)useHardware {
  //1
  if (useHardware) {
    //2
    self.alpha = 0.0;
    //3
    self.controller = [GCController controllers][0];
    //4
    if (self.controller.gamepad) {
      //5
      __weak typeof(self) weakself = self;
      [self.controller.gamepad.buttonA setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) {
        weakself.aPushed = pressed;
      }];
      [self.controller.gamepad.buttonX setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) {
        weakself.shouldDash = pressed;
      }];
      //Add controller pause handler here
    }
  } else {
    //7
    self.alpha = 1.0;
    self.controller = nil;
  }
}
  1. First, you are passing in a boolean to this method. Passing in YES will initiate and set up the methods to handle the input from the controller. It will also hide the on screen HUD. Passing NO will reveal the on screen HUD and reset the controller.
  2. In this part you are just hiding the on screen controls by setting the entire node’s alpha to 0.0.
  3. Here, you set the property to the GCController object that controls the first controller connected. GCController has a controllers class method that returns an array of all the connected hardware. In this app you will only ever communicate with the first controller in the array. You can connect up to four controllers to a single iOS device.

    There are two profiles. The gamepad profile and the extended gamepad profile. The extended gamepad profile has all the controls that the gamepad profile has plus a bunch more. In this app you are going to support both profiles by adding code that responds to both the dpad and the left analog stick. But, for now you are just adding support for the A and B buttons.

  4. In this section you check for the presence of the gamepad profile. If you need to know what profile your controller supports you check for the .gamepad property or the .extendedGamepad property. If the property is not supported, accessing the property will return nil.
  5. Next, you set up the change handlers that will be called when the button is pressed. In this case, it’s very simple. You set the aPushed (or shouldDash) property to the value of pressed.
    Each button or control has a property on the gamePad (or extendedGamePad) object.

    There are two ways to access the game controller object for input. First, you can query the state of these properties and read the state of the various buttons and controls. Alternatively, you can set the changeHandler which executes a block every time the state changes. For the buttons, you use change handlers.

    I’ve mapped the A button on the controller to the A button in the HUD and the X button on the controller to the B button in the HUD. This is the most comfortable way to use both dash and jump on the physical control.

  6. If the method is passed NO, you reset the alpha property to 1.0 (revealing the HUD) and set the controller property to nil.

Now that you have that method in place, it’s time to connect to a controller. First, you will handle the case when a controller is already connected when the app starts. Make sure your bluetooth enables controller is paired with your device or plug your lightning connected controller in (when it’s time to test again).

Find the comment ‘//Add hardware controller code here’ in initWithSize and add this code:

if ([[GCController controllers] count]) {
  [self toggleHardwareController:YES];
}

This will check for any existing controllers and if it finds one or more, it will run the method you just added.

Note: If you have a controller that connects to the lightning cable, testing gets tricky because you have to disconnect your Xcode connected cable to plug your controller in. So, from now on, testing will be several steps:

  1. Build and run.
  2. Disconnect from your Mac.
  3. Connect the controller.
  4. Run your app by tapping on it.

You won’t be able to get logs in that case either, so you may need to create a logging SKLabelNode or do something else to debug issues. You won’t be relying on any specific NSLog statements in this tutorial, but if you get stuck or need to debug something, you won’t be able to do that with a lightning connected controller. If you have a bluetooth controller this will be easier.

Go ahead and connect your controller and run the app. The HUD should be gone and you should be able to dash and jump using the buttons.

Button response

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;
}
  1. 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.
  2. 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.
  3. 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.

Hardware control

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.

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.

Playing Back Serialized Controller Data

The next step is to play back those snapshots.

You’ll need some new instance variables, a boolean to indicate whether you are in replay mode and a index number of the current snapshot in the array. Add these two properties to HUDNode.m, @interface section:

@property (nonatomic, assign) NSUInteger currentSnapshotIndex;
@property (nonatomic, assign) BOOL shouldReplaySnapshot;

Next, set add the replaySnapshots method:

- (BOOL)replaySnapshots {
  //1
  self.snapShots = [NSArray arrayWithContentsOfURL:[self snapShotDataPath]];
  //2
  if (!self.snapShots || ![self.snapShots count]) return NO;
  //3
  self.shouldReplaySnapshot = YES;
  //4
  return YES;
}
  1. First, you initialize the snapshots object with the contents at the plist location. This gives you back the original array before you wrote it to disk in the previous section.
  2. If there isn’t a file at that location or if there’s an error parsing it, arrayWithContentsOfURL returns nil. You check for that next, or if there is a valid plist that’s initialized and it’s empty, then you return NO and you do not set the game into replay mode.
  3. However, if there is a valid data, you proceed to set shouldReplaySnapshot to YES.
  4. Finally, you return YES indicating that you are now in replay mode.

The next step is to convert the objects in the snapshots array back into GCGamepadSnapshot or GCExtendedGamepadSnapshot objects. Create a convenience method that parses the dictionary, initializes the right object of the two depending on the value in the “type” key, and returns whichever is correct.

Add this method to HUDNode.m:

- (id)currentGamepadFromSnapshot {
  //1
  NSDictionary *currentSnapshotData = self.snapShots[self.currentSnapshotIndex];
  //2
  id snapshot = nil;
  //3
  if ([currentSnapshotData[@"type"] isEqualToString:@"gamepad"]) {
    //4
    snapshot = [[GCGamepadSnapshot alloc] initWithSnapshotData:currentSnapshotData[@"data"]];
  } else {
    //5
    snapshot = [[GCExtendedGamepadSnapshot alloc] initWithSnapshotData:currentSnapshotData[@"data"]];
  }
  return snapshot;
}
  1. First, you retrieve the dictionary from the array based on the currentSnapshotIndex. You’ll be incrementing that index in another place to ensure that it’s only incremented once per frame.
  2. Next, initialize a generic object pointer. This needs to be generic because it can either be a GCExtendedGamepadSnapshot or a GCGamepadSnapshot object. You won’t know, so the pointer and the return type of the method are ‘id’.
  3. Next, you check the “type” entry in the dictionary to see if it’s “gamepad”, meaning that it’s a GCGamepadSnapshot type.
  4. If it is, you initialize a GCGampepadSnapshot object, using the “data” entry (the NSData from the snapshot) in the dictionary. Both types of snapshot contain the initWithSnapshotData method.
  5. If it isn’t a gamepad type, then it’s an extended profile, and you initialize that type of object. You then return whichever type you’ve created.

This method is just a way to compartmentalize code and make it easier to write the several bits that query the snapshot, using this common method in multiple places.

The next step is to change the way the inputs are queried if shouldReplaySnapshot is YES. First, modify xJoystickVelocity. Add this block of code to the beginning of that method (before all the existing code):

//1
if (self.shouldReplaySnapshot) {
  //2
  id currentSnapshot = [self currentGamepadFromSnapshot];
  //3
  self.currentSnapshotIndex++;
  //4
  if ([currentSnapshot isKindOfClass:[GCGamepadSnapshot class]]) {
    //5
    GCGamepadSnapshot *gamepadSnapshot = (GCGamepadSnapshot *)currentSnapshot;
    //6
    return gamepadSnapshot.dpad.xAxis.value;
  } else {
    //7
    GCExtendedGamepadSnapshot *extendedGamepadSnapshot = (GCExtendedGamepadSnapshot *)currentSnapshot;
    return extendedGamepadSnapshot.leftThumbstick.xAxis.value;
  }
}
  1. First, you check to see if shouldReplaySnapshot is YES. If it isn’t then the rest of the existing code runs as though you hadn’t added this new block.
  2. Next, retrieve the latest snapshot object using the method that you just built. This returns an id type, so, you still don’t know what kind of snapshot you’re getting. You’ll figure that out in a second.
  3. Third, you increment the currentSnapshotIndex variable. You know that this method, xJoystickVelocity, will be called exactly once per frame. So, it is the right place to increment the index of the snapshot to reliably get a new snapshot each frame.
  4. Then, you inspect the class type of the currentSnapshot object to see if it’s a GCGamepadSnapshot class using isKindOfClass. isKindOfClass is an NSObject method that you can use to determine which class type an object is at runtime.
  5. If you have a GCGamepadSnapshot, you cast that variable to that type so you can access its properties without a compiler error.
  6. Finally, you return the xAxis value from the dpad control.
  7. If you have an extended profile, you change two things, you must cast the snapshot to the GCExtendedGamepadSnapshot type and you ask for the leftThumbstick control instead of the dpad.

The only thing left to handle are the buttons. You may wonder how to do this, because currently, the player’s update method is just querying the state of the two booleans you created earlier, aPushed and shouldDash. There’s actually a really easy way to handle this, create a custom property accessor.

Add the following custom accessor method for shouldDash:

- (BOOL)shouldDash {
  //1
  if (self.shouldReplaySnapshot) {
    //2
    id snapshot = [self currentGamepadFromSnapshot];
    //3
    return [[snapshot valueForKeyPath:@"buttonX.pressed"] boolValue];
  }
  //4
  return _shouldDash;
}
  1. Check whether shouldReplaySnapshot is YES.
  2. If so, get the current snapshot from the array.
  3. This line is doing a couple things. You could have cast the snapshot to its type, either gamepad or extended gamepad, then access the buttons by their dot properties. But, that would take more code.

    The approach I’m using is Key Value coding. KVC is a way of accessing an object’s properties with an NSString matching the property’s name. KVC has a couple important methods, valueForKey and valueForKeyPath. If you need to access a property of a property, like you do here, use the valueForKeyPath. This means that you don’t have to know whether the object is a GCExtendedGamepadSnapshot or a GCGamepadSnapshot, because they both contain the same keys for buttons, you can ask for the value of that same key path from both objects.

    KVC returns an NSNumber representation of the BOOL instead of the BOOL value. So, if you were to return this without calling boolValue, you get a pointer to an NSNumber which would always evaluate to YES. That would give you a bunch of erroneous values, essentially it would interpret the snapshot as you pressing both buttons continuously.

    One word on Key Value Coding. It’s a very useful tool, and there are times when it can save you a lot of code. But, if you can use either dot property notation or Key Value Coding, normally it’s better to use dot property access instead. You get better compile time checking. If you misspell a key or ask for a key that doesn’t exist, with KVC your code will compile then crash at runtime. That wouldn’t happen with dot property notation. I’m using it in this case to bring it to the attention of those who’ve never used it, and because it saves me several lines of code. I’m able to avoid the branching and casting calls on the different types of snapshot objects. It happens to work because the property names on the different objects are the same.

  4. If you aren’t in shouldReplaySnapshot mode, then you just return the value of the properties backing instance variable, _shouldDash.

Go ahead and add the getter for aPushed. It follows identical logic to shouldDash:

- (BOOL)aPushed {
  if (self.shouldReplaySnapshot) {
    id snapshot = [self currentGamepadFromSnapshot];
    return [[snapshot valueForKeyPath:@"buttonA.pressed"] boolValue];
  }
  return _aPushed;
}

That’s it, your recording and replaying code should now all be in place. However, you’ll need to do some careful actions to test it. First, make sure you beat a game as mentioned in the previous section so it saves a valid snapshots plist file.

Then, find this line:

[self recordSnapshots];

Remove that line and replace it with:

[self replaySnapshots];

If you’ve done everything correctly, you will see your player performing the same motions, almost as if by magic, as you directed using your controller.

Moving without buttons

Note: Note that this is a simplistic method of recording screenshots, and suffers from timing variations between the frame rate at which the recording took place versus the frame rate at which the playback takes place.

Due to these variations, the simulation might not be exactly as you expect when you play back the recording. In a real app, you’d want to use a more advanced algorithm that takes into effect timing, and perhaps has periodic checkpoints of player state.

Setting the LED Player Indicator

One last minor thing. You can control the state of the LED lights on the controller. These lights are there to indicate which player is controlled by that controller.

In toggleHardwareController, right after this line:

self.controller = [GCController controllers][0];

Add this line:

self.controller.playerIndex = 0;

This will set the value on the controller for the playerIndex to 0. This does two things, it should set the LED indicator on the controller (on the SteelSeries it didn’t work consistently for me, but it worked fine on the MOGA).

Secondarily, it will remember the playerIndex for that controller. So, if you disconnect that controller, then reconnect it again later, that hardware will still identify itself to the GameController framework as playerIndex 0. If you want to clear the playerIndex (and I recommend that you do when you are finished with a controller), you set the playerIndex value to GCControllerPlayerIndexUnset. This constant is the default value when a controller is connected.

Build and run, and check out the LED!

Where To Go From Here?

You can download the final project here.

Now that you understand hardware controllers, you should add support into your games! There are more than a hundred games now that support hardware controllers, with hopefully many more to come.

To learn more about hardware controllers, check out Apple’s Game Controller Programming Guide.

I hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!

iOS 7 Game Controller Tutorial is a post from: Ray Wenderlich

The post iOS 7 Game Controller Tutorial appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4396

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>