What’s New with Game Center in iOS 6

Ali Hafizji

Learn about the new Challenges feature in GameKit 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!

This is a blog post by iOS Tutorial Team member Ali Hafizji, an iOS and Android developer working at Tavisca Solutions.

You’ve probably heard of Game Center, the online multiplayer social gaming network introduced in iOS 4.1. It allows users to invite friends to play a game, start a multiplayer gaming session, track game achievements, and a lot more.

In addition to making implementing these standard social features much easier for developers, it also helps with a fundamental problem: app discovery. With over 1 million apps in the App Store today, the odds of a single user discovering your app can be frustratingly low. Game Center helps to solve this problem by allowing the user to see what games his/her friends are playing, thereby increasing the visibility of your game.

iOS 6.0 introduces new APIs in Game Center that not only continue to help your game gain visibility, but also increase user engagement. One such feature is the ability to send game challenges to friends, even those who don’t have the game installed. For example, a user could get a high score in your game and send a challenge to their friend saying “Hah, beat this!”

When the friend receives the challenge, they will see the message along with a direct link to download your app. It’s not hard to imagine how this could increase user retention manyfold! When you consider the large number of players using Game Center, this alone is a very good reason to add challenges to your games.

Before you can use challenges you need to use Game Center, so first this tutorial will take you through setting up Game Center and setting up a simple leaderboard, highlighting all the iOS 6 updates along the way.

Note: This tutorial assumes that you have prior knowledge of Cocos2D and have built games using it. If you are new to Cocos2D, you can read some of the many Cocos2D tutorials available on this website.

The MonkeyJump game

First download the starter project for this tutorial – MonkeyJump!

MonkeyJump is a simple side scroller written using my favorite game engine, Cocos2D. It was originally based on a simple game called CatJump developed for a Learning Cocos2D workshop, but I added some new features to make it more fun and added some cute new art from Vicki Wenderlich.

The main character of the game is, obviously, a monkey :] The objective of the game is to make the monkey cover the maximum possible distance while avoiding as many enemies as he can.

MonkeyJump is easy to play – even your mom could play! The player simply has to tap the screen to make the monkey jump over enemies. The game keeps track of the distance the monkey runs and uses it to determine the player’s score.

Try it out for yourself! Unzip the starter project, open it in Xcode and build and run. Try to see how far you can get! :]

Also go through the source code to understand how the layers and scenes work together.

Setting up Game Center

As mentioned earlier, before you to do anything with Game Center Challenges, you first have to set up your app to use Game Center! And to do that, you need to do three things:

  1. Create and set an App ID.
  2. Register your app on iTunes Connect.
  3. Enable Game Center features such as leaderboards.

Let’s go through each of these steps in turn. This will be old hat for many readers, but I promise we’ll go through these necessary tasks quickly.

Creating and setting an App ID

The first thing you need to do is create an App ID. To do this, log on to the iOS Dev Center and from there, select the iOS Provisioning Portal.

From the Provisioning Portal, select App IDs and create a new App ID. Use monkeyjump as the name and enter a bundle identifier – usually it’s good to use reverse DNS notation for a domain you control like com.ali.MonkeyJump (you can use your name if you don’t have a domain name).

Once you are done, click the Submit button. Open the MonkeyJump Xcode project, select project root, then the MonkeyJump target (if it’s not selected), and in the Summary tab change the Bundle Identifier to the identifier you created in the iOS Provisioning Portal.

Give the app a quick compile and run it on a device. If everything is in place the game will start right up. If it doesn’t, clean and build the project and try again.

Registering your app in iTunes Connect

The next step is to create a new app on iTunes Connect. Log in to iTunes Connect, switch to the application management screen, and click the Add New App button in the top left corner. (If you have both Mac and iOS developer accounts, you might have to select the type of app – which is iOS, of course)

On the first screen, enter MonkeyJump as the game name, 400 as the SKU number (this can be any number/word, so if you want you can set it to something else) and select the Bundle Identifier you created in the previous step.

When you are done entering all the values, press Continue. Follow the prompts and enter all the required details. Since you just need to get through these steps for the purposes of this chapter, fill in only the necessary values and be as brief as you want to be. ☺

You will need to upload a large app icon and a screenshot. To make the process easier, I have uploaded an iTunes resources file for you. You can extract the ZIP file and upload these to quickly finish the registration process.

When you’re done, click on the Save button and if everything went fine, you will be presented with the following screen:

Hurray! You have registered your app with iTunes Connect and completed the most perfunctory business. Now there are just a few more steps to activate Game Center. Don’t worry, the tough part is over. ☺

