Chapter     17

Game Kit Recipes

Game Center is an iOS service that increases the “replay factor” of your games. It provides a social hub-like interface into which your apps can integrate to allow a user to share scores, keep track of achievements, initiate turn-based matches, and even create multiplayer matches. The best part about Game Center is that it stores all this information on the Apple servers so you don’t have to store it. With iOS 7, the user interface has been completely redesigned to provide a much cleaner interface.

In this chapter, we first cover some of the basics of the GameKit framework, which is what you use to make your games “Game Center aware.” Next, we walk you through some practical examples, such as implementing leaderboards and achievements. Finally, we teach you how to create a simple, turn-based multiplayer game and integrate it with Game Center.

Recipe 17-1. Making Your App Game Center Aware

In this recipe, you create a simple game and connect it to Game Center. The game consists of four buttons. When the player taps a button, one of two things might happen: if the button is a “safe” button, the player’s score is increased by one and the player is allowed to continue the game; if the button is a “killer” button, the game ends. Because the player has no way of knowing 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.” We’ll keep the same project name for the next few recipes because the bundle identifier, which is a unique app identifier, will be tied to the Game Center.

The game has three difficulty levels:

  • Easy game: Only one of the four buttons is a “killer” button.
  • Normal game: Two of the four buttons are “killer” buttons.
  • Hard game: Three of the four buttons are “killer” buttons.

You will need to set up the main view of the app to be a menu page with options to start a game with one of those levels. You also need to use a navigation controller. To do so, select the view controller from the Main.storyboard file and choose Editor arrow.jpg Embed In arrow.jpg Navigation Controller from the Xcode file menu.

Next, add a label and three buttons to the root view controller from the object library and arrange everything to resemble the storyboard scene in Figure 17-1.

9781430259596_Fig17-01.jpg

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

Next, set the title of the main view controller to “Lucky,” and change the text of the Welcome label to “Welcome Anonymous Player.” Do this by selecting the navigation bar and changing the title in the attributes inspector, as shown in Figure 17-2.

9781430259596_Fig17-02.jpg

Figure 17-2. Changing the root view controller title

Build and run the app to make sure everything is working so far; you should see a screen resembling Figure 17-3.

9781430259596_Fig17-03.jpg

Figure 17-3. The Lucky game has three levels

Next, 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 selected. Because we will create a custom initializer in GameViewController, it makes sense to mix storyboards and .xib files in this instance for simplicity.

Select the GameViewController.xib file and create a user interface that resembles Figure 17-4 for the new view controller. Make sure to leave room for the navigation bar.

9781430259596_Fig17-04.jpg

Figure 17-4. The user interface of the Lucky game

Create the following outlets for the added user interface elements in the GameViewController:

  • scoreLabel
  • button1
  • button2
  • button3
  • button4

For the buttons, 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 17-5.

9781430259596_Fig17-05.jpg

Figure 17-5. Creating an action with a specific type parameter

Now connect the remaining buttons to the same action you created for Button 1 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 17-6, you can release the mouse button and the action will become connected to the button.

9781430259596_Fig17-06.jpg

Figure 17-6. 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 and a couple of private instance variables. You also need the view controller to conform to the UIAlertViewDelegate protocol. So, go to GameViewController.h and add the code in Listing 17-1.

Listing 17-1.  The Completed GameViewController.h File

//
//  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 implementation of the initializer method shown in Listing 17-2.

Listing 17-2.  Implementing the initWithLevel: initializer Method

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

Next, add the helper method shown in Listing 17-3 for updating the score label.

Listing 17-3.  Implementing the updateScoreLabel

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

Finally, add the code to the viewDidLoad method to set up the title and update the score label on game launch, as shown in Listing 17-4.

Listing 17-4.  Adding Code to the ViewDidLoad Method to Set the Level and Update the Score

