Chapter    14

Game Kit Recipes

Game Center is an iOS service that increases the “replay factor” of your games. It provides an easy way to implement features such as leaderboards, game achievements tracking and multiplayer gameplay, which can greatly enhance the social aspect of the games you make.

In this chapter, we’ll go through some of the basics of the Game Kit framework, which is what you use to make your games Game Center aware.

Recipe 14-1: Making Your App Game Center Aware

In this recipe you create a very simple game and connect it to Game Center. The game consists of four buttons. When the player taps a button, one of two things may happen: It’s either a “safe” button in which case the player’s score is increased by one and he or she is allowed to continue the game; or, it’s a “killer” button, which ends the game. Because the player has no way to know which button is which, there’s no skill involved and thus not much of a game. However, it’s easy to implement and therefore a good platform to show some of the basic features of Game Center.

Note  When creating a Game Center−aware game, it’s always a good idea to implement the game first and be sure it works properly before involving Game Center.

Implementing the Game

Start by creating a new single-view app project with the name “Lucky.”

The game has three difficulty levels: Easy game, where only one of the four buttons is a “killer” button; Normal game with two “killer” buttons; and Hard game, with three of the buttons being “killers.”

You set up the main view of the app to be a menu page with options to start a game with one of those levels. Open the ViewController.xib file and build a user interface that resembles the one in Figure 14-1.

9781430245995_Fig14-01.jpg

Figure 14-1.  The main menu view of the Lucky game

Create an outlet named welcomeLabel for the label, and actions named playEasyGame, playNormalGame, and playHardGame for when the user taps the respective button.

Now, before you move on to add and implement a view controller for the actual game, you’ll make this a navigation-based app with a Navigation bar. Start by adding the following property declaration to the AppDelegate.h file:

//
//  AppDelegate.h
//  Testing Game Center
//
 
#import <UIKit/UIKit.h>
 
@class ViewController;
 
@interface AppDelegate : UIResponder <UIApplicationDelegate>
 
@property (strong, nonatomic) UIWindow *window;
 
@property (strong, nonatomic) ViewController *viewController;
@property (strong, nonatomic) UINavigationController *navigationController;
 
@end

Then go to AppDelegate.m and make the following changes to the application:didFinishLaunchingWithOptions: method:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.viewController =
        [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
    self.navigationController =  
        [[UINavigationController alloc] initWithRootViewController:self.viewController];
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    return YES;
}

Finally, set the title of the main view controller to “Lucky,” and change the text of the Welcome label. Go to ViewController.m and add the following code to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    self.title = @"Lucky";
    self.welcomeLabel.text = @"Welcome Anonymous Player";
}

Build and run the app to be sure everything is okay so far; you should see a screen resembling the one in Figure 14-2.

9781430245995_Fig14-02.jpg

Figure 14-2.  The Lucky game has three levels

Now you’re going to add the view controller in which the actual gameplay will take place. Create a new UIViewController subclass with the name GameViewController and make sure the “With XIB for user interface” option is checked.

Go ahead and create a user interface that resembles Figure 14-3 for the new view controller.

9781430245995_Fig14-03.jpg

Figure 14-3.  The user interface of the Lucky game

Create the following outlets for the added user interface elements:

  • scoreLabel
  • button1
  • button2
  • button3
  • button4

For the buttons, also create an action that is shared by all the buttons and invoked when the user taps any one of them. To do that, start by creating an action named gameButtonSelected for Button 1. Be sure you use the specific type of UIButton for the method argument, as shown in Figure 14-4.

9781430245995_Fig14-04.jpg

Figure 14-4.  Creating an action with a specific type parameter

Now, connect the remaining buttons to the same action that you created for Button 1. You do this by Ctrl-clicking the button to be connected, and dragging the blue line onto the gameButtonSelected action declaration. When a Connect Action sign appears, as in Figure 14-5, you can release the mouse button and the action will become connected to the button.

9781430245995_Fig14-05.jpg

Figure 14-5.  Connecting a button to an existing IBAction

Be sure to connect all the remaining buttons to the gameButtonSelected action.

With the user interface elements properly connected to the code, you can move on to add some necessary declarations to the game view controller’s header file. You need an initializer method, a couple of private instance variables, and you need the view controller to conform to the UIAlertViewDelegate protocol. So go to GameViewController.h and add the following code:

//
//  GameViewController.h
//  Lucky
//
 
#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
 
@interface GameViewController : UIViewController <UIAlertViewDelegate>
{
    @private
    int _score;
    int _level;
}
 
@property (weak, nonatomic) IBOutlet UILabel *scoreLabel;
@property (weak, nonatomic) IBOutlet UIButton *button1;
@property (weak, nonatomic) IBOutlet UIButton *button2;
@property (weak, nonatomic) IBOutlet UIButton *button3;
@property (weak, nonatomic) IBOutlet UIButton *button4;
 
- (IBAction)gameButtonSelected:(UIButton *)sender;
 
- (id)initWithLevel:(int)level;
 
@end
 

Now, go to GameViewController.m to start implementing the initializing code. First, add the following implementation of the initializer method:

- (id)initWithLevel:(int)level
{
    self = [super initWithNibName:nil bundle:nil];
    if (self)
    {
        _level = level;
        _score = 0;
    }
    return self;
}

Next, add the following helper method for updating the score label:

- (void)updateScoreLabel
{
    self.scoreLabel.text = [NSString stringWithFormat:@"Score: %i", _score];
}

Finally, add the following code to the viewDidLoad method to set up the title and update the score label on game launch:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    switch (_level)
    {
        case 0:
            self.title = @"Easy Game";
            break;
        case 1:
            self.title = @"Normal Game";
            break;
        case 2:
            self.title = @"Hard Game";
            break;
            
        default:
            break;
    }
     
    [self updateScoreLabel];
}

Now pause the implementation of the game controller and go back to the main menu controller to connect the two view controllers. Start by adding the following import statement to the ViewController.h file:

//
//  ViewController.h
//  Lucky
//
 
#import <UIKit/UIKit.h>
#import "GameViewController.h"
 
@interface ViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UILabel *welcomeLabel;
 
- (IBAction)playEasyGame:(id)sender;
- (IBAction)playNormalGame:(id)sender;
- (IBAction)playHardGame:(id)sender;
 
@end

Now switch to ViewController.m. To avoid some code duplication, add the following helper method that will take a level argument and instantiate and display a game view controller:

- (void)playGameWithLevel:(int)level
{
    GameViewController *gameViewController =
        [[GameViewController alloc] initWithLevel:level];
    [self.navigationController pushViewController:gameViewController animated:YES];
}

You can now use this helper method to invoke a game of the respective level from the three action methods, like so:

- (IBAction)playEasyGame:(id)sender
{
    [self playGameWithLevel:0];
}
 
- (IBAction)playNormalGame:(id)sender
{
    [self playGameWithLevel:1];
}
 
- (IBAction)playHardGame:(id)sender
{
    [self playGameWithLevel:2];
}

Now is a good time to again build and run your app. If you’ve followed the steps correctly, you should be able to tap any of the three buttons in the menu view and have the game view presented, as in Figure 14-6

9781430245995_Fig14-06.jpg

Figure 14-6.  The screen of an easy level of the Lucky game

.

With the main architecture of the app all set up, you can move on to implement the gameplay functionality. Start by initializing the buttons to be either “killer” or “safe” buttons. Go back to GameViewController.m and add the following line to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // . . .
     
    [self updateScoreLabel];
    [self setupButtons];
}

Note that you implement this piece of code top-down meaning you add code that invokes helper methods that do not yet exist. Don’t worry about the compiler errors, as they clear out as you add the missing methods. Now, implement the setupButtons helper method. It simply delegates the job to three specific methods, one for each level, like so:

- (void)setupButtons
{
    switch (_level) {
        case 0:
            [self setupButtonsForEasyGame];
            break;
        case 1:
            [self setupButtonsForNormalGame];
            break;
        case 2:
            [self setupButtonsForHardGame];
            break;
            
        default:
            break;
    }
}

