Challenges

Achievements and leaderboards both allow players to measure their progress playing your game. But it is also common for players to want to test their progress against each other. It’s more satisfying to beat a friend’s score than to merely achieve a decent score. Game Center embraces this idea in the form of challenges. Players on Game Center can challenge other Game Center members to beat earned scores or achievements. Challenges represent a different style of multiplayer experience where players compete against each other indirectly.

When a challenge is issued by one player to another, a push notification is sent to the challenged player. The challenged player can then accept or refuse the challenge. If the player accepts the challenge, the challenge is placed on a list of challenges associated with that player. Later, if the player beats the challenge, Game Center notifies both the challenged player and the challenger that the challenge is complete.

Game Center supports two kinds of challenges:

If your game supports achievements or leaderboards, it automatically supports challenges without requiring any additional code. In this case, players go to the Game Center app to challenge their friends. However, you can also customize your game to directly support challenges:

Checklist for Supporting Challenges

To add challenges to your game, you need to take the following steps:

Issuing Challenges from Your Game

You issue a challenge from within your game using either a GKScore object or a GKAchievement object. Both classes implement an issueChallengeToPlayers:message: method with a similar signature. Issuing a challenge does not display a user interface to the player issuing the challenge; this is code you need to implement yourself. The following section shows a series of methods you can use to issue an achievement challenge.

The design for score-based challenges is similar. Create a score object, submit the score, then use it to generate a challenge request.

Displaying the Challenge User Interface

Listing 6-1 begins the challenge process. This method is called when the player completes an achievement. The first thing the code does is create a new achievement object to tell Game Center that the achievement has been completed. If the player completes multiple achievements for a challenge at once, create a new object for each achievement and add the achievement to an array. Note that a challenge can only be performed on a completed achievement. It disables the achievement’s default completion banner because it plans to display a custom challenge screen instead. The method reports the progress to Game Center and then triggers a segue to display the game’s custom challenge user interface.

Listing 6-1  Displaying the challenge user interface

- (void) bossMonsterDefeated
{
    GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:@"MyGame.bossDefeated"];
    achievement.percentComplete = 100.0;
    achievement.showsCompletionBanner = NO;
 
    [achievement reportAchievements: [NSArray arrayWithObjects:achievement, nil] WithCompletionHandler:NULL];
    [self performSegueWithIdentifier:@"achievementChallenge" sender:achievement];
}

Displaying and Dismissing the Challenge View Controller

Listing 6-2 shows the issuing view controller’s prepareForSegue:sender: method. When the method detects that this is a segue to show the achievement user interface, it sets the issuing view controller as a delegate. The achievement object is also provided to the challenge view controller so that it can customize its user interface to match the earned achievement.

Listing 6-2  Preparing the challenge view controller

- (void )prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"achievementChallenge"])
    {
        MyAchievementChallengeViewController* challengeVC = (MyAchievementChallengeViewController*) segue.destinationViewController;
        challengeVC.delegate = self;
        challengeVC.achievement = (GKAchievement*) sender;
    }
}

Listing 6-3 shows the issuing controller’s implementation of the delegate method. When the user dismisses the challenge view controller, the delegate is called. The delegate method takes a parameter that states whether the player issued a challenge. If the player issued a challenge, then the method retrieves the list of players and the message from the challenge view controller and issues the challenge. Note that this design gives control over issuing the challenge to the original view controller. If your design does not need this additional flexibility, you could simply issue the challenge directly from the challenge view controller instead.

Listing 6-3  Dismissing the challenge view controller

- (void) challengeViewController:(MyAchievementChallengeViewController*)controller wasDismissedWithChallenge:(BOOL)issued
{
    [self dismissViewControllerAnimated:YES completion:NULL];
    if (issued)
    {
        [controller.achievement issueChallengeToPlayers:controller.players message:controller.message];
    }
}

Best Practices for Issuing Challenges

The local player must always have control over when and how a challenge is issued. Your game must never issue a challenge on the player’s behalf without the player first approving the challenge. When you implement your custom user interface for challenges, you must ensure that the player has the option to not issue a challenge. Even when a player issues a challenge, the player needs to be in control of who receives the challenge and what message is displayed to those players.

Ideally, your game should provide default behavior that matches a player’s expectations. The default message and list of players should be reasonable choices that a player might make. For example, when a player wants to issue a score challenge, your first impulse might be to include all of a player’s friends in the challenge. In practice, that may not be the best answer. Some of the player’s friends may have already earned much higher scores. Instead, consider something like the algorithm in Listing 6-4. It searches the leaderboard using a friends-only filter. The resulting friends list is then filtered so that only players who have not earned as high of a score are challenged.

Listing 6-4  Retrieving the list of players with lower scores than the one just earned

- (void) challengeLesserMortalsForScore: (int64_t) playerScore inCategory: (NSString*) category
{
    GKLeaderboard *query = [[GKLeaderboard alloc] init];
    query.category = category;
    query.playerScope = GKLeaderboardPlayerScopeFriendsOnly;
    query.range = NSMakeRange(1,100);
    [query loadScoresWithCompletionHandler:^(NSArray *scores, NSError *error) {
        NSPredicate *filter = [NSPredicate predicateWithFormat:@"value < %qi",playerScore];
        NSArray *lesserScores = [scores filteredArrayUsingPredicate:filter];
        [self presentChallengeWithPreselectedScores: lesserScores];
    }];
}

Your challenge view controller can then use the score data to populate its user interface. For example, it can use the player identifiers embedded in the score to load the name and photo of each challenged player. You can perform a similar operation for achievement challenges using the selectChallengeablePlayerIDs:withCompletionHandler: class method. Listing 6-5 shows a possible implementation of this.