Enabling Game Center features

Click the blue Manage Game Center button and then click the Enable for Single Game button. Awesome! You have just enabled Game Center for your game. Yep, it is as simple as clicking a button – but you still have a bunch of code to write. :]

You aren’t done with this section yet – you still need to add a leaderboard. You might wonder why you have to bother with leaderboards since this chapter is about challenges – don’t worry, you will see why later!

For now, take my word at it and add a leaderboard and some challenges, starting with a leaderboard. Click the Add Leaderboard button and select the Single Leaderboard type. You will be presented with a form, like so:

Enter the leaderboard reference name as High Scores and the leaderboard ID as HighScores.

Note: I generally recommend you keep the leaderboard/achievement ID as an extension of the package name. For example, in the above case for me it would be com.ali.MonkeyJump.HighScores (you would need to replace the com.ali part to match your own setup). But for the purposes of this tutorial, just name it plain HighScores (without the reverse domain name prefix) to keep things simple.

Set the Sort Order as High to Low and the Score Format Type as Integer. Finally, click on the Add Language button. Enter the language details as shown below:

Adding an image is not mandatory, but is always a good practice. The one used above is included in the iTunes resources (it’s named icon_leaderboard_512.png), and you can use it here for the high scores leaderboard. When you’re done, click Save.

Finally, click on the Done button. For now, one leaderboard is enough, but in the future if you want to add more, you now know the drill.

Authenticating the local player

Before you start writing code, you need to import the GameKit framework. Open the MonkeyJump project in Xcode 4.5 and navigate to the target settings. Open the Build Phases tab and expand the Link Binary With Libraries section. Click the “+” button and add the GameKit framework to the project.

Next you will write code to authenticate the player. Without authenticating the player, you cannot use any of the awesome features that Game Center provides.

Player here means the user who is playing your game. In Game Center terms, this is the GKLocalPlayer.

Player authentication is a simple two-step process:

  1. First you make an authenticate call to the Game Center platform.
  2. The platform then asynchronously calls you back when authentication is complete. If the player was already logged in (95% of the time), a welcome banner is presented. If not, then a login screen is presented which also allows the player to register.

Let’s write some code. You are going to use a singleton pattern, so that all the Game Center code is in one class.

Right-click on the MonkeyJump group in Xcode and select New Group. Name the group GameKitFiles. Next, right-click on the newly-created group and select New File…, then select the Objective-C Class template. Name the class GameKitHelper and make sure it extends NSObject.

Replace the contents of GameKitHelper.h with the following:

//   Include the GameKit framework
#import <GameKit/GameKit.h>
 
//   Protocol to notify external
//   objects when Game Center events occur or
//   when Game Center async tasks are completed
@protocol GameKitHelperProtocol<NSObject>
@end
 
 
@interface GameKitHelper : NSObject
 
@property (nonatomic, assign)
    id<GameKitHelperProtocol> delegate;
 
// This property holds the last known error
// that occured while using the Game Center API's
@property (nonatomic, readonly) NSError* lastError;
 
+ (id) sharedGameKitHelper;
 
// Player authentication, info
-(void) authenticateLocalPlayer;
@end

The code is self-explanatory and is heavily commented. All you are doing here is declaring two methods and two properties – one is the delegate, and the other will hold the last error that occurred while using the GameKit framework.

Switch to GameKitHelper.m and replace its contents with the following:

#import "GameKitHelper.h"
#import "GameConstants.h"
 
@interface GameKitHelper ()
        <GKGameCenterControllerDelegate> {
    BOOL _gameCenterFeaturesEnabled;
}
@end
 
@implementation GameKitHelper
 
#pragma mark Singleton stuff
 
+(id) sharedGameKitHelper {
    static GameKitHelper *sharedGameKitHelper;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedGameKitHelper =
                [[GameKitHelper alloc] init];
    });
    return sharedGameKitHelper;
}
 
#pragma mark Player Authentication
 
-(void) authenticateLocalPlayer {
 
  GKLocalPlayer* localPlayer =
    [GKLocalPlayer localPlayer];
 
    localPlayer.authenticateHandler =
    ^(UIViewController *viewController,
      NSError *error) {
 
        [self setLastError:error];
 
        if ([CCDirector sharedDirector].isPaused)
            [[CCDirector sharedDirector] resume];
 
        if (localPlayer.authenticated) {
            _gameCenterFeaturesEnabled = YES;
        } else if(viewController) {
            [[CCDirector sharedDirector] pause];
            [self presentViewController:viewController];
        } else {
            _gameCenterFeaturesEnabled = NO;
        }
    };
}
@end

