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?
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;
}
- 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.
- If there isn’t a file at that location or if there’s an error parsing it,
arrayWithContentsOfURL
returnsnil
. You check for that next, or if there is a valid plist that’s initialized and it’s empty, then you returnNO
and you do not set the game into replay mode. - However, if there is a valid data, you proceed to set
shouldReplaySnapshot
toYES
. - 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;
}
- 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. - 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’.
- Next, you check the “type” entry in the dictionary to see if it’s “gamepad”, meaning that it’s a GCGamepadSnapshot type.
- 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. - 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;
}
}
- First, you check to see if
shouldReplaySnapshot
isYES
. If it isn’t then the rest of the existing code runs as though you hadn’t added this new block. - 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.
- 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. - Then, you inspect the class type of the
currentSnapshot
object to see if it’s a GCGamepadSnapshot class usingisKindOfClass
.isKindOfClass
is an NSObject method that you can use to determine which class type an object is at runtime. - If you have a
GCGamepadSnapshot
, you cast that variable to that type so you can access its properties without a compiler error. - Finally, you return the
xAxis
value from thedpad
control. - 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 thedpad
.
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.
- Check whether
shouldReplaySnapshot
isYES
. - If so, get the current snapshot from the array.
- 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
andvalueForKeyPath
. If you need to access a property of a property, like you do here, use thevalueForKeyPath
. 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 toYES
. 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.
- 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.
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.
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.