Next, implement the setup method for an Easy level game. An easy game sets up only one of the four buttons to be a “killer.” To indicate a “killer” button, use the tag property; a zero (0) means “safe” while a one (1) indicates “killer.” Here is the implementation:

- (void)setupButtonsForEasyGame
{
    [self resetButtonTags];
    int killerButtonIndex = rand() % 4;
    [self buttonForIndex:killerButtonIndex].tag = 1;
}

As you can see, this method resets the button’s tag property and picks a random button to make a “killer.” It makes use of two helper methods that have not yet been created, resetButtonTags and buttonForIndex:. Let’s start with resetButtonTags. It iterates over the four buttons and sets their tag property to 0:

- (void)resetButtonTags
{
    for (int i = 0; i < 4; i++)
    {
        UIButton *button = [self buttonForIndex:i];
        button.tag = 0;
    }
}

This method also makes use of the buttonForIndex: helper method. So go ahead and add it with the following implementation:

- (UIButton *)buttonForIndex:(int)index
{
    switch (index)
    {
        case 0:
            return self.button1;
        case 1:
            return self.button2;
        case 2:
            return self.button3;
        case 3:
            return self.button4;
        default:
            return nil;
    }
}

Now, let’s turn to setting up a Normal game. It’s like an Easy game except that it selects two “killer” buttons; implementation as follows:

- (void)setupButtonsForNormalGame
{
    [self resetButtonTags];
    int killerButtonIndex1 = rand() % 4;
    int killerButtonIndex2;
    do {
        killerButtonIndex2 = rand() % 4;
    } while (killerButtonIndex1 == killerButtonIndex2);
     
    [self buttonForIndex:killerButtonIndex1].tag = 1;
    [self buttonForIndex:killerButtonIndex2].tag = 1;
}

Finally, the set up method for a Hard game, where all but one button are “killers.”

- (void)setupButtonsForHardGame
{
    int safeButtonIndex = rand() % 4;
    for (int i=0; i < 4; i++) {
        if (i == safeButtonIndex) {
            [self buttonForIndex:i].tag = 0;
        }
        else
        {
            [self buttonForIndex:i].tag = 1;
        }
    }
}

What’s left now is to implement the gameButtonSelected: action method. It checks the tag property of the sending button to see whether it is a “killer” or a “safe” button. If it’s “safe”, the score will be increased and new “killers” will be picked. On the other hand, if it’s a “killer” the game is finished and an alert will be displayed showing the final score. Here is the complete implementation:

- (IBAction)gameButtonSelected:(UIButton *)sender
{
    if (sender.tag == 0)
    {
        // Safe, continue game
        _score += 1;
        [self updatfeScoreLabel];
        [self setupButtons];
    }
    else
    {
        // Game Over
        NSString *message = [NSString stringWithFormat:@"Your score was %i.", _score];
        UIAlertView *gameOverAlert = [[UIAlertView alloc] initWithTitle:@"Game Over"  
            message:message delegate:self cancelButtonTitle:@"OK"  
            otherButtonTitles:nil];
        [gameOverAlert show];
    }
}

The only thing remaining now until the basics of the game are complete, is to take the user back to the menu screen when the game is over and the user has dismissed the alert view. Do this by adding the following delegate method:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    [self.navigationController popViewControllerAnimated:YES];
}

The game is now finished, so go ahead and test it. You should be able to choose between an Easy, Normal, and Hard game and play until you hit a “killer” button. Figure 14-7 shows an Easy level finished game.

9781430245995_Fig14-07.jpg

Figure 14-7.  A player has hit a “killer” button in a game of Lucky

With the game in place and working, you can turn your focus to making it Game Center aware.

Registering with iTunes Connect

Normally when you develop an iOS app, registering with iTunes Connect is the last step before publishing to the App Store. With Game Center aware apps, this is a bit different. With a Game Center aware app, you must register the app as soon as you are ready to start developing the Game Center−specific functions.

Once you’ve registered, and marked the app as Game Center aware, iTunes Connect sets up a Game Center sandbox for your app. The Game Center sandbox is a development area where you can test the Game Center integration without impacting production scores or achievements.

The first step to enabling Game Center integration is to create a bundle identifier in the iOS Provisioning Portal. The bundle identifier is used to identify all the related data for an app. For instance, if you have a free and a paid version of your game and you want them to use the same high score tables, you would give them the same bundle identifier.

Start by logging in to the iOS Dev Center portal (developer.apple.com/devcenter/ios) and click on the iOS Provisioning Portal link. In there, go to the App IDs page and click the New App ID button. For the description, enter Lucky and as Bundle Identifier, use your project’s bundle identifier, which can be found on the Summary page in the Target settings editor. Figure 14-8 shows the configuration. (Your bundle identifier will of course be different.)

9781430245995_Fig14-08.jpg

Figure 14-8.  Configuring an App ID in the iOS Provisioning Portal

Now that you have registered the app with the iOS Provisioning Portal, you can move on to make a corresponding registration with iTunes Connect. Go to itunesconnect.apple.com and log in using your Developer ID. Now, click the Manage Your Applications link and then the Add New App button. Follow the instructions, and when you reach the page titled App Information, enter the information in Figure 14-9, except for the app name, which needs to be unique and not used by any other developer. You could try using your initials as a prefix, as we did in the figure.

9781430245995_Fig14-09.jpg

Figure 14-9.  Entering application information in iTunes Connect

Continue to fill out the required metadata information about your app. You also need to upload a screenshot and a large app icon. To save time, you can download the images from Source Code/Downloads on this book’s page at www.apress.com. The required files are

  • Lucky Large App Icon.jpg
  • Lucky Screenshot.png
  • Lucky Screenshot (iPhone 5).png

When you’re done filling out the required information, click Save. At this point you’ll see a page resembling the one in Figure 14-10.

9781430245995_Fig14-10.jpg

Figure 14-10.  An app registered with iTunes Connect

Now, click the Manage Game Center button on the right side of the page. In the Enable Game Center page, click the Enable for Single Game button, as shown in Figure 14-11.

9781430245995_Fig14-11.jpg

Figure 14-11.  Enabling Game Center for an app in iTunes Connect

You’re now done with the registration. Later you get back to iTunes Connect to configure leaderboards and achievements, but for now return to your Xcode project to set up the basic Game Center support.

Authenticating Local Player

With the Game Center sandbox enabled, you can go back to Xcode to implement Game Center support in the app. Your app checks whether Game Center is available and allows the user to sign in if she hasn’t already done so.

Start by adding the Game Kit framework to your project. Because you want the game to work without Game Center support, make this link optional, as shown in Figure 14-12.

9781430245995_Fig14-12.jpg

Figure 14-12.  Configuring the Game Kit framework to be optional

Note  If you develop a game that requires Game Center, you need to mark the GameKit.framework link as “Required,” but also add “gamekit” to the Required device capabilities array in the project’s .plist file, as shown in Figure 14-13.

9781430245995_Fig14-13.jpg

Figure 14-13.  Making Game Kit a required device capability

Your users need to be logged in to Game Center to take advantage of its features. It’s usually a good idea to authenticate the player at application launch so that he or she can start receiving challenges and other Game Center notifications right away. Go to ViewController.h, import the Game Kit API, and add the following property to keep track of the logged in player:

//
//  ViewController.h
//  Lucky
//
 
#import <UIKit/UIKit.h>
#import "GameViewController.h"
#import <GameKit/GameKit.h>
 
@interface ViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UILabel *welcomeLabel;
@property (strong, nonatomic) GKLocalPlayer *player;
 
- (IBAction)playEasyGame:(id)sender;
- (IBAction)playNormalGame:(id)sender;
- (IBAction)playHardGame:(id)sender;
 
@end

Add a custom setter for the property to update the Welcome label when the authenticated player changes. Go to ViewController.m and add the following method:

