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 4 of 5 of this article. Click here to view the first page.

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;
}

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.

  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.
  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.

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.