What’s New with Game Center in iOS 6
Note from Ray: This is the seventh iOS 6 tutorial in the iOS 6 Feast! This tutorial is an abbreviated version of one of the chapters from our new book iOS 6 By Tutorials. Ali Hafizji wrote this chapter – the same guy who’s written several Android tutorials for this site in the past. Enjoy! […] By Ali Hafizji.
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
Submitting scores to Game Center
To send a score to Game Center, use the GKScore class. This class holds information about the player’s score and the category to which it belongs.
The category refers to the leaderboard ID. For example, if you wish to submit a score to the High Scores leaderboard, then the category of the GKScore object would be the leaderboard ID that you set in iTunes Connect, which in your case is HighScores.
Open GameKitHelper.h and add the following method declaration:
// Scores
-(void) submitScore:(int64_t)score
category:(NSString*)category;
Next, add the following method declaration to GameKitHelperProtocol:
-(void) onScoresSubmitted:(bool)success;
Now open GameKitHelper.m and add the following code:
-(void) submitScore:(int64_t)score
category:(NSString*)category {
//1: Check if Game Center
// features are enabled
if (!_gameCenterFeaturesEnabled) {
CCLOG(@"Player not authenticated");
return;
}
//2: Create a GKScore object
GKScore* gkScore =
[[GKScore alloc]
initWithCategory:category];
//3: Set the score value
gkScore.value = score;
//4: Send the score to Game Center
[gkScore reportScoreWithCompletionHandler:
^(NSError* error) {
[self setLastError:error];
BOOL success = (error == nil);
if ([_delegate
respondsToSelector:
@selector(onScoresSubmitted:)]) {
[_delegate onScoresSubmitted:success];
}
}];
}
Here’s a step-by-step explanation of the above method:
- Check to see if Game Center features are enabled, and execution proceeds only if they are.
- Create an instance of GKScore. The category used to create an object of GKScore is passed as an argument to the method.
- Set the value of the GKScore object.
- Send the GKScore object to Game Center using the reportScoreWithCompletionHandler: method. Once the score is sent, the platform calls the completion handler. The completion handler is a block that has one argument, in this case an NSError object that you can use to find out if the score was submitted successfully.
Now that you have defined a method to submit a score to Game Center, its time to use it! But before you do that, open GameConstants.h and add the following define statement at the end (before the last #endif):
#define kHighScoreLeaderboardCategory @"HighScores"
Next, open GameLayer.m and find the method named monkeyDead. As the name implies, this method is called when the monkey dies. In other words, this is where the game would end.
Add the following code as the first line in the method:
[[GameKitHelper sharedGameKitHelper]
submitScore:(int64_t)_distance
category:kHighScoreLeaderboardCategory];
Now build and run the app. Play the game until the monkey dies. Poor little dude!
When you finish playing, your score will be sent to Game Center. To verify that everything is working, open the Game Center app, tap on the Games tab and select the MonkeyJump game. The leaderboard should show your score. Here is a screenshot of Game Center showing the HighScores leaderboard:
Did you beat my score? No hacking the source code, now! :]
Game Center challenges
Finally – the section you’ve been waiting for!
Game Center challenges is the biggest new Game Center feature introduced in iOS 6.0. Challenges can help make your game go viral, and they increase user retention tremendously.
The only problem is, integrating challenges is extremely difficult since the API is vast and complicated.
Just kidding! To integrate challenges into your game, all you have to do is… ABSOLUTELY NOTHING! ☺ If your game supports leaderboards or achievements, it will automatically support challenges without you having to do any extra work.
To test this, open the Game Center application (make sure you are in sandbox mode). Go to the Games tab and open the MonkeyJump game.
If you have played the game a few times select your high score from the leaderboard. You will see a Challenge Friends button in the detail. Tap on it, enter the names of friends you want to challenge and press Send. When the challenge is sent, your friends will receive a push notification.
Note: To test challenges, you will need two devices running iOS 6.0, each logged into Game Center with a different account, and the accounts need to have added each other as friends.
Note: To test challenges, you will need two devices running iOS 6.0, each logged into Game Center with a different account, and the accounts need to have added each other as friends.
Challenges are not mere push notifications. Let me briefly explain how challenges work with an example.
Suppose I send a score challenge to Ray of 500 meters. Ray will receive a push notification on his device informing him of the challenge. Let’s suppose he gets a score of 1000 meters when he plays the game. In other words, Ray wipes the floor with the challenge. And he definitely wants me to know about that.
Since the game reports all scores to Game Center, it knows automatically that Ray killed the challenge, so it will send a challenge completion push notification to both the devices. Ray can then challenge me with his 1000 meter score. Little does he know that I can do 1000 meters in my sleep.
This process can go on indefinitely, with each party repeatedly topping the other’s score. It is because of this addictive, self-perpetuating use pattern that every game developer should integrate challenges into his/her games!
Up until now, you’ve tested challenges using the Game Center application, but what if you want to allow the user to challenge his/her friends from within your game?
That’s exactly what you’re going to do next. :] You will add this functionality to your game and allow the player to select which friends s/he wants to challenge using a friend picker.
Open GameKitHelper.h and add a new property to it.
@property (nonatomic, readwrite)
BOOL includeLocalPlayerScore;
Next add the following method declarations to GameKitHelperProtocol:
-(void) onScoresOfFriendsToChallengeListReceived:
(NSArray*) scores;
-(void) onPlayerInfoReceived:
(NSArray*)players;
Also add these method declarations to GameKitHelper:
-(void) findScoresOfFriendsToChallenge;
-(void) getPlayerInfo:(NSArray*)playerList;
-(void) sendScoreChallengeToPlayers:
(NSArray*)players
withScore:(int64_t)score
message:(NSString*)message;
Next you’re going to define each one of the above methods in GameKitHelper.m. Let’s start with findScoresOfFriendsToChallenge. Add the following lines of code:
-(void) findScoresOfFriendsToChallenge {
//1
GKLeaderboard *leaderboard =
[[GKLeaderboard alloc] init];
//2
leaderboard.category =
kHighScoreLeaderboardCategory;
//3
leaderboard.playerScope =
GKLeaderboardPlayerScopeFriendsOnly;
//4
leaderboard.range = NSMakeRange(1, 100);
//5
[leaderboard
loadScoresWithCompletionHandler:
^(NSArray *scores, NSError *error) {
[self setLastError:error];
BOOL success = (error == nil);
if (success) {
if (!_includeLocalPlayerScore) {
NSMutableArray *friendsScores =
[NSMutableArray array];
for (GKScore *score in scores) {
if (![score.playerID
isEqualToString:
[GKLocalPlayer localPlayer]
.playerID]) {
[friendsScores addObject:score];
}
}
scores = friendsScores;
}
if ([_delegate
respondsToSelector:
@selector
(onScoresOfFriendsToChallengeListReceived:)]) {
[_delegate
onScoresOfFriendsToChallengeListReceived:scores];
}
}
}];
}
This method is responsible for fetching the scores of all the player’s friends. To do this, the method queries the HighScores leaderboard for scores of the local player’s friends.
Every time you request scores, Game Center adds the score of the local player to the results by default. For example, in the above method when you request the scores of all the player’s friends, Game Center returns an array containing not only the scores of the player’s friends, but the player’s score as well. So, you use the includeLocalPlayerScore property to decide whether or not to remove the local player’s score from the scores array. By default, this is NO (don’t include the player’s score).
Now add the following method:
-(void) getPlayerInfo:(NSArray*)playerList {
//1
if (_gameCenterFeaturesEnabled == NO)
return;
//2
if ([playerList count] > 0) {
[GKPlayer
loadPlayersForIdentifiers:
playerList
withCompletionHandler:
^(NSArray* players, NSError* error) {
[self setLastError:error];
if ([_delegate
respondsToSelector:
@selector(onPlayerInfoReceived:)]) {
[_delegate onPlayerInfoReceived:players];
}
}];
}
}
This method gets player information for a list of players by passing in an array of player IDs.
One final method – add the following code:
-(void) sendScoreChallengeToPlayers:
(NSArray*)players
withScore:(int64_t)score
message:(NSString*)message {
//1
GKScore *gkScore =
[[GKScore alloc]
initWithCategory:
kHighScoreLeaderboardCategory];
gkScore.value = score;
//2
[gkScore issueChallengeToPlayers:
players message:message];
}
This method sends out a score challenge to a list of players, accompanied by a message from the player.
Great! Next, you need a friend picker. The friend picker will allow the player to enter a custom message and select the friends that s/he wants to challenge. By default, it will select those friends that have a score lower than the local player’s score, since these are the ones that the player should definitely challenge. After all, the player wants to win! ☺
Create a new group in Xcode and name it ViewControllers. Then create a new file in that group that extends UIViewController and name it FriendsPickerViewController. Make sure you check the “With XIB for user interface” checkbox, as shown below
Open the FriendsPickerViewController.xib file, set the view’s orientation to landscape, drag a UITableView, a UITextField and a UILabel onto the canvas, and set the text property of the label as “Challenge message”.
Also, to make sure that this view controller has the same look and feel as the rest of the game, add bg_menu.png as the background image. The final view controller should look like this:
Open FriendsPickerViewController.h and add the following statements above the @interface line:
typedef void
(^FriendsPickerCancelButtonPressed)();
typedef void
(^FriendsPickerChallengeButtonPressed)();
These two new data types, FriendsPickerCancelButtonPressed and FriendsPickerChallengeButtonPressed, describe the blocks you’ll be using. A block is like a C function; it has a return type (in this case void) and zero or more parameters. The typedef makes it a bit easier to refer to this block in the code.
Add the following properties to the @interface section:
//1
@property (nonatomic, copy)
FriendsPickerCancelButtonPressed
cancelButtonPressedBlock;
//2
@property (nonatomic, copy)
FriendsPickerChallengeButtonPressed
challengeButtonPressedBlock;
These properties represent blocks of code that will be executed when either the Cancel or the Challenge buttons are pressed.
Next add the Cancel and Challenge buttons to the view controller. Open FriendsPickerViewController.m and replace viewDidLoad with the following code:
- (void)viewDidLoad {
[super viewDidLoad];
UIBarButtonItem *cancelButton =
[[UIBarButtonItem alloc]
initWithTitle:@"Cancel"
style:UIBarButtonItemStylePlain
target:self
action:@selector(cancelButtonPressed:)];
UIBarButtonItem *challengeButton =
[[UIBarButtonItem alloc]
initWithTitle:@"Challenge"
style:UIBarButtonItemStylePlain
target:self
action:@selector(challengeButtonPressed:)];
self.navigationItem.leftBarButtonItem =
cancelButton;
self.navigationItem.rightBarButtonItem =
challengeButton;
}
The method adds two UIBarButtonItems to the view controller, representing the Cancel and Challenge buttons. Now add the methods that will be called when these buttons are tapped.
- (void)cancelButtonPressed:(id) sender {
if (self.cancelButtonPressedBlock != nil) {
self.cancelButtonPressedBlock();
}
}
- (void)challengeButtonPressed:(id) sender {
if (self.challengeButtonPressedBlock) {
self.challengeButtonPressedBlock();
}
}
The above methods are easy to understand – all you do is execute the code in the challenge and cancel blocks.
Before you can integrate this view controller into the game and test to see if everything works, you first need to write an initialization method that takes the score of the local player. But before you do this, you must define a variable to hold the score.
Add the following variable to the class extension at the top of FriendsPickerViewController.m – and remember to enclose the variable in curly brackets so that the final class extension looks like this:
@interface FriendsPickerViewController () {
int64_t _score;
}
@end
Now add the following initialization method:
- (id)initWithScore:(int64_t) score {
self = [super
initWithNibName:
@"FriendsPickerViewController"
bundle:nil];
if (self) {
_score = score;
}
return self;
}
Add the method declaration for the above to FriendsPickerViewController.h, as shown below:
-(id)initWithScore:(int64_t) score;
Now you are ready to test this view controller to see if everything works as expected. Open GameKitHelper.h and define a method as follows:
-(void)
showFriendsPickerViewControllerForScore:
(int64_t)score;
Then open GameKitHelper.m and add the following import statement:
#import "FriendsPickerViewController.h"
Next, add the method as follows:
-(void)
showFriendsPickerViewControllerForScore:
(int64_t)score {
FriendsPickerViewController
*friendsPickerViewController =
[[FriendsPickerViewController alloc]
initWithScore:score];
friendsPickerViewController.
cancelButtonPressedBlock = ^() {
[self dismissModalViewController];
};
friendsPickerViewController.
challengeButtonPressedBlock = ^() {
[self dismissModalViewController];
};
UINavigationController *navigationController =
[[UINavigationController alloc]
initWithRootViewController:
friendsPickerViewController];
[self presentViewController:navigationController];
}
This method presents the FriendPickerViewController modally. It also defines the blocks that will be executed when the Challenge and Cancel buttons are pressed. In this case, all that happens is that the view controller is dismissed.
Now open GameOverLayer.m and replace the CCLOG(@”Challenge button pressed”); line in menuButtonPressed: with the following:
[[GameKitHelper sharedGameKitHelper]
showFriendsPickerViewControllerForScore:_score];
Here is the moment of truth! Build and run the game, play a round of MonkeyJump, and when you press the Challenge Friends button on the game over screen, you will be presented with the FriendsPickerViewController. If you tap on either the Challenge or the Cancel button the view controller will be dismissed.
Great! Your game now has the ability to show the friends picker view controller. But the view controller does not show any friends, which kind of defeats the purpose.
No need to feel lonely – let’s add this functionality!
Open FriendsPickerViewController.m and replace the class extension with the following:
@interface FriendsPickerViewController ()
<UITableViewDataSource,
UITableViewDelegate,
UITextFieldDelegate,
GameKitHelperProtocol> {
NSMutableDictionary *_dataSource;
int64_t _score;
}
@property (nonatomic, weak)
IBOutlet UITableView *tableView;
@property (nonatomic, weak)
IBOutlet UITextField *challengeTextField;
@end
Notice that the interface now implements a bunch of protocols. Along with that, it also has two IBOutlets, one for the UITableView and the other for the UITextfield. Connect these properties to their respective views using Interface Builder, as shown below:
Next set the delegate and the data source of the UITableView, and the delegate of the UITextField, as the File’s Owner in Interface Builder.
To do this, select the UITableView and in the Connections inspector, drag the data source and delegate outlets to the File’s Owner on the left, as shown in the image below:
Repeat the process for the UITextField.
Switch to FriendsPickerViewController.m and add the following code within the if condition in initWithScore:, below the _score = score; line:
dataSource = [NSMutableDictionary dictionary];
GameKitHelper *gameKitHelper = [GameKitHelper sharedGameKitHelper];
gameKitHelper.delegate = self;
[gameKitHelper findScoresOfFriendsToChallenge];
This method initializes the data source, sets itself as the delegate for GameKitHelper, and calls findScoresOfFriendsToChallenge. If you recall, this method is used to find the scores of all the friends of the local player. Implement the onScoresOfFriendsToChallengeListReceived: delegate method to handle what happens after the player’s friends’ scores are fetched:
-(void)
onScoresOfFriendsToChallengeListReceived:
(NSArray*) scores {
//1
NSMutableArray *playerIds =
[NSMutableArray array];
//2
[scores enumerateObjectsUsingBlock:
^(id obj, NSUInteger idx, BOOL *stop){
GKScore *score = (GKScore*) obj;
//3
if(_dataSource[score.playerID]
== nil) {
_dataSource[score.playerID] =
[NSMutableDictionary dictionary];
[playerIds addObject:score.playerID];
}
//4
if (score.value < _score) {
[_dataSource[score.playerID]
setObject:[NSNumber numberWithBool:YES]
forKey:kIsChallengedKey];
}
//5
[_dataSource[score.playerID]
setObject:score forKey:kScoreKey];
}];
//6
[[GameKitHelper sharedGameKitHelper]
getPlayerInfo:playerIds];
[self.tableView reloadData];
}
The code is quite self-explanatory, but here’s an explanation anyway:
- An array named playerIds is created to hold the IDs of the local player’s friends.
- Then the method starts iterating over the returned scores.
- For each score, an entry in the data source is created and the player ID is stored in the playerIds array.
- If the score is less than the local player’s score, the entry is marked in the data source.
- The score is stored in the data source dictionary.
- The GameKitHelper’s getPlayerInfo: method is invoked with the playerIds array. This will return the details of each friend, such as the player’s name and profile picture.
The above method requires a few #defines in order to work. Add those (and a few others you’ll need later on in the code) to the top of the file below the #import line:
#define kPlayerKey @"player"
#define kScoreKey @"score"
#define kIsChallengedKey @"isChallenged"
#define kCheckMarkTag 4
Next you need to implement the onPlayerInfoReceived: delegate method. This method is called when information for all the local player’s friends has been received.
-(void) onPlayerInfoReceived:(NSArray*)players {
//1
[players
enumerateObjectsUsingBlock:
^(id obj, NSUInteger idx, BOOL *stop) {
GKPlayer *player = (GKPlayer*)obj;
//2
if (_dataSource[player.playerID]
== nil) {
_dataSource[player.playerID] =
[NSMutableDictionary dictionary];
}
[_dataSource[player.playerID]
setObject:player forKey:kPlayerKey];
//3
[self.tableView reloadData];
}];
}
This method is also quite straightforward; since you have the details of each player, you just update the _dataSource dictionary with each player’s information.
The _dataSource dictionary is used to populate the table view. Implement the table view’s data source methods as shown below:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return _dataSource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell identifier";
static int ScoreLabelTag = 1;
static int PlayerImageTag = 2;
static int PlayerNameTag = 3;
UITableViewCell *tableViewCell =
[tableView
dequeueReusableCellWithIdentifier:
CellIdentifier];
if (!tableViewCell) {
tableViewCell =
[[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
tableViewCell.selectionStyle =
UITableViewCellSelectionStyleGray;
tableViewCell.textLabel.textColor =
[UIColor whiteColor];
UILabel *playerName =
[[UILabel alloc] initWithFrame:
CGRectMake(50, 0, 150, 44)];
playerName.tag = PlayerNameTag;
playerName.font = [UIFont systemFontOfSize:18];
playerName.backgroundColor =
[UIColor clearColor];
playerName.textAlignment =
UIControlContentVerticalAlignmentCenter;
[tableViewCell addSubview:playerName];
UIImageView *playerImage =
[[UIImageView alloc]
initWithFrame:CGRectMake(0, 0, 44, 44)];
playerImage.tag = PlayerImageTag;
[tableViewCell addSubview:playerImage];
UILabel *scoreLabel =
[[UILabel alloc]
initWithFrame:
CGRectMake(395, 0, 30,
tableViewCell.frame.size.height)];
scoreLabel.tag = ScoreLabelTag;
scoreLabel.backgroundColor =
[UIColor clearColor];
scoreLabel.textColor =
[UIColor whiteColor];
[tableViewCell.contentView
addSubview:scoreLabel];
UIImageView *checkmark =
[[UIImageView alloc]
initWithImage:[UIImage
imageNamed:@"checkmark.png"]];
checkmark.tag = kCheckMarkTag;
checkmark.hidden = YES;
CGRect frame = checkmark.frame;
frame.origin =
CGPointMake(tableView.frame.size.width - 16, 13);
checkmark.frame = frame;
[tableViewCell.contentView
addSubview:checkmark];
}
NSDictionary *dict =
[_dataSource allValues][indexPath.row];
GKScore *score = dict[kScoreKey];
GKPlayer *player = dict[kPlayerKey];
NSNumber *number = dict[kIsChallengedKey];
UIImageView *checkmark =
(UIImageView*)[tableViewCell
viewWithTag:kCheckMarkTag];
if ([number boolValue] == YES) {
checkmark.hidden = NO;
} else {
checkmark.hidden = YES;
}
[player
loadPhotoForSize:GKPhotoSizeSmall
withCompletionHandler:
^(UIImage *photo, NSError *error) {
if (!error) {
UIImageView *playerImage =
(UIImageView*)[tableView
viewWithTag:PlayerImageTag];
playerImage.image = photo;
} else {
NSLog(@"Error loading image");
}
}];
UILabel *playerName =
(UILabel*)[tableViewCell
viewWithTag:PlayerNameTag];
playerName.text = player.displayName;
UILabel *scoreLabel =
(UILabel*)[tableViewCell
viewWithTag:ScoreLabelTag];
scoreLabel.text = score.formattedValue;
return tableViewCell;
}
That’s a lot of code. :] But if you have used a UITableView before, the code should not be new to you. The tableView:cellForRowAtIndex: method creates a new UITableViewCell. Each cell of the table view will contain a profile picture, the player’s name and the player’s score.
Now add tableView:didSelectRowAtIndex: to handle the user selecting a row in the table view:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:
(NSIndexPath *)indexPath {
BOOL isChallenged = NO;
//1
UITableViewCell *tableViewCell =
[tableView cellForRowAtIndexPath:
indexPath];
//2
UIImageView *checkmark =
(UIImageView*)[tableViewCell
viewWithTag:kCheckMarkTag];
//3
if (checkmark.isHidden == NO) {
checkmark.hidden = YES;
} else {
checkmark.hidden = NO;
isChallenged = YES;
}
NSArray *array =
[_dataSource allValues];
NSMutableDictionary *dict =
array[indexPath.row];
//4
[dict setObject:[NSNumber
numberWithBool:isChallenged]
forKey:kIsChallengedKey];
[tableView deselectRowAtIndexPath:indexPath
animated:YES];
}
All the method does is set an entry in the _dataSource to YES or NO.
Build and run the app. Now when the FriendsPickerViewController is launched the UITableView is be populated with the local player’s friends. The details for each friend, such as their name and profile picture, are also displayed in each cell. Here’s what it looks like:
The final thing left to do is to actually send the challenge. Replace challengeButtonPressed: in FriendsPickerViewController.m with the following:
- (void)challengeButtonPressed:
(id) sender {
//1
if(self.challengeTextField.text.
length > 0) {
//2
NSMutableArray *playerIds =
[NSMutableArray array];
NSArray *allValues =
[_dataSource allValues];
for (NSDictionary *dict in allValues) {
if ([dict[kIsChallengedKey]
boolValue] == YES) {
GKPlayer *player =
dict[kPlayerKey];
[playerIds addObject:
player.playerID];
}
}
if (playerIds.count > 0) {
//3
[[GameKitHelper sharedGameKitHelper]
sendScoreChallengeToPlayers:playerIds
withScore:_score message:
self.challengeTextField.text];
}
if (self.challengeButtonPressedBlock) {
self.challengeButtonPressedBlock();
}
} else {
self.challengeTextField.layer.
borderWidth = 2;
self.challengeTextField.layer.
borderColor =
[UIColor redColor].CGColor;
}
}
Here is a step-by-step explanation of the above method:
- The method first checks to see if the user has entered a message. If not, the border of the challengeTextField is turned red.
- If the user has entered text, the method finds the player IDs of all the selected players and stores them in an array called playerIds.
- If the user has selected players to challenge, then sendScoreChallengeToPlayers:withScore: is called from GameKitHelper with those player IDs. This will send the challenge to all the selected players.
Build and run the game. Now when you press the Challenge Friends button on the FriendsPickerViewController, it will send out a score challenge. If you have two devices you can easily test this to see if it works.
Challenges unlocked! w00t - you can now send challenges through code!