- (void)setPlayer:(GKLocalPlayer *)player
{
    _player = player;
    NSString *playerName;
    if (_player)
    {
        playerName = _player.alias;
    }
    else
    {
        playerName = @"Anonymous Player";
    }
    self.welcomeLabel.text = [NSString stringWithFormat:@"Welcome %@", playerName];
}

The Game Kit framework handles the authentication process for you. All you have to do is provide an authentication handler block to the localPlayer shared instance. Add the following helper method, which assigns such a block:

- (void)authenticatePlayer
{
    GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
    localPlayer.authenticateHandler =
    ^(UIViewController *authenticateViewController, NSError *error)
    {
        if (authenticateViewController != nil)
        {
            [self presentViewController:authenticateViewController animated:YES
                completion:nil];
        }
        else if (localPlayer.isAuthenticated)
        {
            self.player = localPlayer;
        }
        else
        {
            // Disable Game Center
            self.player = nil;
        }
    };
}

After an authentication handler is assigned, Game Kit tries to authenticate the local player. The outcome can be one of three possible scenarios:

  1. If the user is not signed into Game Center, the authentication handler will be invoked with a login view controller provided by Game Kit. All your app needs to do then is to present the view controller at an appropriate time. Whether the user signs in, or cancels the view controller, your authentication handler will be invoked again with the new state.
  2. If the user is currently signed in, no view controller is provided, and the isAuthenticated property of localPlayer returns YES. Your app can then enable its Game Center features. In this case, that means assigning the player property.
  3. If the user is not signed in, or if Game Center is unavailable for some reason, no view controller is provided and the isAuthenticated property returns NO. Your app should then disable its Game Center features or stop the execution, whichever makes the most sense. In this case, because you allow anonymous playing, you simply set the player property to nil.

Finally, to start the authentication process after the main view has finished loading, make the following changes to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
     
    self.title = @"Lucky";
    self.player = nil;
    [self authenticatePlayer];
}

That’s pretty much all there is to authenticating a user. When you run your app, it will prompt you to log in or create a new account, as in Figure 14-14. Authenticated users can be passed between apps, so if you or a user have authenticated to Game Center in another app, this can be passed to your app without prompting you to log in again.

9781430245995_Fig14-14.jpg

Figure 14-14.  The Lucky app prompting for a Game Center account and then displaying a welcoming message on the menu screen

As a final touch before moving on to implement Game Center features, you add a button to the menu screen allowing the user to go to Game Center without leaving your app.

Displaying Game Center From Your App

Open the ViewController.xib file again and add a button so that the user interface resembles Figure 14-15.

9781430245995_Fig14-15.jpg

Figure 14-15.  The menu screen with a Visit Game Center button

Create an action with the name showGameCenter: for when the user taps the new button.

Next, go to ViewController.h and add the GKGameCenterControllerDelegate protocol:

//
//  ViewController.h
//  Lucky
//
 
#import <UIKit/UIKit.h>
#import "GameViewController.h"
#import <GameKit/GameKit.h>
 
@interface ViewController : UIViewController <GKGameCenterControllerDelegate>
 
@property (weak, nonatomic) IBOutlet UILabel *welcomeLabel;
@property (strong, nonatomic) GKLocalPlayer *player;
 
- (IBAction)playEasyGame:(id)sender;
- (IBAction)playNormalGame:(id)sender;
- (IBAction)playHardGame:(id)sender;
- (IBAction)showGameCenter:(id)sender;
 
@end

Now, go to ViewController.m and add the following implementation to the showGameCenter: action method:

- (IBAction)showGameCenter:(id)sender
{
    GKGameCenterViewController *gameCenterController =  
        [[GKGameCenterViewController alloc] init];
    if (gameCenterController != nil)
    {
        gameCenterController.gameCenterDelegate = self;
        [self presentViewController:gameCenterController animated:YES completion:nil];
    }
}

Finally, add the following delegate method to dismiss the Game Center view controller when the user is finished with it:

- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

Now that you have a basic Game Center aware app all set up, it’s time to start implementing some of the Game Center features, starting with Leaderboards.

Recipe 14-2: Implementing Leaderboards

Competing against others is an essential ingredient in gaming. The possibility for players to compare highscores and compete is an effective way to increase the replay factor of any game. With Game Center, this feature is easily implemented using Leaderboards. In this recipe you build on the project from Recipe 14-1 and implement Leaderboard support.

The first thing you need to do is to define in iTunes Connect the Leaderboards you’ll be using.

Defining the Leaderboards

Set up three different Leaderboards for your app, one for each difficulty level.

Log in to itunesconnect.apple.com and click the Manage Your Applications link. Select the Lucky app that you registered in Recipe 14-1 and then click the Manage Game Center button.

In the Leaderboards section, click the Add Leaderboard button. Now, in the Add Leaderboard page (see Figure 14-16), choose to create a Single Leaderboard.

9781430245995_Fig14-16.jpg

Figure 14-16.  Creating a Single Leaderboard

Note  Once a leaderboard has gone live for an app, it cannot be deleted, so create leaderboards with some thought. You can have up to 25 leaderboards per app. This allows you to create multiple leaderboards for different difficulties or even one for each level of your game, whatever makes the most sense.

Fill in a name and an identifier for the leaderboard. The name is an internal name for tracking purposes and will not be displayed to the player. (The display name is configured in the next step when adding a language.) Now select the score format type; in this case, you’re going to use a simple integer, but you also can use time-based, floats, and currency. Select the sort order for your leaderboard. If you want high scores at the top (typical), then sort “High to Low”; if you want your low scores at the top (for instance in a golf game), then sort “Low to High.”

You also need to set up at least one language for the leaderboard. To do that, click the Add Language button. Select the language and then enter a display name for the leaderboard; this is the name that is visible to the player in the game. You can set the formatting of the score and what unit to call them (singular and plural); in this case, they are “Point” and “Points.”

Figure 14-17 shows the configurations you’ll be using for this Leaderboard.

9781430245995_Fig14-17.jpg

Figure 14-17.  Configuring a Leaderboard for Lucky easy game high scores

Once complete, click Save to store the Leaderboard.

Repeat the process for the remaining two Leaderboards, this time using Lucky.normal and Lucky.hard as Leaderboard IDs. The resulting Leaderboards section should now resemble Figure 14-18.

9781430245995_Fig14-18.jpg

Figure 14-18.  Three Leaderboards registered for the Lucky game in iTunes Connect

Now, let’s dive in to some code.

Reporting Scores to Game Center

To report a score to Game Center, use the GKScore class. Go to the GameViewController.m file and add the following helper method:

- (void)reportScore:(int64_t)score forLeaderboard: (NSString*)leaderboardID
{
    GKScore *gameCenterScore = [[GKScore alloc] initWithCategory:leaderboardID];
    gameCenterScore.value = score;
    gameCenterScore.context = 0;
     
    [gameCenterScore reportScoreWithCompletionHandler:^(NSError *error)
     {
         if (error)
         {
             NSLog(@"Error reporting score: %@", error);
         }
     }];
}

The preceding method creates and initiates a new GKScore object, which it then reports to Game Center providing a completion handler.

Note  If a score could not be reported due to connection problems, Game Kit stores the request and tries to resend the score later, making any kind of retry handling unnecessary in the preceding completion handler.

You can now use the reportScore:forLeaderboard: helper method to report the score to Game Center when a game has finished. In this app, you do this right after the user has dismissed the Game Over alert view. Add the following code to the alertView:didDismissWithButtonIndex: delegate method:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    [self.navigationController popViewControllerAnimated:YES];
    if ([GKLocalPlayer localPlayer].isAuthenticated)
    {
        [self reportScore:_score forLeaderboard:[self leaderboardID]];
    }
}

The leaderboardID helper method returns the ID that corresponds to the current game level. Here is its implementation:

- (NSString *)leaderboardID
{
    switch (_level) {
        case 0:
            return @"Lucky.easy";
        case 1:
            return @"Lucky.normal";
        case 2:
            return @"Lucky.hard";
        default:
            return @"";
    }
}

You can now build and run the app, play a game, and your score is automatically reported to Game Center. You can use the Visit Game Center button to view the resulting Leaderboard, as shown in Figure 14-19.