Listing 6-5  Determining the list of players who can complete an achievement challenge

- (void) challengePlayersToCompleteAchievement: (GKAchievement*) achievement
{
    [achievement selectChallengeablePlayerIDs:[GKLocalPlayer localPlayer].friends withCompletionHandler:^(NSArray *challengeablePlayerIDs, NSError *error) {
        if (challengeablePlayerIDs)
        {
            [self presentChallengeWithPreselectedPlayerIDs: challengeablePlayerIDs];
        }
    }];
}

Receiving Information About Existing Challenges

A common design for a game that directly supports challenges is to offer a Challenges button on the title screen. When the button is tapped, your game presents a list of challenges issued to the local player. If a challenge is then selected from the list, your game automatically transitions to a screen that allows the player to work towards completing the selected challenge.

Challenges are represented on the system by GKChallenge objects. You can get the list of outstanding challenges by calling the loadReceivedChallengesWithCompletionHandler: class method. Listing 6-6 shows a simple implementation of this concept. This method loads the challenge objects and when that task completes, the challenges are passed to another method to populate the user interface for the presented content.

Listing 6-6  Retrieving the list of challenges

- (void) showChallengesList
{
    [GKChallenge loadReceivedChallengesWithCompletionHandler:^(NSArray *challenges, NSError *error) {
        if (challenges)
            [self presentChallengeList: challenges];
    }];
}

Table 6-1 lists the most common properties used to populate your user interface.

Table 6-1  Common challenge properties

Property

Description

issuingPlayerID

The player identifier for the issuing player.

receivingPlayerID

The player identifier for the player that received the challenge.

issueDate

The date the challenge was issued.

completionDate

The date the challenge was completed.

Each type of challenge is defined by a distinct subclass of GKChallenge. Table 6-2 lists the subclasses and how to retrieve the remaining information needed to display the challenge to the player.

Table 6-2  Challenge subclasses

Subclass

Description

GKScoreChallenge

Read the score property to obtain a score object representing the score used to generate the challenge.

GKAchievementChallenge

Read the achievement property to obtain an achievement object representing the achievement that completes the challenge.

Reacting to Challenge Events

Your game can directly connect to the notification system so that it is called directly when challenges issued to or by the local player are updated. By default Game Kit automatically displays a banner when the status of a challenge changes, but you can also suppress specific banners if you want to display a custom user interface instead.

You hook into the challenge notification system by installing a challenge event handler, which is defined by the GKChallengeEventHandlerDelegate protocol. After the local player is authenticated on the device, your app installs a challenge event handler and is then notified when challenges change state. Listing 6-7 shows the code to install a new event handler.

Listing 6-7  Installing a challenge event handler

- (void) installChallengeHandler
{
    GKChallengeEventHandler *eventHandler = [GKChallengeEventHandler challengeEventHandler];
    eventHandler.delegate = self;
}

Responding to Challenge Events

Game Kit displays banners to the local player under the following circumstances:

  • When the local player receives a challenge

  • When the local player completes a challenge

  • When a remote player completes a challenge issued by the local player

In each of these situations, you can customize the behavior using a pair of methods. The methods for overriding the behaviors are shown in Table 6-3. The first method in each pair allows you to choose whether a banner is displayed at all. The second method in each pair allows you to handle the event yourself.

Table 6-3  Kinds of challenge events

Event

Method to Control Banner Presentation

Method to Handle the Event

A local player receives a challenge

shouldShowBannerForLocallyReceivedChallenge:

localPlayerDidReceiveChallenge:

A local player completes a challenge

shouldShowBannerForLocallyCompletedChallenge:

localPlayerDidCompleteChallenge:

A remote player completes a challenge

shouldShowBannerForRemotelyCompletedChallenge:

remotePlayerDidCompleteChallenge:

For example, if you wanted to completely customize the appearance when the local player completes a challenge, first you would suppress the banner from being displayed, as shown in Listing 6-8:

Listing 6-8  Suppressing the challenge banner when the player completes a challenge

- (BOOL)shouldShowBannerForLocallyCompletedChallenge:(GKChallenge *)challenge
{
    return NO;
}

Then you implement a localPlayerDidCompleteChallenge: method to handle the challenge. Listing 6-9 shows a possible implementation of this method. First, it loads the player data for the player issuing the challenge. In the completion handler for this first call, it then loads that player’s photo. Finally, when the photo is loaded, it invokes its own method to display the loaded challenge data.

Listing 6-9  Displaying a custom user interface when the player completes a challenge

- (void)localPlayerDidCompleteChallenge:(GKChallenge *)challenge
{
    NSString *issuerID = challenge.issueingPlayerID;
    [GKPlayer loadPlayersForIdentifiers@[issuerID] withCompletionHandler:^(NSArray *players, NSError *error) {
        GKPlayer *player = [players lastObject];
        // Load the challenging player's photo.
        [player loadPhotoForSize: GKPhotoSizeNormal withCompletionHandler:^(UIImage *photo, NSError *error) {
            [self presentCompletedChallenge: challenge photo: photo name:player.displayName];
        }];
    }];
}

Responding When the Local Player Selects a Challenge

When a player receives a new challenge and the banner appears, the player may tap the challenge banner to immediately play the challenge. Similarly, the player can look up a challenge in the Game Center app and choose to launch your game to play the challenge. To be notified when the player wants to play a challenge, you implement the localPlayerDidSelectChallenge: method. If your game was launched to handle the challenge, the handler’s localPlayerDidSelectChallenge: method is called immediately after the authentication process completes.

You implement the localPlayerDidSelectChallenge: method similarly to the other handler methods. For example, you might use an implementation similar to that in Listing 6-9 to load player information and display an interface to the player.