You have declared a variable called _gameCenterFeaturesEnabled. This BOOL variable will be true if authentication was successful, and in any other case will be false.

The way to authenticate a player has changed in iOS 6.0. All you need to do now is set the authenticationHandler of the GKLocalPlayer object, as seen in the -authenticateLocalPlayer method. The authenticationHandler is a block that takes two parameters and is called by the Game Center platform.
This block is called by the system in a number of scenarios:

  • When you set the authenticationHandler and request for the player to be authenticated.
  • When the app moves to the foreground.
  • On sign-in, i.e., if the player has not signed in before a sign-in screen is presented, any interaction on that screen leads to the authenticationHandler being called.

The authenticationHandler has two arguments:

  • A UIViewController representing the login view controller if you need to present it; and
  • An NSError object in case authentication happens to fail.

Notice that in the block, you first check if the local player is authenticated. If the player has already been authenticated, then all you need to do is set the _gameCenterEnabled flag as true and get on with your game.

If the login view controller (i.e., the first parameter of the authenticationHandler) is not nil, it means that the player has not logged into Game Center. If so, you first pause the game and then present the login view controller to the player. If the player logs in or selects the Cancel button on the login view controller, this handler is called again.

In earlier versions of Game Center, the developer did not have the power to decide when to present the login view controller. This new method gives developers more control, which is always a good thing. ☺

Finally, if authentication fails you need to gracefully fall back and disable all Game Center features. This is achieved in this app by setting the _gameCenterFeaturesEnabled flag to false.

In order for the code in authenticateLocalPlayer to work, you need a few more bits of code. Add the following to GameKitHelper.m:

#pragma mark Property setters
 
-(void) setLastError:(NSError*)error {
    _lastError = [error copy];
    if (_lastError) {
        NSLog(@"GameKitHelper ERROR: %@", [[_lastError userInfo] 
          description]);
    }
}
 
#pragma mark UIViewController stuff
 
-(UIViewController*) getRootViewController {
    return [UIApplication 
      sharedApplication].keyWindow.rootViewController;
}
 
-(void)presentViewController:(UIViewController*)vc {
    UIViewController* rootVC = [self getRootViewController];
    [rootVC presentViewController:vc animated:YES 
      completion:nil];
}

The above three methods set up a few things needed by authenticateLocalPlayer:

  1. The lastError property is declared as a readonly property. Hence, you cannot assign to it directly. So you need a setter method that will take care of setting the lastError property. That’s what setLastError: is.
  2. The Game Center login controller needs to be displayed to the user so that s/he can actually login. The presentViewController: and getRootViewController methods handle discovering the root view controller for the application and then displaying the login view via the root view controller.

Awesome! Now it’s time to put GameKitHelper to the test. Open Prefix.pch and add the necessary import:

#import "GameKitHelper.h"

Next, open MenuLayer.m and add the following code to onEnter (right below the initial [super onEnter]). This will authenticate the player every time the menu screen is presented

[[GameKitHelper sharedGameKitHelper]
                authenticateLocalPlayer];

Build and run the application, and now when the menu view controller appears you will see one of the following screens:

The image on the left shows the login view controller, presented in case the user was not logged in with Game Center. The image on the right shows the welcome banner, displayed every time an authentication call is successful.

Note: To test the authentication, first logout of Game Center and then login through the MonkeyJump app. This will run Game Center in sandbox mode. Additionally, this probably will not work on the Simulator (at least, it didn’t work at the time of writing). You would need to test this on an actual device.

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:

  1. Check to see if Game Center features are enabled, and execution proceeds only if they are.
  2. Create an instance of GKScore. The category used to create an object of GKScore is passed as an argument to the method.
  3. Set the value of the GKScore object.
  4. 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.

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:

  1. An array named playerIds is created to hold the IDs of the local player’s friends.
  2. Then the method starts iterating over the returned scores.
  3. For each score, an entry in the data source is created and the player ID is stored in the playerIds array.
  4. If the score is less than the local player’s score, the entry is marked in the data source.
  5. The score is stored in the data source dictionary.
  6. 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:

  1. The method first checks to see if the user has entered a message. If not, the border of the challengeTextField is turned red.
  2. 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.
  3. 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!

Where To Go From Here?

Here is the final project.

If you want to learn more about GameKit and all the awesome new things introduced be sure to check out our new book titled iOS 6 by Tutorials!