9781430245995_Fig14-19.jpg

Figure 14-19.  A Game Center Leaderboard with one score

Recipe 14-3: Implementing Achievements

Achievements in games are similar to badges and other unlockables in other apps and games. Using Game Center Achievements you can provide your players with a notification when they reach certain milestones. Achievements make most sense in games with natural milestones, but to show you how they work you’ll set one up for the Lucky game project that you’ve built on in Recipes 14-1 and 14-2.

Specifically, you reward the player with an Achievement if he or she has managed to use all four buttons in a game. As with Leaderboards, you’ll start by registering the Achievements in iTunes Connect.

Defining Achievements in iTunes Connect

Again, log in to itunesconnect.apple.com. Click Manage Your Applications, click the app you have set up for Game Center, and then the Manage Game Center button.

You’ll define three achievements, one for each level of difficulty. Start by clicking the Add Achievement button in the Manage Game Center page. Enter a name, ID, and point value as shown in Figure 14-20. Also, set the Hidden and the Achievable More Than Once options to No.

9781430245995_Fig14-20.jpg

Figure 14-20.  Configuring a Game Center Achievement in iTunes Connect

Each game can have up to 1,000 achievement points. These points can be assigned to different achievements as you see fit, but each achievement can have a max of only 100 achievement points, which is what you used here.

As with Leaderboards, you can’t save an achievement until you add at least one language to it. To do that, click Add Language button. The resulting view resembles Figure 14-21 (after filling out the fields). Here, you can set the achievement title as well as the “pre-earned” description. The pre-earned description should detail how the achievement is earned. There is also an earned description, which is the description shown after the achievement is earned.

9781430245995_Fig14-21.jpg

Figure 14-21.  Configuring a language for a Game Center Achievement

Note that you need an image to depict the Achievement in the Game Center app. You can download premade images for the three Achievements from the web page of this book:

  • All Four Buttons Achievement Easy.png
  • All Four Buttons Achievement Normal.png
  • All Four Buttons Achievement Hard.png

Save the language and then the Achievement. You can now repeat the process for the other two Achievements, using the IDs AllFourButtons.normal and AllFourButtons.hard, respectively.

Your list of Achievements should now resemble Figure 14-22.

9781430245995_Fig14-22.jpg

Figure 14-22.  Three Achievements defined in iTunes Connect

You are now finished with the configuration of the achievements. Code writing is next.

Reporting the Achievements

This app keeps track of which buttons have been tapped. You do this using a simple NSMutableArray. Start by going to GameViewController.h and add the following instance variable:

//
//  GameViewController.h
//  Lucky
//
 
#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
 
@interface GameViewController : UIViewController<UIAlertViewDelegate>
{
    @private
    int _score;
    int _level;
    NSMutableArray *_selectedButtons;
}
 
// . . .
 
@end

Next, go to ViewController.m and add code to instantiate the array in the initWithLevel: method:

- (id)initWithLevel:(int)level
{
    self = [super initWithNibName:nil bundle:nil];
    if (self)
    {
        _level = level;
        _score = 0;
        _selectedButtons = [[NSMutableArray alloc] initWithCapacity:4];
    }
    return self;
}

Finally, in the gameButtonSelected: action method, add the following code:

- (IBAction)gameButtonSelected:(UIButton *)sender
{
    if (sender.tag == 0)
    {
        // Safe, continue game
        _score += 1;
        [self updateScoreLabel];
        [self setupButtons];
        if (![_selectedButtons containsObject:sender])
        {
            [_selectedButtons addObject:sender];
            if (_selectedButtons.count == 4)
            {
                [self reportAllFourButtonsAchievementCompleted];
            }
        }
    }
    else
    {
        // Game Over
        // . . .
    }
}

The code adds the selected button to the array if it hasn’t already been added. Then, if all four buttons have been tapped, it reports the achievement to Game Center using the helper method you implement next.

Reporting an achievement is very similar to the way you report Leaderboard scores, except you use the class GKAchievement instead of GKScore. Here is the implementation of the reportAllFourButtonsAchievementCompleted helper method:

- (void)reportAllFourButtonsAchievementCompleted
{
    NSString *achievementID = [self achievementID];
    GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:achievementID];
    if (achievement != nil)
    {
        achievement.percentComplete = 100;
        achievement.showsCompletionBanner = NO;
        [achievement reportAchievementWithCompletionHandler:^(NSError *error)
         {
             if (error != nil)
             {
                 NSLog(@"Error when reporting achievement: %@", error);
             }
             else
             {
                 [GKNotificationBanner showBannerWithTitle:@"Achievement Completed"
                     message:@"You have used all four buttons and earned 100 points!"
                     completionHandler:nil];
             }
         }];
    }
}

Note  For achievements that have a way to track sub milestones, you can use the percentComplete property to report partial progress. For the purpose of this Recipe, you directly set the percentComplete property to 100, which means the achievement is fully completed.

Finally, implement the achievementID helper method, which returns the achievement ID based on the current game level:

- (NSString *)achievementID
{
    switch (_level) {
        case 0:
            return @"AllFourButtons.easy";
        case 1:
            return @"AllFourButtons.normal";
        case 2:
            return @"AllFourButtons.hard";
        default:
            return @"";
    }
}

You can now build and run the app again. Start a game and try to use all four buttons. If you’re lucky and don’t hit a “killer,” you’ll be awarded an achievement. Figure 14-23 shows the Game Center view controller displaying a player who has been awarded all three available achievements.

9781430245995_Fig14-23.jpg

Figure 14-23.  A player who has completed three achievements in the game

With the current implementation, an achievement will be reported even though the player already has completed it. Let’s fix that. What you’ll do is to cache the achievements of the current player so that you can check whether an achievement has been completed before reporting it to Game Center.

Start by adding an NSMutableDictionary property to hold the achievements. Go to ViewController.h and add the following declaration:

//
//  ViewController.h
//  Lucky
//
 
#import <UIKit/UIKit.h>
#import "GameViewController.h"
#import <GameKit/GameKit.h>
 
@interface ViewController : UIViewController<GKGameCenterControllerDelegate>
 
@property (weak, nonatomic) IBOutlet UILabel *welcomeLabel;
@property (strong, nonatomic) GKLocalPlayer *player;
@property (strong, nonatomic) NSMutableDictionary *achievements;
 
// . . .
 
@end

Next, go to ViewController.m and add the following line to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    self.achievements = [[NSMutableDictionary alloc] init];
    self.title = @"Lucky";
    self.player = nil;
    [self authenticatePlayer];
}

In the setter method of the player property, add the following lines to initiate the achievements dictionary:

- (void)setPlayer:(GKLocalPlayer *)player
{
    if (_player == player)
        return;
    [self.achievements removeAllObjects];
     
    _player = player;
     
    NSString *playerName;
    if (_player)
    {
        playerName = _player.alias;
        [self loadAchievements];
    }
    else
    {
        playerName = @"Anonymous Player";
    }
    self.welcomeLabel.text = [NSString stringWithFormat:@"Welcome %@", playerName];
}

The loadAchievements helper method loads the achievements for the current player and populates the dictionary, like so:

- (void)loadAchievements
{
    [GKAchievement loadAchievementsWithCompletionHandler:
    ^(NSArray *achievements, NSError *error)
     {
         if (error == nil)
         {
             for (GKAchievement* achievement in achievements)
                 [self.achievements setObject: achievement
                     forKey: achievement.identifier];
         }
         else
         {
             NSLog(@"Error loading achievements: %@", error);
         }
     }];
}

Now initiate the game view controller with the achievements dictionary. First, go to GameViewController.h and make the following changes:

//
//  GameViewController.h
//  Lucky
//
 
#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
 
@interface GameViewController : UIViewController<UIAlertViewDelegate>
{
    @private
    int _score;
    int _level;
    NSMutableArray *_selectedButtons;
}
 