- (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 import statement to the ViewController.h file, as shown in Listing 17-5.

Listing 17-5.  Importing the GameViewController.h File into 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 helper method shown in Listing 17-6 that will take a level argument and instantiate and display a game view controller.

Listing 17-6.  Implementing the playGameWithLevel: helper Method

- (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 shown in Listing 17-7.

Listing 17-7.  Implementing Methods to Start Each Game Type

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

9781430259596_Fig17-07.jpg

Figure 17-7. 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 line in Listing 17-8 to the viewDidLoad method.

Listing 17-8.  Adding a setupButtons Call 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 because 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, as shown in Listing 17-9.

Listing 17-9.  Implementing the setupButtons Method

- (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.” Listing 17-10 shows the implementation.

Listing 17-10.  The setupButtonsForEasyGame Method 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, as shown in Listing 17-11.

Listing 17-11.  Implementing the resetButtonTags Method

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

The method in Listing 7-11 also makes use of the buttonForIndex: helper method. So go ahead and add it, as shown in Listing 17-12.

Listing 17-12.  Implementing the buttonForIndex: Method

- (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 it selects two “killer” buttons instead of one; Listing 17-13 shows the implementation.

Listing 17-13.  Implementing the setupButtonsForNormalGame

- (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, implement the setup method for a Hard game, where all but one button are “killers,” as shown in Listing 17-14.

Listing 17-14.  Implementing the setupButtonsForHardGame

- (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 “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. Listing 17-15 shows the complete implementation.

Listing 17-15.  Implementing the gameButtonSelected: Action Method

- (IBAction)gameButtonSelected:(UIButton *)sender
{
    if (sender.tag == 0)
    {
        // Safe, continue game
        _score += 1;
        [self updateScoreLabel];
        [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 task 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 delegate method shown in Listing 17-16.

Listing 17-16.  Implementing the alterView:didDismissWithButtonIndex: 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 17-8 shows a finished game for an Easy level.

9781430259596_Fig17-08.jpg

Figure 17-8. 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. The reason you need to register the app with iTunes Connect is that Game Center needs to be aware of the app to test the functionality. Without registering, you will not be able to access the Game Center server, so nothing will work.

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 affecting production scores or achievements.

With iOS 7, you can easily enable Game Center if you have a developer account. With the root project selected, you can navigate to the Capabilities tab and turn on Game Center. When prompted for your team, select it and click Choose, as shown in Figure 17-9.

9781430259596_Fig17-09.jpg

Figure 17-9. Configuring GameKit capabilities

Now that you have enabled the GameKit capability and chosen your provisioning profile, go to http://itunesconnect.apple.com and log in using your developer ID. Click the Manage Your Applications link, and then click the “Add New App” button. Follow the instructions, and when you reach the App Information page, enter the information shown in Figure 17-10 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.

9781430259596_Fig17-10.jpg

Figure 17-10. Entering application information in iTunes Connect

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

  • 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 17-11.

9781430259596_Fig17-11.jpg

Figure 17-11. An app registered with iTunes Connect

Now, click the “Manage Game Center” button on the right side of the page. On the Enable Game Center page, click the “Enable for Single Game” button, as shown in Figure 17-12.

9781430259596_Fig17-12.jpg

Figure 17-12. Enabling Game Center for an app in iTunes Connect

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

Authenticating a 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 that hasn’t been done. Because we used the Capabilities tab to turn on Game Center, the info plist file and GameKit framework have already been set up for you. Now you can start jumping into some code.

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 the player can start receiving challenges and other Game Center notifications right away. Go to ViewController.h, import the GameKit API, and add the property shown in Listing 17-17 to keep track of the logged-in player.

Listing 17-17.  Adding a Property and Import Statement to the ViewController.h File

//
//  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 implement the method in Listing 17-18.

Listing 17-18.  Implementing the setPlayer: Setter 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 GameKit framework handles the authentication process for you. All you have to do is provide an authentication handler block to the localPlayer shared instance. Add a helper method, which assigns such a block, as shown in Listing 17-19.

Listing 17-19.  Implementing the authenticatePlayer Method

- (void)authenticatePlayer
{
    __weak 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, GameKit tries to authenticate the local player. The outcome can be one of three possible scenarios:

  • If the user is not signed into Game Center, the authentication handler will be invoked with a login view controller provided by GameKit. 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.
  • 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.
  • If the user is not signed in and 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, change the viewDidLoad method, as shown in Listing 17-20.

Listing 17-20.  Updating the viewDidLoad Method to Make the authenticatePlayer Method Call

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    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 17-13. Once a user logs in, the user can access any app in Game Center, so if you or a user has authenticated to Game Center in another app, this can be passed to your app without prompting you to log in again.

9781430259596_Fig17-13.jpg

Figure 17-13. 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 Main.storyboard file again and add a “Visit Game Center” button to the view controller so that the user interface resembles Figure 17-14.

9781430259596_Fig17-14.jpg

Figure 17-14. 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, as shown in Listing 17-21.

Listing 17-21.  Adding the GKGameCenterControllerDelegate to the ViewController.h File

//
//  ViewController.h
//  Recipe 17-1 Making Your App Game Center Aware
//

#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 implementation to the showGameCenter: action method shown in Listing 17-22.

Listing 17-22.  Implementing 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 delegate method to dismiss the Game Center view controller when the user is finished with it, as shown in Listing 17-23.

Listing 17-23.  Implementing the gameCenterViewControllerDidFinish: Delegate Method

- (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 17-2. Implementing Leaderboards

Competing against others is an essential ingredient in gaming. The possibility for players to compare high scores 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 17-1 and implement leaderboard support. Leaderboards allow users to post the score to Game Center, which will in turn list the top-scoring players.

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 http://itunesconnect.apple.com and click the Manage Your Applications link. Select the Lucky app you registered in Recipe 17-1, and then click the “Manage Game Center” button.

In the Leaderboards section, click the Add Leaderboard button. In the Add Leaderboard page (see Figure 17-15), choose to create a single leaderboard.

9781430259596_Fig17-15.jpg

Figure 17-15. 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 will use a simple integer, but you also can use time-based, floats, and currency. Select the sort order for your leaderboard and the score submission type. If you want high scores at the top (typical), then sort High to Low; if you want low scores at the top (for example, 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 17-16 shows the configurations you’ll be using for this leaderboard.

9781430259596_Fig17-16.jpg

Figure 17-16. Configuring a leaderboard for Lucky’s 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 17-17.

9781430259596_Fig17-17.jpg

Figure 17-17. 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 helper method shown in Listing 17-24. The method creates and initiates a new GKScore object, which it then reports to Game Center, providing a completion handler.

Listing 17-24.  Implementing the reportScore:forLeaderboard: Helper Method

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

Note   If a score cannot be reported because of connection problems, GameKit 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 code shown in bold in Listing 17-25 to the alertView:didDismissWithButtonIndex: delegate method.

Listing 17-25.  Adding Functionality to the Report Score

- (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. Listing 17-26 shows this implementation.

Listing 17-26.  Implementing the leaderboardID Method

- (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 and play a game. 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 17-18.

9781430259596_Fig17-18.jpg

Figure 17-18. A Game Center leaderboard with one score

Recipe 17-3. Implementing Achievements

Achievements in games are similar to badges and other accomplishable goals in most 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, such as beating a race course record in a racing game. However, to show you how they work, you’ll set up one for the Lucky game project you built in Recipes 17-1 and 17-2.

Specifically, you reward the player with an achievement if 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 http://itunesconnect.apple.com. Click Manage Your Applications, click the app you have set up for Game Center, and then click 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 17-19. Also, set the Hidden and Achievable More Than Once options to No. In your own applications, you can set Hidden to Yes to surprise the users with achievements they didn’t know existed. For this app, though, we’ll make the user aware of all possible achievements.

9781430259596_Fig17-19.jpg

Figure 17-19. 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 the “Add Language” button. The resulting view should resemble Figure 17-20 (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.

9781430259596_Fig17-20.jpg

Figure 17-20. 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 pre-made 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 17-21.

9781430259596_Fig17-21.jpg

Figure 17-21. Three achievements defined in iTunes Connect

You are now finished configuring the achievements. You will take advantage of the achievement configurations 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 adding an instance variable, as shown in Listing 17-27.

Listing 17-27.  Adding an NSMutableArray Instance Method to the GameVewController.h File

//
//  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, as shown in Listing 17-28.

Listing 17-28.  Instantiating the Buttons 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 code shown in bold in Listing 17-29. 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.

Listing 17-29.  Modifying the gameButtonSelected: Method to Report the Achievement

- (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
        // ...
    }
}

Reporting an achievement is similar to the way you report leaderboard scores, except you use the class GKAchievement instead of GKScore. Listing 17-30 shows the implementation of the reportAllFourButtonsAchievementCompleted helper method.

Listing 17-30.  Implementing the reportAllFourButtonsAchievementCompleted Method

- (void)reportAllFourButtonsAchievementCompleted
{
    NSString *achievementID = [self achievementID];
    GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:achievementID];
    if (achievement != nil)
    {
        achievement.percentComplete = 100;
        achievement.showsCompletionBanner = NO;
        NSArray *achievementArray = [[NSArray alloc] initWithObjects:achievement, nil];
        
        [GKAchievement reportAchievements:achievementArray withCompletionHandler:^(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 submilestones, 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, as shown in Listing 17-31.

Listing 17-31.  Implementing the achievementID Method

- (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 17-22 shows the Game Center view controller displaying a player who has been awarded all three available achievements.

9781430259596_Fig17-22.jpg

Figure 17-22. A player who has completed three achievements in the game

With the current implementation, an achievement will be reported even though the player has previously completed it. Let’s fix that. What you’ll do is cache the achievements of the current player so that you can check whether an achievement has been completed previously before reporting it to Game Center. That way, you don’t make any unnecessary server calls.

Start by adding an NSMutableDictionary property to hold the achievements. Go to ViewController.h and add the declaration shown in Listing 17-32.

Listing 17-32.  Adding the MSMutableDictionary for Achievements to the ViewController.h File

//
//  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 bold line shown in Listing 17-33 to the viewDidLoad method.

Listing 17-33.  Initializing the Achievements Array in the viewDidLoad Method

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.achievements = [[NSMutableDictionary alloc] init];
    self.player = nil;
    [self authenticatePlayer];
}

In the setter method of the player property, add the bold lines in Listing 17-34 to initiate the achievements dictionary. This basically removes all achievements when setting a new player and loads the achievements for those players using the loadAchievements helper method, which we’ll implement shortly.

Listing 17-34.  Modifying the setPlayer Setter Method to Load Achievements

- (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, as shown in Listing 17-35.

Listing 17-35.  Implementing the loadAchievements Method

- (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);
         }
     }];
}

Next, update the playGameWithLevel: method to include the achievements in the initializer, as shown in Listing 17-36.

Listing 17-36.  Updating the playGameWithLevel: Method to Include Achievements

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

Now initiate the game view controller with the achievements dictionary. First, go to GameViewController.h and make the changes shown in Listing 17-37.

Listing 17-37.  Modifying the GameViewController.h File to Accommodate Achievements Caching

//
//  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 to the initWithLevel: method, as shown in Listing 17-38.

Listing 17-38.  Modifying the initWithLevel: Method to Add an Achievements Array

- (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, as shown in Listing 17-39.

Listing 17-39.  Implementing the getAchievement Method

- (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, as shown in Listing 17-40.

Listing 17-40.  Modifying the reportAllFourButtonsAchievementCompleted Method to Account for Cached Achievements

- (void)reportAllFourButtonsAchievementCompleted
{
    GKAchievement *achievement = [self getAchievement];
    if (achievement != nil && !achievement.completed)
    {
        achievement.percentComplete = 100;
        achievement.showsCompletionBanner = NO;
        NSArray *achievementArray = [[NSArray alloc] initWithObjects:achievement, nil];
        
        [GKAchievement reportAchievements:achievementArray withCompletionHandler:^(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 17-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 gives a new dimension to the gaming experience. Game Kit has great support for multiplayer games, both real-time and turn-based. This means two players can play a game and take turns on two different devices with nearly the same speed as if they were playing together on one device.

In this chapter, you’ll build a simple turn-based tic-tac-toe multiplayer game that uses Game Center for matchmaking and the low-level network implementation. The Game Center matchmaking tools are helpful because they allow you to invite players to a game without writing a lot of extra code.

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 building the basic user interface of the game. Open the Main.storyboard file to start editing the interface. Add a label and nine buttons with backgrounds to the view so it resembles Figure 17-23. For this example, we used a light gray background color. Also, add one more button in the upper-right corner to initiate gameplay.

9781430259596_Fig17-23.jpg

Figure 17-23. A simple user interface for a tic-tac-toe game

Create outlets with the following names for the respective elements:

  • 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. Create another action for the play button called playGame. Next, go to ViewController.h and add the private instance variable shown in Listing 17-41.

Listing 17-41.  The Complete ViewController.h File

//
//  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;
- (IBAction)playGame:(id)sender;

@end

Because it’s not an essential part of this recipe, we skip the details of the basic game implementation and instead ask that 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, make 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 17-24 shows an example of a game that’s halfway complete.

9781430259596_Fig17-24.jpg

Figure 17-24. 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 from earlier in this chapter, that starts with registering your app.

Preparing the Game for Game Center

First, you need to enable Game Center from the Capabilities tab and select a provisioning profile as we did in Recipe 17-1.

After you have enabled Game Center, the next step is to register your app with iTunes Connect. Again, refer to Recipe 17-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 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.

After you’ve registered the app with iTunes Connect, you can start preparing it for Game Center. Go back to Xcode and link the GameKit framework to your project. Then open ViewController.h and add the property declaration to hold a reference to the local player. Your ViewController.h heading file should now look like Listing 17-42.

Listing 17-42.  The Complete ViewController.h File

//
//  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 17-1. Go to ViewController.m and add the authenticateLocalPlayer helper method shown in Listing 17-43.

Listing 17-43.  Implementing the authenticateLocalPlayer Method

- (void)authenticateLocalPlayer
{
    __weak 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 17-1, try to authenticate the local player directly on app launch. Add the bold line in Listing 17-44 to the viewDidLoad method.

Listing 17-44.  Adding an authenticateLocalPlayer Method Call to the viewDidLoad Method

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self enableSquareButtons:NO];
    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. Add four labels and arrange them so that the view controller user interface resembles Figure 17-25. Note that “Playing X:” and “<Player 1 Label>” as well as “Playing O:” and “<Player 2 Label>” are separate labels.

9781430259596_Fig17-25.jpg

Figure 17-25. 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 implement the next step, which is the matchmaking feature.

Implementing Matchmaking

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

Specifically, you will use the GKTurnBasedMatchmakerViewController to handle the matchmaking. 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 bold lines shown in Listing 17-45.

Listing 17-45.  Updating ViewController.h to Include New Properties

//
//  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 code shown in Listing 17-46. This code checks 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.

Listing 17-46.  Replacing the Code in the playGame: Action Method

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

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 box, in which case you should simply dismiss the matchmaker view controller. This is shown in Listing 17-47.

Listing 17-47.  Implementing the turnBasedMatchmakerViewControllerWasCancelled: Delegate Method

- (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, as shown in Listing 17-48.

Listing 17-48.  Implementing the turnBasedMatchmakerViewController:didFailWithError: Method

- (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, simply store a reference to the match object, as shown in Listing 17-49.

Listing 17-49.  Implementing the turnBasedMatchmakerViewController:didFindMatch: Delegate Method

- (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, as shown in Listing 17-50. It loads the participating players and the match data using two currently nonexistent helper methods that you’ll implement soon.

Listing 17-50.  Implementing the setMatch Custom Setter Method

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

Let’s start with the loadPlayers method. Basically, you need 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, as shown in Listing 17-51.

Listing 17-51.  Implementing the partial loadPlayers Helper Method

- (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 might 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 bold code in Listing 17-52 to the loadPlayers method to handle that.

Listing 17-52.  Adding Code to Account for Missing Players

- (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. Listing 17-53 shows the entire loadPlayers method with recent changes in bold.

Listing 17-53.  The Complete loadPlayers Method Implementation

- (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 custom setter methods shown in Listing 17-54.

Listing 17-54.  Implementing Custom Setter Methods for player1 and player2

- (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 to set the player1 and player2 properties to nil in viewDidLoad, as shown in Listing 17-55.

Listing 17-55.  Setting the Players to nil to Initialize Them in the viewDidLoad Method

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self enableSquareButtons:NO];

    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 how to choose to encode and decode the data you need to store the state of the game in Game Center. The only restrictions are that it is of type NSData and that you keep the size of the data within 64 KB. For the sake of this recipe, keep it simple and store the current state in a simple array, which you then transform into an NSData object using the NSKeyedArchiver class. Listing 17-56 shows the implementation.

Listing 17-56.  Implementing the encodeMatchData Method

- (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, as shown in Listing 17-57.

Listing 17-57.  Implementing the decodeMatchData Method

- (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 shown in Listing 17-58 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.

Listing 17-58.  Implementing the loadMatchData Method

- (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 code in Listing 17-58 makes use of three helper methods that you’ve not yet defined. The localPlayerIsCurrentPlayer method shown in Listing 17-59 checks the currentParticipant property of the match object and compares it with the identity of the local player.

Listing 17-59.  Implementing the localPlayerIsCurrentPlayer Method

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

The localPlayerMark method shown in Listing 17-60 returns X or O depending on whether the local player is player 1 or player 2.

Listing 17-60.  Implementing the localPlayerMark Method

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

The opponentMark shown in Listing 17-61 simply returns the opposite of localPlayerMark.

Listing 17-61.  Implementing the opponentMark Method

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

However, before we update 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 her moves remotely from another instance of the app). So, make the change shown in Listing 17-62 to the existing code.

Listing 17-62.  Updating the selectbutton and Replacing the _currentMark Instance

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

Make sure you also remove _currentMark from both the gameEndedWithWinner and gameEndedInTie methods.

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. Listing 17-63 shows the new code doing this.

Listing 17-63.  Replacing the Code in the advanceTurn Method

- (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 code in Listing 17-63 hands over the turn using a helper method to identify the opponent’s participant object. Listing 17-64 shows the implementation for that method.

Listing 17-64.  Implementing the opponentParticipant 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 changes shown in Listing 17-65 to the gameEndedWithWinner: method.

Listing 17-65.  Setting the matchOutcome Property

- (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 is shown in Listing 17-66.

Listing 17-66.  Updating 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 delegate method to deal with that scenario, as shown in Listing 17-67.

Listing 17-67.  Implementing the turnBasedMatchmakerViewController:playerQuitForMatch: Delegate Method

- (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 task 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 is to conform to the GKLocalPlayerListener protocol. Go to ViewController.h and add it to the list of protocols, as shown in Listing 17-68.

Listing 17-68.  Declaring the GKLocalPlayerListener Protocol in the ViewController.h File

//
//  ViewController.h
//  Testing Turn-Based Game
//

// ...

@interface ViewController : UIViewController<GKTurnBasedMatchmakerViewControllerDelegate ,
    GKLocalPlayerListener>

// ...

@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, as shown in Listing 17-69.

Listing 17-69.  Creating a custom localPlayer property setter

- (void)setLocalPlayer:(GKLocalPlayer *)localPlayer
{
    _localPlayer = localPlayer;
    if (_localPlayer)
    {
[[GKLocalPlayer localPlayer] registerListener:self];
    }
    else
    {
[[GKLocalPlayer localPlayer] unregisterListener:self];
    }
}

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 assign the match property. This sets up the game with the new state and with the local player’s turn to act. Implement this delegate method, as shown in Listing 17-70.

Listing 17-70.  Implementing the player:receivedTurnEventForMatch:didBecomeActive: Method

-(void)player:(GKPlayer *)player receivedTurnEventForMatch:(GKTurnBasedMatch *)match didBecomeActive:(BOOL)didBecomeActive
{
    
    self.match = match;

}

The second event is when a match has ended remotely. In this case, you need to load the match data using your decodeMatchData: helper method. This, too, puts the game in a correct state, as shown in Listing 17-71.

Listing 17-71.  Implementing the handleMatchEnded: Delegate Method

- (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 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 to 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 17-26 shows the turn-based matchmaking view controller and an ongoing multiplayer game of Tic Tac Toe. If you are using the simulator, you might have issues receiving turns if your firewall is enabled.

9781430259596_Fig17-26.jpg

Figure 17-26. A Game Center turn-based game of Tic Tac Toe

Note   At the time of this writing, the Game Center sandbox would not consistently notify the app that the other player had taken a turn. To fix this problem, you can create a button, which calls the loadMatchData method we already created. This will pull the match data again, and should update the current game state.

(IBAction)reloadMatch:(id)sender
{
    [self loadMatchData];
}

Although this recipe was quite long, there is still more to learn with multiplayer, turn-based games. You also have tools available to you that will allow more than two players and even allow you to take turns out of sequence. For more information on GameKit, see the Game Center programming guide at https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/GameKit_Guide/Introduction/Introduction.html.

Summary

In this chapter, you’ve learned how to extend your game with Game Center and GameKit. 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 gameplay against live opponents.

Developing iOS applications is a multifarious process, a combination of visual design and programmatic functionality that requires a versatile skill set as well as significant dedication. Thankfully, Apple provides an excellent development tool set and programming language to work with, both of which are constantly updated and improved upon. 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 you will 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