The books has 2 full length chapters on GameKit and covers the following topics:

  1. Details on new classes introduced such as GKGameCenterViewController.
  2. Setting up achievements for the monkey jump game and unlocking them through GameKit.
  3. Reporting achievement using GameKit.
  4. Sending achievement challenges to friends.
  5. Sharing score and achievements on social channels such as facebook, twitter etc.
  6. Adding the ability to record every move the player makes and sending that data along with a challenge.
  7. Adding a ghost mode that show every move the challenger made while you are trying to beat a challenge.

So definitely check out iOS 6 by Tutorials as there a ton you will get to learn! In the meantime, if you have any questions or comments on this tutorial, please join the forum discussion below!

This is a blog post by iOS Tutorial Team member Ali Hafizji, an iOS and Android developer working at Tavisca Solutions.

Ali Hafizji

Ali is an independent iOS and Android developer currently focussing on building immersive experiences on mobile devices. He is an avid programmer and loves learning better and faster ways of solving problems. You can follow him on Twitter or github.

User Comments

23 Comments

[ 1 , 2 ]
  • Why do you use code:
    Code: Select all
    [_monkey runAction:[CCSpawn actions:jumpAction,sequenceAction, nil]];

    instead of
    Code: Select all
    [_monkey runAction:sequenceAction];


    I tried the later code and it works fine. I just wanna know why since I am not familiar with CCSpawn.
    gongpengjun
  • Im getting an error @:

    if ([CCDirector sharedDirector].isPaused)
    [[CCDirector sharedDirector] resume];

    if (localPlayer.authenticated) {
    _gameCenterFeaturesEnabled = YES;
    } else if(viewController) {
    [[CCDirector sharedDirector] pause];
    [self presentViewController:viewController];
    } else {
    _gameCenterFeaturesEnabled = NO;
    }

    at every call for CCDirector saying its an unknown receiver and asks me if I meant CIDetector.

    Whats that about? :( Sorry, Ive never read about CIDetector :)
    marciokoko
  • @marciokoko

    I was also getting that error, but I think you need to import cocos2d.h in your prefix.pch.

    Now, I'm getting an error in my GameKItHelper.m file:

    GameKitHelper.m:12: Expected identifier or '(' before '{' token

    It doesn't like this line, but this is exactly what he has:
    @interface GameKitHelper () {
    BOOL _gameCenterFeaturesEnabled;
    }
    @end
    elg
  • @elg

    Thanks, yeah I had to import it into my class file but I did so.

    Your line looks fine. Maybe it's an error to a related file, such as GameKitHelper.h. Did you check for any typos there?

    Wait I saw it! You wrote GameKItHelper instead of GameKitHelper.

    Capital I :)
    marciokoko
  • Hi,

    just a hint regarding a problem I faced on real devices: If using this on a real device make sure your Leaderboard ID constant holds the FULL qualified ID you set in iTunes Connect. I set it to "HighScores" as mentioned in the tutorial but the full name was "my.package.appname.HighScores". It worked in the emulator but NOT on a real device, neither in sandbox nor in live mode. High scores were submitted but not displayed.

    So use:

    Code: Select all
    #define kHighScoreLeaderboardCategory @"my.package.appname.HighScores"


    instead of:

    Code: Select all
    #define kHighScoreLeaderboardCategory @"HighScores"


    Otherwise a very good tutorial, thanks again!
    RichardR
  • Great tutorial! However, I keep getting the warning "Capturing 'localPlayer' strongly in this block will likely lead to a retain cycle". Does anyone know how to fix this? The problem is in the GameKitHelper.m
    evanlws12
  • I have been working through this tutorial in Xcode 6 and when I click on "Game Center" from the menu screen, it brings up a blank translucent screen. There are no buttons, and no scores. Also there is no way to dismiss it. I have to kill it in the app. I have my own certificate and have renamed the game and done everything in iTunes Connect. The scores are being reported, and everything else seems to work fine. I am trying this on the simulator. So for an experiment, I went back to the MonkeyJump Final and ran it unchanged and had the same result.

    I also went to the the Apple sample code and ran the GKAchievement sample and it worked fine (with lots of deprecated code warnings)

    Any suggestions as to how to get the GameCenter page to work?
    macinsmith3
  • Works fine on my iPhone 5s
    macinsmith3
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: How to Make a Simple 2D Game with Metal.

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

  • Ron Kliffer
  • Jake Gundersen

... 53 total!

Update Team

  • Zouhair Mahieddine

... 14 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

... 4 total!