@property (weak, nonatomic) IBOutlet UILabel *scoreLabel;
@property (weak, nonatomic) IBOutlet UIButton *button1;
@property (weak, nonatomic) IBOutlet UIButton *button2;
@property (weak, nonatomic) IBOutlet UIButton *button3;
@property (weak, nonatomic) IBOutlet UIButton *button4;
@property (strong, nonatomic) NSMutableDictionary *achievements;
 
- (IBAction)gameButtonSelected:(UIButton *)sender;
 
- (id)initWithLevel:(int)level achievements:(NSMutableDictionary *)achievements;
 
@end

In GameViewController.m, make the corresponding changes the initWithLevel: method:

- (id)initWithLevel:(int)level achievements:(NSMutableDictionary *)achievements
{
    self = [super initWithNibName:nil bundle:nil];
    if (self)
    {
        _level = level;
        _score = 0;
        _selectedButtons = [[NSMutableArray alloc] initWithCapacity:4];
        self.achievements = achievements;
    }
    return self;
}

Next, define a helper method to get the current achievement by fetching it from the cache or create a new GKAchievement object if it doesn’t exist:

- (GKAchievement *)getAchievement
{
    NSString *achievementID = [self achievementID];
    GKAchievement *achievement = [self.achievements objectForKey:achievementID];
    if (achievement == nil)
    {
        achievement = [[GKAchievement alloc] initWithIdentifier:achievementID];
        [self.achievements setObject:achievement forKey:achievement.identifier];
    }
    return achievement;
}

Finally, change the reportAllFourButtonsAchievement method to use the new helper method:

- (void)reportAllFourButtonsAchievementCompleted
{
    GKAchievement *achievement = [self getAchievement];
    if (achievement != nil && !achievement.completed)
    {
        achievement.percentComplete = 100;
        achievement.showsCompletionBanner = NO;
        [achievement reportAchievementWithCompletionHandler:^(NSError *error)
         {
             if (error != nil)
             {
                 NSLog(@"Error when reporting achievement: %@", error);
             }
             else
             {
                 [GKNotificationBanner showBannerWithTitle:@"Achievement Completed"
                     message:@"You have used all four buttons and earned 100 points!"
                     completionHandler:nil];
             }
         }];
    }
}

That’s it. The app now only reports real progress and therefore does not unnecessarily use up network activity for reports that don’t change the state of the Game Center.

Recipe 14-4: Creating a Simple Turn-Based Multiplayer Game

Gaming at its essence, is a social activity. While playing against a computer can be fun, playing with or against other humans puts a whole new dimension to the gaming experience. Game Kit has great support for multiplayer games, both real-time and turn-based. In this chapter you’ll build a simple Tic Tac Toe multiplayer game that uses Game Center for matchmaking and the low-level network implementation.

As usual when building a Game Center aware app, start with the basic game functionality before adding any Game Center support.

Building the Tic Tac Toe Game

Create a new single-view project called “Tic Tac Toe.”

Start by making this a Navigation-based app. Go to AppDelegate.h and add a UINavigationController property:

//
//  AppDelegate.h
//  Tic Tac Toe
//
 
#import <UIKit/UIKit.h>
 
@class ViewController;
 
@interface AppDelegate : UIResponder <UIApplicationDelegate>
 
@property (strong, nonatomic) UIWindow *window;
 
@property (strong, nonatomic) ViewController *viewController;
@property (strong, nonatomic) UINavigationController *navigationController;
 
@end

Switch to AppDelegate.m and make the following changes to the application:didFinishLaunchingWithOptions: method:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.viewController =
        [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
    self.navigationController =  
        [[UINavigationController alloc] initWithRootViewController:self.viewController];
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    return YES;
}

Next, build the basic user interface of the game. Open the ViewController.xib file. To help you account for the Navigation bar when laying out your user interface elements later, go to the Attributes inspector and set Top Bar attribute in the Simulated Metrics section to “Navigation Bar.”

Now, add a label and nine buttons to the view so that it resembles Figure 14-24.

9781430245995_Fig14-24.jpg

Figure 14-24.  A simple user interface for a Tic Tac Toe game

Create outlets with the following names for the respective element:

  • statusLabel
  • row1Col1Button, row1Col2Button, row1Col3Button
  • row2Col1Button, row2Col2Button, row2Col3Button
  • row3Col1Button, row3Col2Button, row3Col3Button

Also, create an action with the name selectButton with the parameter type UIButton. Connect all the nine buttons to that action.

Next, go to ViewController.h and add the following private instance variable:

//
//  ViewController.h
//  Tic Tac Toe
//
 
#import <UIKit/UIKit.h>
 
@interface ViewController : UIViewController
{
    @private
    NSString *_currentMark;
}
 
@property (weak, nonatomic) IBOutlet UILabel *statusLabel;
@property (weak, nonatomic) IBOutlet UIButton *row1Col1Button;
@property (weak, nonatomic) IBOutlet UIButton *row1Col2Button;
@property (weak, nonatomic) IBOutlet UIButton *row1Col3Button;
@property (weak, nonatomic) IBOutlet UIButton *row2Col1Button;
@property (weak, nonatomic) IBOutlet UIButton *row2Col2Button;
@property (weak, nonatomic) IBOutlet UIButton *row2Col3Button;
@property (weak, nonatomic) IBOutlet UIButton *row3Col1Button;
@property (weak, nonatomic) IBOutlet UIButton *row3Col2Button;
@property (weak, nonatomic) IBOutlet UIButton *row3Col3Button;
 
- (IBAction)selectButton:(UIButton *)sender;
 
@end

Because it’s not the essential part of this recipe, we skip the details of the basic game implementation and instead ask you go to the web page of this book (at www.apress.com), download the file ViewController.m and add it to your project. However, be sure you go through the code carefully so that you understand how it works before moving on.

If you’ve added the code correctly, you should be able to run the app now and play a game of Tic Tac Toe against yourself. Figure 14-25 shows an example of a game that’s half-way complete.

9781430245995_Fig14-25.jpg

Figure 14-25.  An ongoing game of Tic Tac Toe

With the basic game functionality in place, you can move on to implement Game Center support. As you know by now, that starts with registering your app.

Preparing the Game for Game Center

First, you need to create a new App ID for your game in the iOS Provisioning Portal. For details on how to do this, refer to Recipe 14-1. Enter iOS 6 Recipes Tic Tac Toe as the App ID description and be sure you enter the bundle identifier of your project.

After you’ve submitted the new App ID in the Provisioning Portal, the next step is to register your app with iTunes Connect. Again, refer to Recipe 14-1 for details. To save time, use the following art files from this book’s web page, for the Large App Icon and iPhone Screenshot files that you must upload as a part of the registration.

  • Tic Tac Toe Large App Icon.png
  • Tic Tac Toe Screenshot.png
  • Tic Tac Toe Screenshot (iPhone 5).png

Also, don’t forget to enable Game Center (for Single Game) in the Manage Game Center page. Enabling Game Center is all you need to do for the sake of this Recipe; there’s no need to define any Leaderboards or Achievements for the Tic Tac Toe game.

When you’ve registered the app with iTunes Connect, you can start preparing it for Game Center. Go back to Xcode and link the Game Kit framework to your project. Then open up ViewController.h and add the following property declaration to hold a reference to the local player:

//
//  ViewController.h
//  Tic Tac Toe
//
 
#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
 
@interface ViewController : UIViewController
{
@private
    NSString *_currentMark;
}
 
@property (weak, nonatomic) IBOutlet UILabel *statusLabel;
@property (weak, nonatomic) IBOutlet UIButton *row1Col1Button;
@property (weak, nonatomic) IBOutlet UIButton *row1Col2Button;
@property (weak, nonatomic) IBOutlet UIButton *row1Col3Button;
@property (weak, nonatomic) IBOutlet UIButton *row2Col1Button;
@property (weak, nonatomic) IBOutlet UIButton *row2Col2Button;
@property (weak, nonatomic) IBOutlet UIButton *row2Col3Button;
@property (weak, nonatomic) IBOutlet UIButton *row3Col1Button;
@property (weak, nonatomic) IBOutlet UIButton *row3Col2Button;
@property (weak, nonatomic) IBOutlet UIButton *row3Col3Button;
 
@property (strong, nonatomic) GKLocalPlayer *localPlayer;
 
- (IBAction)selectButton:(UIButton *)sender;
 
@end

The method to authenticate the local player is identical to what you did in Recipe 14-1. Go to ViewController.m and add the authenticateLocalPlayer helper method:

- (void)authenticateLocalPlayer
{
    GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
    localPlayer.authenticateHandler =
    ^(UIViewController *authenticateViewController, NSError *error)
    {
        if (authenticateViewController != nil)
        {
            [self presentViewController:authenticateViewController animated:YES
                completion:nil];
        }
        else if (localPlayer.isAuthenticated)
        {
            self.localPlayer = localPlayer;
        }
        else
        {
            // Disable Game Center
            self.localPlayer = nil;
        }
    };
}

As in Recipe 14-1, you try to authenticate the local player directly on app launch. Add the following line to the viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
    UIBarButtonItem *playButton = [[UIBarButtonItem alloc] initWithTitle:@"Play"
        style:UIBarButtonItemStyleBordered target:self action:@selector(playGame:)];
    self.navigationItem.rightBarButtonItem = playButton;
    [self enableSquareButtons:NO];
    self.title = @"Tic Tac Toe";
    self.statusLabel.text = @"Press Play to start a game";
    [self authenticateLocalPlayer];
}

Finally, add a couple of elements to the user interface for displaying the two Game Center players currently participating in the match. Open ViewController.xib, add four labels, and arrange them so that the user interface resembles Figure 14-26. Note that “Playing X:” and “<Player 1 Label>,” as well as “Playing O:” and “<Player 2 Label>” are separate labels.

9781430245995_Fig14-26.jpg

Figure 14-26.  A simple Tic Tac Toe user interface with labels showing the participating players

Create outlets called player1Label and player2Label for the corresponding labels.

Now that you have the basic Game Center authentication in place, you can go on to implement the next step, which is the matchmaking feature.

Implementing Matchmaking

For this recipe, you use the standard view controller provided by the Game Kit framework, to allow the user to find other players to play your Tic Tac Toe game with, and to keep track of the games he or she is currently involved in. Using the standard view controller saves you a lot of trouble implementing these matchmaking features.

Specifically, you’re going to use the GKTurnBasedMatchmakerViewController to handle the matchmaking. You’ll start by making the main view controller conform to the GKTurnBasedMatchmakerViewControllerDelegate protocol. You also need to hold a reference to a GKTurnBasedMatch object, as well as two GKPlayer instances. So, go to ViewController.h and add the following code:

//
//  ViewController.h
//  Tic Tac Toe
//
 
// . . .
 
@interface ViewController : UIViewController <GKTurnBasedMatchmakerViewControllerDelegate>
 
// . . .
 
@property (strong, nonatomic) GKLocalPlayer *localPlayer;
@property (strong, nonatomic) GKTurnBasedMatch *match;
@property (strong, nonatomic) GKPlayer *player1;
@property (strong, nonatomic) GKPlayer *player2;
 
- (IBAction)selectButton:(UIButton *)sender;
 
@end

Now, return to ViewController.m and replace the current implementation of the playGame: action method with the following code:

- (void)playGame:(id)sender
{
    if (self.localPlayer.isAuthenticated)
    {
        GKMatchRequest *request = [[GKMatchRequest alloc] init];
        request.minPlayers = 2;
        request.maxPlayers = 2;
         
        GKTurnBasedMatchmakerViewController *matchMakerViewController =  
            [[GKTurnBasedMatchmakerViewController alloc] initWithMatchRequest:request];
        matchMakerViewController.turnBasedMatchmakerDelegate = self;
        [self presentViewController:matchMakerViewController animated:YES  
            completion:nil];
    }
    else
    {
        UIAlertView *notLoggedInAlert = [[UIAlertView alloc] initWithTitle:@"Error"  
            message:@"You must be logged into Game Center to play this game!"  
            delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];
        [notLoggedInAlert show];
    }
}

What the preceding code does, is to check whether the user has signed in with Game Center; if not, an error message is displayed; otherwise the method proceeds to create a match request and present a matchmaker view controller.

The matchmaker view controller requires you to implement a few delegate methods to handle the result of the user’s decisions. The first is if the user cancels the dialog, in which case you should simply dismiss the matchmaker view controller:

- (void)turnBasedMatchmakerViewControllerWasCancelled:
(GKTurnBasedMatchmakerViewController *)viewController
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

The next scenario is if the view controller fails for some reason. Apart from dismissing the view, you also log the error:

- (void)turnBasedMatchmakerViewController:
(GKTurnBasedMatchmakerViewController *)viewController didFailWithError:(NSError *)error
{
    [self dismissViewControllerAnimated:YES completion:nil];
    NSLog(@"Error while matchmaking: %@", error);
}

Finally, if the matchmaker produced a match, the viewController:didFindMatch: delegate method is invoked with a GKTurnBasedMatch object. For now, you just store a reference to the match object, like so:

- (void)turnBasedMatchmakerViewController:
(GKTurnBasedMatchmakerViewController *)viewController didFindMatch:(GKTurnBasedMatch *)match
{
    [self dismissViewControllerAnimated:YES completion:nil];
     
    self.match = match;
}

Receiving the match object from Game Center is a key event when implementing a turn-based game. At that point, the app should load the game data and set up the user interface to reflect the current state of the game.

To handle these things, add a custom setter method for the match property. It loads the participating players and the match data, using two currently nonexistent helper methods that you’ll implement in a minute:

- (void)setMatch:(GKTurnBasedMatch *)match
{
    _match = match;
     
    [self loadPlayers];
    [self loadMatchData];
}

Let’s start with the loadPlayers method. Roughly what you want to do, is to identify the players participating in the match and load information about them from the Game Center. The match object contains an array of GKTurnBasedParticipant objects, which you can use to get the playerIDs you need to load the information. Because you know that a game of Tic Tac Toe contains exactly two players, which is what you defined in the matchmaking request earlier, you can extract the participant objects like so:

- (void)loadPlayers
{
    GKTurnBasedParticipant *participant1 = [self.match.participants objectAtIndex:0];
    GKTurnBasedParticipant *participant2 = [self.match.participants objectAtIndex:1];
 
    // TODO: Load player info
}

Note  The participants array of the match object is arranged in the order of how the players take turns. You can therefore assume that the first object is player 1, and the second object is player 2 of the game.

A turn-based match can start without all seats being filled. This way, a player who starts a new match can make the first move without having to wait for the other player. Because of this, the playerID of the opponent’s participant object may be nil at this point. For that reason, you need to design your code with care so that you don’t send undefined playerIDs to the loadPlayersForIdentifiers:withCompletionHandler: method. Add the following code to the loadPlayers method to handle that:

- (void)loadPlayers
{
    GKTurnBasedParticipant *participant1 = [self.match.participants objectAtIndex:0];
    GKTurnBasedParticipant *participant2 = [self.match.participants objectAtIndex:1];
     
    NSMutableArray *playerIDs = [[NSMutableArray alloc] initWithCapacity:2];
    if (participant1.playerID &&  
        ![participant1.playerID isEqualToString:self.player1.playerID])
    {
        [playerIDs addObject:participant1.playerID];
    }
    if (participant2.playerID  &&  
        ![participant2.playerID isEqualToString:self.player2.playerID])
    {
        [playerIDs addObject:participant2.playerID];
    }
     
    if (playerIDs.count == 0)
        return; // No players to load
     
    [GKPlayer loadPlayersForIdentifiers:playerIDs withCompletionHandler:
     ^(NSArray *players, NSError *error)
     {
         // TODO: Handle Result   
     }];
}

Finally, when the players’ objects have been loaded, you need to figure out which is which by checking their playerIDs. Here’s the complete loadPlayers method:

- (void)loadPlayers
{
    GKTurnBasedParticipant *participant1 = [self.match.participants objectAtIndex:0];
    GKTurnBasedParticipant *participant2 = [self.match.participants objectAtIndex:1];
     
    NSMutableArray *playerIDs = [[NSMutableArray alloc] initWithCapacity:2];
    if (participant1.playerID && ![participant1.playerID isEqualToString:self.player1.playerID])
    {
        [playerIDs addObject:participant1.playerID];
    }
    if (participant2.playerID  && ![participant2.playerID isEqualToString:self.player2.playerID])
    {
        [playerIDs addObject:participant2.playerID];
    }
     
    if (playerIDs.count == 0)
        return; // No players to load
     
    [GKPlayer loadPlayersForIdentifiers:playerIDs withCompletionHandler:^(NSArray *players, NSError *error)
     {
         if (players)
         {
             GKPlayer *player1;
             GKPlayer *player2;
             for (GKPlayer *player in players)
             {
                 if ([player.playerID isEqualToString:participant1.playerID])
                 {
                     player1 = player;
                 }
                 else if ([player.playerID isEqualToString:participant2.playerID])
                 {
                     player2 = player;
                 }
             }
             dispatch_async(dispatch_get_main_queue(),^{
                 self.player1 = player1;
                 self.player2 = player2;
             });
         }
         if (error)
         {
             NSLog(@"Error loading players: %@", error);
         }
     }];
}

The reason you wrap the assigning of the player1 and player2 properties within a dispatch_async call in the preceding code, is because these assignments trigger an update of the user interface, so that piece of code needs to run on the main thread.

When the player1 and player2 properties are set, the respective label should be updated. To accomplish this, add the following custom setter methods:

- (void)setPlayer1:(GKPlayer *)player1
{
    _player1 = player1;
    if (_player1)
    {
        self.player1Label.text = _player1.displayName;
    }
    else
    {
        self.player1Label.text = @"<vacant>";
    }
}
 
- (void)setPlayer2:(GKPlayer *)player2
{
    _player2 = player2;
    if (_player2)
    {
        self.player2Label.text = _player2.displayName;
    }
    else
    {
        self.player2Label.text = @"<vacant>";
    }
}

Now, to initiate the player labels on app launch, you simply need set the player1 and player2 properties to nil in viewDidLoad, like so:

- (void)viewDidLoad
{
    [super viewDidLoad];
    UIBarButtonItem *playButton = [[UIBarButtonItem alloc] initWithTitle:@"Play"
        style:UIBarButtonItemStyleBordered target:self action:@selector(playGame:)];
    self.navigationItem.rightBarButtonItem = playButton;
    [self enableSquareButtons:NO];
    self.title = @"Tic Tac Toe";
    self.statusLabel.text = @"Press Play to start a game";
    self.player1 = nil;
    self.player2 = nil;
    [self authenticateLocalPlayer];
}

Next, you’ll turn your focus to the loadMatchData helper method. But before you start implementing that, let’s add a couple of helper methods to encode and decode such data.

Encoding and Decoding Match Data

It’s completely up to you to encode and decode the data you need to store the state of the game in Game Center. The only restriction is that you keep the size of the data within 64k. For the sake of this recipe, keep it simple and store the current state in a simple array, which you then transform to a NSData object using the NSKeyedArchiver class. Here is the implementation:

- (NSData *)encodeMatchData
{
    NSArray *stateArray = @[@1 /* version */,
    self.row1Col1Button.currentTitle, self.row1Col2Button.currentTitle,
        self.row1Col3Button.currentTitle,
    self.row2Col1Button.currentTitle, self.row2Col2Button.currentTitle,
        self.row2Col3Button.currentTitle,
    self.row3Col1Button.currentTitle, self.row3Col2Button.currentTitle,
        self.row3Col3Button.currentTitle
    ];
    return [NSKeyedArchiver archivedDataWithRootObject:stateArray];
}

It’s generally a good idea to store a version number with the data in case you need to change the storage format in future upgrades of your app. This is why we added a number (1) as the first object of the array.

The corresponding decode helper method does the reverse and extracts the current state from the provided NSData object, like so:

- (void)decodeMatchData:(NSData *)matchData
{
    NSArray *stateArray = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
     
    [self.row1Col1Button setTitle:[stateArray objectAtIndex:1]
        forState:UIControlStateNormal];
    [self.row1Col2Button setTitle:[stateArray objectAtIndex:2]
        forState:UIControlStateNormal];
    [self.row1Col3Button setTitle:[stateArray objectAtIndex:3]
        forState:UIControlStateNormal];
    [self.row2Col1Button setTitle:[stateArray objectAtIndex:4]
        forState:UIControlStateNormal];
    [self.row2Col2Button setTitle:[stateArray objectAtIndex:5]
        forState:UIControlStateNormal];
    [self.row2Col3Button setTitle:[stateArray objectAtIndex:6]
        forState:UIControlStateNormal];
    [self.row3Col1Button setTitle:[stateArray objectAtIndex:7]
        forState:UIControlStateNormal];
    [self.row3Col2Button setTitle:[stateArray objectAtIndex:8]
        forState:UIControlStateNormal];
    [self.row3Col3Button setTitle:[stateArray objectAtIndex:9]
        forState:UIControlStateNormal];
}

The loadMatchData method retrieves the stored data from Game Center and decodes it using the decodeMatchData method you just added. However, if the match contains no data yet, the method will instead reset the user interface to set the state for a new game. Here is the implementation:

- (void)loadMatchData
{
    [_match loadMatchDataWithCompletionHandler:^(NSData *matchData, NSError *error)
     {
         dispatch_async(dispatch_get_main_queue(),^{
             if (matchData.length > 0)
             {
                 [self decodeMatchData:matchData];
             }
             else
             {
                 [self resetButtonTitles];
             }
             NSString *currentMark;
             if ([self localPlayerIsCurrentPlayer])
             {
                 [self enableSquareButtons:YES];
                 currentMark = [self localPlayerMark];
             }
             else
             {
                 [self enableSquareButtons:NO];
                 currentMark = [self opponentMark];
             }
             self.statusLabel.text =
                 [NSString stringWithFormat:@"%@'s turn", currentMark];
         });
     }];
}

The preceding piece of code makes use of three helper methods that you’ve not yet defined. The localPlayerIsCurrentPlayer checks the currentParticipant property of the match object and compares it with the identity of the local player:

- (BOOL)localPlayerIsCurrentPlayer
{
    return [self.localPlayer.playerID
        isEqualToString:self.match.currentParticipant.playerID];
}

The localPlayerMark returns “X” or “O” depending on whether the local player is player 1 or player 2.

- (NSString *)localPlayerMark
{
    if ([self.localPlayer.playerID isEqualToString:self.player1.playerID])
    {
        return @"X";
    }
    else
    {
        return @"O";
    }
}

And the opponentMark simply returns the opposite of localPlayerMark:

- (NSString *)opponentMark
{
    if ([[self localPlayerMark] isEqualToString:@"X"])
    {
        return @"O";
    }
    else
    {
        return @"X";
    }
}

Now that you have code to load match data all set up, let’s look at the methods where your app saves the data. One place where you’re expected to store an updated state to Game Center is when the local player has made a move and hands over the turn to the opponent.

But before we look at updating the advanceTurn method, let’s make a necessary change to the selectButton: action method. You no longer rely on the _currentMark instance variable when setting the mark of a selected square button. In fact, when you’ve finished implementing the Game Center support, you can completely remove the _currentMark instance variable. Instead, you can safely assume that it’s the local player who is making the moves (which is right because the opponent makes his or her moves remotely from another instance of the app). So, make the following change to the existing code:

- (IBAction)selectButton:(UIButton *)sender
{
    if (sender.currentTitle.length != 0)
    {
        UIAlertView *squareOccupiedAlert =
            [[UIAlertView alloc] initWithTitle:@"Invalid Move"
                message:@"You can only pick empty squares" delegate:nil
                cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [squareOccupiedAlert show];
        return;
    }
     
    [sender setTitle: [self localPlayerMark]forState:UIControlStateNormal];
    [self checkCurrentState];
}

Now, the new implementation of the advanceTurn method saves the current state to Game Center, disables the square buttons and hands over the turn to the opponent. Here is the new code doing this:

- (void)advanceTurn
{
    [self enableSquareButtons:NO];
    self.statusLabel.text =  
        [NSString stringWithFormat:@"%@'s turn", [self opponentMark]];
    self.match.message = self.statusLabel.text;
    NSData *matchData = [self encodeMatchData];
    [self.match endTurnWithNextParticipants:@[[self opponentParticipant]]  
        turnTimeout:GKTurnTimeoutDefault matchData:matchData completionHandler:
     ^(NSError *error)
     {
         if (error)
         {
             NSLog(@"Error advancing turn: %@", error);
         }
     }];
}

The preceding code hands over the turn using a helper method to identify the opponent’s participant object. Here is the implementation for that method:

- (GKTurnBasedParticipant *)opponentParticipant
{
    GKTurnBasedParticipant *candidate = [self.match.participants objectAtIndex:0];
    if ([self.localPlayer.playerID isEqualToString:candidate.playerID])
    {
        return [self.match.participants objectAtIndex:1];
    }
    else
    {
        return candidate;
    }
}

Another place you should store the game state is when the game has ended. There are two reasons why this is necessary. First, this is how the opponent will know the final state of the game. Second, the players can open a game that’s ended to view the final state again.

In addition to saving the final state, when a game ends your app needs to set the matchOutcome property of the participant objects. To do these things, make the following changes to the gameEndedWithWinner: method:

- (void)gameEndedWithWinner:(NSString *)mark
{
    NSString *message = [NSString stringWithFormat:@"%@ won!", mark];
    UIAlertView *gameOverAlert = [[UIAlertView alloc] initWithTitle:@"Game Over"
        message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [gameOverAlert show];
     
    self.statusLabel.text = message;
    self.match.message = self.statusLabel.text;
     
    GKTurnBasedParticipant *participant1 = [self.match.participants objectAtIndex:0];
    GKTurnBasedParticipant *participant2 = [self.match.participants objectAtIndex:1];
    participant1.matchOutcome = GKTurnBasedMatchOutcomeTied;
    participant2.matchOutcome = GKTurnBasedMatchOutcomeTied;
     
    if ([participant1.playerID isEqualToString:self.localPlayer.playerID])
    {
        participant1.matchOutcome = GKTurnBasedMatchOutcomeWon;
        participant2.matchOutcome = GKTurnBasedMatchOutcomeLost;
    }
    else
    {
        participant2.matchOutcome = GKTurnBasedMatchOutcomeWon;
        participant1.matchOutcome = GKTurnBasedMatchOutcomeLost;
    }
     
    NSData *matchData = [self encodeMatchData];
    [self.match endMatchInTurnWithMatchData:matchData completionHandler:
    ^(NSError *error)  
    {
        if (error)
        {
            NSLog(@"Error ending match: %@", error);
        }
        //
    }];
     
    [self enableSquareButtons:NO];
}

The corresponding change to the gameEndedInTie method:

- (void)gameEndedInTie
{
    NSString *message = @"Game ended in a tie!";
    UIAlertView *gameOverAlert = [[UIAlertView alloc] initWithTitle:@"Game Over"
        message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [gameOverAlert show];
     
    self.statusLabel.text = message;
    self.match.message = self.statusLabel.text;
    NSData *matchData = [self encodeMatchData];
    GKTurnBasedParticipant *participant1 = [self.match.participants objectAtIndex:0];
    GKTurnBasedParticipant *participant2 = [self.match.participants objectAtIndex:1];
    participant1.matchOutcome = GKTurnBasedMatchOutcomeTied;
    participant2.matchOutcome = GKTurnBasedMatchOutcomeTied;
    [self.match endMatchInTurnWithMatchData:matchData completionHandler:
    ^(NSError *error)
    {
        if (error)
        {
            NSLog(@”Error ending match: %@”, error);
        }
        //
    }];
    [self enableSquareButtons:NO];
}

If the opponent quits the game prematurely, which can be done from the matchmaking view controller, the app needs to respond to this and declare the local player as the winner. Add the following delegate method to deal with that scenario:

- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController playerQuitForMatch:(GKTurnBasedMatch *)match
{
    if ([self.match.matchID isEqualToString:match.matchID])
    {
        [self gameEndedWithWinner:[self localPlayerMark]];
    }
}

You’re nearly finished with this rather extensive recipe. The only thing that’s left is to handle the events triggered by the actions of the opponent.

Handling Turn-Based Events

To respond to the actions from the remote players, your app needs to assign a Turn-Based event handler. The first step to that, is to conform to the GKTurnBasedEventHandlerDelegate protocol. Go to ViewController.h and add it to the list of protocols:

//
//  ViewController.h
//  Testing Turn-Based Game
//
 
// . . .
 
@interface ViewController : UIViewController<GKTurnBasedMatchmakerViewControllerDelegate ,  
    GKTurnBasedEventHandlerDelegate>
 
// . . .
 
@end

A good time to assign the event handler, is right after the local player has been authenticated. Therefore, add the following custom setter for the localPlayer property:

- (void)setLocalPlayer:(GKLocalPlayer *)localPlayer
{
    _localPlayer = localPlayer;
    if (_localPlayer)
    {
        [GKTurnBasedEventHandler sharedTurnBasedEventHandler].delegate = self;
    }
    else
    {
        [GKTurnBasedEventHandler sharedTurnBasedEventHandler].delegate = nil;
    }
}

There are two turn-based events that you need to handle. The first is when the turn has returned to the local player. Thanks to the code we’ve written, the only thing you need to do is to assign the match property. This sets up the game with the new state, and with the local player’s turn to act:

- (void)handleTurnEventForMatch:(GKTurnBasedMatch *)match didBecomeActive:(BOOL)didBecomeActive
{
    self.match = match;
}

The second event is match ended remotely. What you need to do then is to load the match data using your decodeMatchData: helper method. This too, puts the game in a correct state:

- (void)handleMatchEnded:(GKTurnBasedMatch *)match
{
    if ([self.match.matchID isEqualToString:match.matchID])
    {
        [self.match loadMatchDataWithCompletionHandler:
         ^(NSData *matchData, NSError *error)
         {
             dispatch_async(dispatch_get_main_queue(),^{
                 if (matchData.length > 0)
                 {
                     [self decodeMatchData:matchData];
                 }
                 self.statusLabel.text = match.message;
             });
         }];
    }
}

You’re now done! To test this app you need two or more Game Center accounts. It’s recommended that you don’t use your own account when testing Game Center features, so be sure you register a couple of test accounts.

You also need two devices to run the app simultaneously and get the real multiplayer feeling. You can however test it in the iOS Simulator, but then you need to sign out the current player and sign in the opponent between the moves.

Figure 14-27 shows the turn-based matchmaking view controller and an ongoing multiplayer game of Tic Tac Toe.

9781430245995_Fig14-27.jpg

Figure 14-27.  A Game Center turn-based game of Tic Tac Toe

Summary

In this chapter, you’ve learned how to extend your game with Game Center and Game Kit. You can include high scores in your games to encourage competition among players and establish bragging rights. You also can implement achievements that give your players a feeling of accomplishment during long levels or even easily provide mini-games within a game. Finally, you implemented basic multiplayer functionality in the form of a turn-based game to encourage even more social game play against live opponents.

Developing iOS applications is a multifarious process: a combination of visual design and programmatic functionality that requires a versatile skillset, as well as significant dedication. Thankfully, Apple provides an excellent development toolset and programming language to work with, both of which are constantly updated and improved on. With such a flexible language, tasks ranging from organizing massive data stores, to complex web requests, to image filtering can be simplified, designed, and implemented for some of the most widely used and powerful devices of our generation. Whether you use this book as a simple reference or a full guide, we hope that you can use these recipes to build stronger applications to help improve and contribute to the world of iOS technology.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset