iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)

Chapter 4. Achievements

Like leaderboards, achievements are quickly becoming a vital component of modern gaming. Although the history of achievements isn’t as well cemented into gaming history as that of leaderboards, today it could be argued that they are even more important to the social success of a game.

An achievement is, in short, an unlockable accomplishment that, although not necessary to the completion of a game, tracks the user’s competition of additional aspects of the game. Commonly, achievements are issued for additional side tasks or extended play, such as beating a game on hard difficulty, exploring areas, or collecting items. One of the primary features of Game Center is the achievement system, which makes adding your own achievements to your game much simpler than it was previously.

Unlike most other chapters, this chapter shares the sample app, Whack-a-Cac, with Chapter 3, “Leaderboards.” Although it is not necessary to complete that chapter before beginning this one, there are several instances of overlap. For the sake of the environment and trees, that information will not be reprinted here; it is recommended that you read the “Whack-a-Cac,” “iTunes Connect,” “Game Center Manager,” and “Authenticating” sections of Chapter 3 before proceeding with this chapter. These sections provide the background required to interact with the sample app as well as the basic task of setting up Game Center and authenticating the local user. This chapter continues to expand on the already existing Game Center Manager sample code provided to you in Chapter 3.

iTunes Connect

Before beginning to write code within Xcode for achievements, first visit iTunes Connect (http://itunesconnect.apple.com) to set up the achievements. For an introduction to working with Game Center in iTunes Connect, see the “iTunes Connect” section of Chapter 3.

When entering the Manage Game Center section of iTunes Connect, there are two configuration options: one to set up leaderboards, and the other to set up achievements. In the sample app, Whack-a-Cac, there will be six achievements.

To create a new achievement, click the Add Achievement button, as shown in Figure 4.1.

Image

Figure 4.1 A view of the Achievements section before any achievements have been added in iTunes Connect.

Similar to the leaderboards from Chapter 3, several fields are required in order to set up a new achievement, as shown in Figure 4.2. The Achievement Reference Name is simply a reference to your achievement in iTunes Connect; it is not seen anywhere outside of the Web portal. Achievement ID, on the other hand, is what will be referenced from the code in order to interact with the achievement. Apple recommends the use of a reverse DNS type of system for the Achievement ID, such as com.dragonforged.whackacac.100whacks.

Image

Figure 4.2 Adding a new achievement in iTunes Connect.

The Point Value attribute is unique to achievements in Game Center. Achievements can have an assigned point value from 1 to 100. Each app can have a maximum of 1,000 achievement points. Points can be used to denote difficulty or value of an achievement. The achievement points are not required, nor are they required to add up to exactly 1,000.

Also unique to achievements is the Hidden property, which keeps the achievement hidden from the user until it has been achieved or the user has made any progress toward achieving it. The Achievable More Than Once setting is new to iOS 6; it allows the user to accept Game Center challenges based on achievements that they have previously earned.

As with leaderboards, at least one localized description needs to be set up for each achievement. This information consists of four attributes. The title will appear above the achievement description. Each achievement will have two descriptions, one that will be displayed before the user unlocks it and one that is displayed after it has been completed. Additionally, an image will need to be supplied for each achievement; the image must be at least 512×512 in size. To see how this information is laid out, see Figure 4.3.

Image

Figure 4.3 The new combined Game Center View Controller, launching to the Achievement section.


Note

As with leaderboards, after an achievement has gone live in a shipping app, it cannot be removed.


See the section “Adding Achievements into Whack-a-Cac” for a walk-through of adding several achievements into the sample game.

Displaying Achievement Progress

Without being able to display the current progress of achievements to the user, they are next to useless. If required to present a custom interface for achievements, refer to the section “Going Further with Achievements.” The following method launches the combined Game Center View Controller that is new in iOS 6, as shown in Figure 4.3:

- (void)showAchievements
{
    [[GKGameCenterViewController sharedController] setDelegate:self];

    [[GKGameCenterViewController sharedController] setViewState:GKGameCenterViewControllerStateAchievements];

    [self presentViewController:[GKGameCenterViewController sharedController] animated:YES completion: nil];
}

In the preceding method, used to launch the new iOS 6 Game Center View Controller, you will notice that a delegate was set. There is also a new required delegate method to be used with this view controller, and it is implemented in the following fashion:

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

It might be necessary to support devices that are not yet running iOS 6. In that instance, you need to implement a different method of displaying achievements. When you use the older version of displaying achievements on devices running iOS 6, it will present a view controller identical to the iOS 6 version, as shown in Figure 4.3. The sample game uses the pre-iOS 6 type of implementation.

A new instance of GKAchievementViewController is allocated and initialized, followed by a delegate being set. The view is presented the same as any modal view controller and the memory is then cleaned up.

- (IBAction)achievements:(id)sender
{
    GKAchievementViewController *achievementViewController = [[GKAchievementViewController alloc] init];

    if(achievementViewController == nil)
    {
        NSLog(@"Unable to create achievement view controller");
        return;
    }

    achievementViewController.achievementDelegate = self;

    [self presentViewController:achievementViewController animated:YES completion: nil];

    [achievementViewController release];
}

There is also a required delegate method that is attached to using this approach. The following method must be implemented. Don’t forget to declare that the class conforms to the GKAchievementViewControllerDelegate protocol.

- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController
{
    [self dismissModalViewControllerAnimated: YES completion: nil];
}

Game Center Manager and Authentication

In Chapter 3, a new reusable class called Game Center Manager was introduced. This class provides the groundwork for quickly implementing Game Center into your apps. That information will not be reprinted in this chapter. Reference the sections “Game Center Manager” and “Authenticating” in Chapter 3 before continuing with the material in this chapter.

The Achievement Cache

When a score is being submitted to Game Center, it’s simply a matter of sending the score in and having Game Center determine its value compared to previously submitted scores. However, achievements are a bit tricky; all achievements have a percentage complete value, and users can work toward completing an achievement over many days or even months. It is important to make sure that users don’t lose the progress they have already earned and that they continue to make steady progress toward their goals. This is solved by using an achievement cache to store all of the cloud achievement data locally and refresh it with each new session.

A new convenience method will need to be added to the ICFGameCenterManager class, as well as a new classwide NSMutableDictionary, which will be called achievementDictionary. The first thing that is done in this method is to post an alert if it is being run after an achievement cache has already been established. Although you should not attempt to populate the cache more than once, it will not cause anything to stop functioning as expected. If the achievementDictionary does not yet exist, a new NSMutableDictionary will need to be allocated and initialized.

After the achievementDictionary is created, it can be populated with the data from the Game Center servers. This is accomplished by calling the loadAchievementsWithCompletionHandler class method on GKAchievement. The resulting block will return the user’s progress for all achievements. If no errors are returned an array of GKAchievements will be returned. These are then inserted into the achievementDictionary using the achievement identifier as the key.


Note

The loadAchievementsWithCompletionHandler method does not return GKAchievement objects for achievements that do not have progress stored on them yet.


-(void)populateAchievementCache
{
    if(achievementDictionary != nil)
    {
        NSLog(@"Repopulating achievement cache: %@",
        achievementDictionary);
    }

    else
    {
        achievementDictionary = [[NSMutableDictionary alloc] init];
    }

    [GKAchievement loadAchievementsWithCompletionHandler:^(NSArray *achievements, NSError *error)
    {

         if(error != nil)
         {
             NSLog(@"An error occurred while populating the achievement cache: %@", [error localizedDescription]);
         }

         else
         {
             for(GKAchievement *achievement in achievements)
             {
                 [achievementDictionary setObject:achievement forKey:[achievement identifier]];
             }
         }
     }];
}


Note

You cannot make any Game Center calls until a local user has been successfully authenticated.


Reporting Achievements

After an achievement cache has been implemented, it is safe to submit progress on an achievement. A new method reportAchievement:withPercentageComplete: will need to be added to the ICFGameManager class to accomplish this task. When this method is called, the achievement ID and percentage complete are used as arguments. For information on how to determine the current percentage of an achievement, see the section “Adding Achievement Hooks.”

When submitting achievement progress, the first thing that needs to be done is make sure that the achievementDictionary has already been populated. Making sure to check Game Center for the current progress of any achievements prevents users from losing progress when switching devices or reinstalling the app. In this example we fail out with a log message if achievementDictionary is nil; however, a more complex system of initializing and populating the achievement cache and retrying can be implemented as well.

After it has been determined that the achievementDictionary is initialized, a new GKAchievement object is created. When created, the GKAchievement object is stored in the achievementDictionary using the achievement identifier. If this achievement has not yet been progressed, it will not appear in the achievementDictionary and the achievement object will be nil. In this case a new instance of GKAchievement is allocated and initialized using the achievement identifier.

If the achievement object is non-nil, it can be assumed that the achievement has previously had some sort of progress made. A safety check is performed to make sure that the percentage complete that is being submitted isn’t lower than the percentage complete that was found on Game Center. Additionally, a check is performed to make sure that the achievement isn’t already fully completed. In either case an NSLog is printed to the console and the method returns.

At this point, a valid GKAchievement object has been created or retrieved and the percentage complete is greater than the one that is in the cache. A call on the achievement object with setPercentComplete: is used to update the percentage-complete value. At this point the achievement object is also stored back into the achievementDictionary so that the local achievement cache is up-to-date.

To report the actual achievement value to Game Center, reportAchievementWithCompletionHandler: is called on the achievement object. When it is finished checking for errors, a new delegate callback is used to inform the GameCenterManager Delegate of the success or failure.

-(void)reportAchievement:(NSString *)identifier
 withPercentageComplete:(double)percentComplete
{
    if(achievementDictionary == nil)
    {
            NSLog(@"An achievement cache must be populated before submitting achievement progress");

            return;
    }

    GKAchievement *achievement = [achievementDictionary objectForKey: identifier];

    if(achievement == nil)
    {
        achievement = [[[GKAchievement alloc] initWithIdentifier: identifier] autorelease];

        [achievement setPercentComplete: percentComplete];

        [achievementDictionary setObject: achievement forKey:identifier];
    }

    else
    {
        if([achievement percentComplete] >= 100.0 || [achievement percentComplete] >= percentComplete)
        {
            NSLog(@"Attempting to update achievement %@ which is either already completed or is decreasing percentage complete (%f)", identifier, percentComplete);

           return;
        }

            [achievement setPercentComplete: percentComplete];

            [achievementDictionary setObject: achievement forKey:identifier];
    }

    [achievement reportAchievementWithCompletionHandler:^(NSError *error)
    {
            if(error != nil)
            {
                NSLog(@"There was an error submitting achievement %@:%@", identifier, [error localizedDescription]);
            }

            [self callDelegateOnMainThread: @selector (gameCenterAchievementReported:) withArg: NULL error:error];
    }];
}


Note

Achievements like leaderboards in Chapter 3 will not attempt to resubmit if they fail to successfully reach the Game Center servers. The developer is solely responsible for catching errors and resubmitting achievements later. However, unlike with leaderboards, there is no stored date property so it isn’t necessary to store the actual GKAchievement object.


Adding Achievement Hooks

Arguably the most difficult aspect of incorporating achievements into your iOS app is hooking them into the workflow. For example, the player might earn an achievement after collecting 100 coins in a role-playing game. Every time a coin is collected, the app will need to update the achievement value.

A more difficult example is an achievement such as play one match a week for six months. This requires a number of hooks and checks to make sure that the user is meeting the requirements of the achievement. Although attempting to document every single possible hook is a bit ambitious, the section “Adding Achievements into Whack-a-Cac” has several types of hooks that will be demonstrated.

Before an achievement can be progressed, first your app must determine its current progress. Since Game Center achievements don’t take progressive arguments (for example, add 1% completion to existing completion), this legwork is left up to the developer. Following is a convenience for quickly getting back a GKAchievement object for any identifier. After a GKAchievement object has been returned, a query to percentageComplete can be made to determine the current progress.

-(GKAchievement *)achievementForIdentifier:(NSString *)identifier
{
    GKAchievement *achievement = nil;

    achievement = [achievementDictionary objectForKey:identifier];

    if(achievement == nil)
    {
        achievement = [[[GKAchievement alloc] initWithIdentifier:identifier] autorelease];

        [achievementDictionary setObject: achievement forKey:identifier];
    }

    return achievement;
}

If the achievement requires more precision than 1%, the true completion value cannot be retrieved from Game Center. Game Center will return and accept only whole numbers for percentage complete. In this case you have two possible options. The easy path is to round off to the nearest percentage. A slightly more difficult approach would be to store the true value locally and use that to calculate the percentage. Keep in mind that a player might be using more than one device, and storing a true achievement progress locally can be problematic in these cases; refer to Chapter 8, “Getting Started with iCloud,” for additional solutions.

Completion Banners

In iOS 5.0, Apple added the capability to use an automatic message to let the user know that an achievement has been successfully earned. If supporting an iOS version that predates iOS 5, you will need to implement a custom system. There is no functional requirement to inform users that they have completed an achievement beyond providing a good user experience.

To automatically show achievement completion, set the showsCompletionBanner property to YES before submitting the achievement to Game Center, as shown in Figure 4.4. A good place to add this line of code is in the reportAchievement: withPercentageComplete:method.

achievement.showsCompletionBanner = YES;

Image

Figure 4.4 A Game Center automatic achievement completion banner shown on an iPad with the achievement title and earned description.

Achievement Challenges

iOS 6 brought a new feature to Game Center called Challenges, in which a user can challenge a Game Center friend to beat her score or match her achievements, as shown in Figure 4.5.

Image

Figure 4.5 Challenging a friend to complete an achievement that is still being progressed.


Note

Game Center allows the user to challenge a friend to beat an unearned achievement as long as it is visible to the user.


- (void)showChallenges
{
    [[GKGameCenterViewController sharedController]setDelegate:self];

     [[GKGameCenterViewController sharedController] setViewState: GKGameCenterViewControllerStateAchievements];

    [self presentViewController:[GKGameCenterViewController sharedController] animated:YES completion: nil];
}

The gameCenterViewControllerDidFinish delegate method will also need to be implemented if that was not already done for the previous examples in this chapter.

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


Note

If users need to be able to accept achievement challenges for achievements that they have already earned, you will need to select the Achievable More Than Once option when creating the achievement in iTunes Connect.


A challenge can also be created pragmatically using the following approach:

[(GKAchievement *)achievement issueChallengeToPlayers: (NSArray *)players message:@"I earned this achievement, can you?"];

If it is required to get a list of users that can receive an achievement challenge for a particular achievement (if you do not have the Achievable More Than Once property set to on), use the following snippet to get a list of those users:

[achievement selectChallengeablePlayerIDs:arrayOfPlayersToCheck withCompletionHandler:^(NSArray *challengeablePlayerIDs, NSError *error)
{
      if(error != nil)
      {
            NSLog(@"An error occurred while retrieving a list of challengeable players: %@", [error localizedDescription]);
      }

      NSLog(@"The following players can be challenged: %@", challengeablePlayerIDs);
}];

It is also possible to retrieve an array of all pending GKChallenges for the authenticated user by implementing the following code:

[GKChallenge loadReceivedChallengesWithCompletionHandler:^(NSArray *challenges, NSError *error)
{
      if (error != nil)
      {
            NSLog(@"An error occurred: %@", [error localizedDescription]);
      }

      else
      {
            NSLog(@"Challenges: %@", challenges);
      }

}];

Challenges have states associated with them that can be queried on the current state of the challenge. The SDK provides the states invalid, pending, completed, and declined.

if(challenge.state == GKChallengeStateCompleted)
      NSLog(@"Challenge Completed");

Finally, it is possible to decline an incoming challenge by simply calling decline on it, as shown here:

[challenge decline];

Challenges are an exciting new feature of iOS 6. By leveraging them and encouraging users to challenge their friends, you will increase the retention rates and play times of the game. If using the built-in GUI for Game Center, you don’t even have to write any additional code to support challenges.


Note

Whack-a-Cac does not contain sample code for creating programmatic achievement challenges.


Adding Achievements into Whack-a-Cac

Whack-a-Cac will be using six different achievements using various hook methods. The achievements that will be implemented are listed and described in Table 4.1.

Image

Table 4.1 Achievements Used in Whack-a-Cac with Details on Required Objectives to Earn

Assuming that all the ICFGameCenterManager changes detailed earlier in this chapter have been implemented already, you can begin adding in the hooks for the achievements. You will need to add the delegate callback method for achievements into ICFGameViewController. This will allow the delegate to receive success messages as well as any errors that are encountered.

-(void)gameCenterAchievementReported:(NSError *)error;
{
    if(error != nil)
    {
        NSLog(@"An error occurred trying to report an achievement to Game Center: %@", [error localizedDescription]);
    }

    else
    {
        NSLog(@"Achievement successfully updated");
    }
}


Note

In Whack-a-Cac the populateAchievementCache method is called as soon as the local user is successfully authenticated.


Earned or Unearned Achievements

The easiest achievement from Table 4.1 to implement is the com.dragonforged.whackacac.killone achievement. Whenever you’re working with adding a hook for an achievement, the first step is to retrieve a copy of the GKAchievement that will be incremented. Use the method discussed in the section “Adding Achievement Hooks” to grab an up-to-date copy of the achievement.

GKAchievement *killOneAchievement = [[ICFGameCenterManager sharedManager] achievementForIdentifier: @"com.dragonforged.whackacac.killone"];

Next a query is performed to see whether this achievement has already been completed. If it has, there is no need to update it again.

if(![killOneAchievement isCompleted])

Since the Kill One achievement cannot be partially achieved because it is impossible to kill fewer than one cactus, it is incremented to 100% at once. This is done using the reportAchievement:withPercentageComplete: method that was added to theICFGameCenterManager class earlier in the chapter.

[[ICFGameCenterManager sharedManager] reportAchievement:@"com.dragonforged.whackacac.killone" withPercentageComplete:100.00];

Since this achievement is tested and submitted when a cactus is whacked, an appropriate place for it is within the cactusHit: method. The updated cactusHit: method is presented for clarity.

- (IBAction)cactusHit:(id)sender;
{
     [UIView animateWithDuration:0.1
                          delay:0.0
                        options: UIViewAnimationCurveLinear | UIViewAnimationOptionBeginFromCurrentState
                     animations:^
                     {
                        [sender setAlpha: 0];
                     }
                     completion:^(BOOL finished)
                     {
                         [sender removeFromSuperview];
                     }];

    score++;

    [self displayNewScore: score];

    GKAchievement *killOneAchievement = [[ICFGameCenterManager sharedManager] achievementForIdentifier: @"com.dragonforged.whackacac.killone"];

    if(![killOneAchievement isCompleted])
    {
        [[ICFGameCenterManager sharedManager]reportAchievement: @"com.dragonforged.whackacac.killone" withPercentageComplete:100.00];
    }

    [self performSelector:@selector(spawnCactus) withObject:nil afterDelay:(arc4random()%3) + .5];
}

Partially Earned Achievements

In the previous example, the achievement was either fully earned or not earned at all. The next achievement that will be implemented into Whack-a-Cac is com.dragonforged.whackacac.score100. Unlike the Kill One achievement, this one can be partially progressed, although it is nonstackable between games. The user is required to score 100 points in a single game. The process begins the same way as the preceding example, in that a reference to the GKAchievement object is created.

GKAchievement *score100Achievement = [[ICFGameCenterManager
sharedManager] achievementForIdentifier:
@"com.dragonforged.whackacac.score100"];

A quick test is performed to ensure that the achievement is not already completed.

if(![score100Achievement isCompleted])

After the achievement has been verified as not yet completed, it can be incremented by the appropriate amount. Since this achievement is completed at 100% and is for 100 points tied to the score, there is a 1%-to-one point ratio. The score can be used to substitute for a percentage complete when populating this achievement.

[[ICFGameCenterManager sharedManager]
reportAchievement:@"com.dragonforged.whackacac.score100"
withPercentageComplete:score];

Although this hook could be placed into the cactusHit: method again it makes more sense to place it into the displayNewScore: method since it is dealing with the score. The entire updated displayNewScore: method with the new achievement hook follows for clarity.

- (void)displayNewScore:(float)updatedScore;
{
    int scoreInt = score;

    if(scoreInt % 10 == 0 && score <= 50)
    {
        [self spawnCactus];
    }

    scoreLabel.text = [NSString stringWithFormat: @"%06.0f", updatedScore];

    GKAchievement *score100Achievement = [[ICFGameCenterManager sharedManager] achievementForIdentifier: @"com.dragonforged.whackacac.score100"];

    if(![score100Achievement isCompleted])
    {
        [[ICFGameCenterManager sharedManager] reportAchievement: @"com.dragonforged.whackacac.score100" withPercentageComplete:score];
    }
}

Since the Score 100 achievement is hidden, it will not appear to the user until the user has completed progress toward it (at least one point). At any time after beginning working on this achievement, the user can see the progress in the Game Center View Controllers, as shown in Figure 4.6.

Image

Figure 4.6 Viewing the partially progressed Score 100 achievement after scoring 43 points in a game; note the completion of the Kill One achievement.

Multiple Session Achievements

In the previous example, the Score 100 achievement required the player to earn the entire 100 points while in a single match. However, there often will be times when it will be required to track a user across multiple matches and even app launches as they progress toward an achievement. The first of the multiple session achievements that will be implemented is the com.dragonforged.whackacac.play5 achievement.

Each time the player completes a round of game play, the com.dragonforged.whackacac.play5 achievement will be progressed. Since the achievement is completed at five games played, each game increments the progress by 20%. This hook will be added to theviewWillDisappear method of ICFGameViewController. As with the previous examples, first a reference to the GKAchievement object is created. After making sure that the achievement isn’t already completed, it can be incremented. A new variable is created to determine the number of matches played, which is the percentage complete divided by 20. The matchesPlayed is then incremented by one, and submitted using reportAchievement:withPercentageComplete: by multiplying matchesPlayed by 20.

-(void)viewWillDisappear:(BOOL)animated
{
    GKAchievement *play5MatchesAchievement = [[ICFGameCenterManager sharedManager] achievementForIdentifier: @"com.dragonforged.whackacac.play5"];

    if(![play5MatchesAchievement isCompleted])
    {
        double matchesPlayed = [play5MatchesAchievement percentComplete]/20.0f;

        matchesPlayed++;

        [[ICFGameCenterManager sharedManager] reportAchievement: @"com.dragonforged.whackacac.play5" withPercentageComplete: matchesPlayed*20.0f];
    }

    [super viewWillDisappear: animated];
}

Piggybacked Achievements and Storing Achievement Precision

Sometimes, it is possible to piggyback two achievements off of each other, such as the next case when dealing with the Whack 100 and Whack 1000 achievements. Since both of these achievements are tracking the same type of objective (whacks) a more streamlined approach can be taken.

As with the other achievement hooks that have been implemented in this chapter, the first thing that is done is to create a reference to a GKAchievement. In the following example a reference to the larger of the two achievements is used. Since the largest achievement has more objectives to complete than it does percentage, it will be rounded down to the nearest multiple of 10. To help combat this problem, a localKills variable is populated from NSUserDefaults. This system falls apart when the achievement exists on two different devices, but can be further polished using iCloud to store the data (see Chapter 8).

A calculation is also made to determine how many kills Game Center reports (with loss of accuracy). If remoteKills is greater than the localKills, we know that the player either has reinstalled the game or has progressed it on another device. In this event the system will default to the Game Center’s values as to not progress the user backward; otherwise, the local information is used.

This code example can be placed inside the cactusHit: method following the Kill One achievement from earlier in this section. After each hit, the localKill value is increased by one. Two checks are performed to make sure that each achievement is not already completed. Since references to both GKAchievements are not available here, a check of the kills number can be used to substitute the standard isComplete check. After the achievements are submitted, the new local value is stored into the NSUserDefaults for future reference.

GKAchievement *killOneThousandAchievement = [[ICFGameCenterManager sharedManager] achievementForIdentifier: @"com.dragonforged.whackacac.1000whacks"];

double localKills = [[[NSUserDefaults standardUserDefaults] objectForKey:@"kills"] doubleValue];

double remoteKills = [killOneThousandAchievement percentComplete] * 10.0;

if(remoteKills > localKills)
{
    localKills = remoteKills;
}

localKills++;

if(localKills <= 1000)
{
      if(localKills <= 100)
      {
          [[ICFGameCenterManager sharedManager] reportAchievement: @"com.dragonforged.whackacac.100whacks" withPercentageComplete:localKills];
      }

      [[ICFGameCenterManager sharedManager] reportAchievement:@"com.dragonforged.whackacac.1000whacks" withPercentageComplete:(localKills/10.0)];
}

[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble: localKills] forKey:@"kills"];

Timer-Based Achievements

One of the most popular achievements in iOS gaming is to play for a certain amount of time. In Whack-a-Cac com.dragonforged.whackacac.play5Mins is used to track the user’s progress towards five minutes of total gameplay. This particular example exists for the loss-of-accuracy problem that was faced in the Whack 1000 example from earlier in this section, and it can be overcome in the same manner.

To track time, a NSTimer will be created. To determine how often the timer should fire, you will need to determine what 1% of five minutes is.

play5MinTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(play5MinTick) userInfo:nil repeats:YES];

When the timer fires a new call to a new play5MinTick method is called. If the game is paused or in a gameOver state, the achievement progress is ignored and the method returns. As with the other examples, a reference to the GKAchievement object is created, and a check is performed to see whether it has completed. If this achievement is completed, the timer is invalidated to prevent wasted CPU time. Otherwise, since the timer is firing every three seconds (1% of five minutes), the achievement is progressed by 1%.

- (void)play5MinTick;
{
    if(paused || gameOver)
    {
        return;
    }

    GKAchievement *play5MinAchievement = [[ICFGameCenterManager sharedManager] achievementForIdentifier: @"com.dragonforged.whackacac.play5Mins"];

    if([play5MinAchievement isCompleted])
    {
        [play5MinTimer invalidate];
        play5MinTimer = nil;
        return;
    }

    double percentageComplete =    play5MinAchievement.percentComplete + 1.0;

    [[ICFGameCenterManager sharedManager] reportAchievement: @"com.dragonforged.whackacac.play5Mins" withPercentageComplete: percentageComplete];
}

Resetting Achievements

There is often a need to reset all achievement progress for a user, more so in development than in production. Sometimes it is helpful to provide users with a chance to take a fresh run at a game or even some sort of prestige mode that starts everything over, but at a harder difficulty. Resetting achievement progress is simple; the following code snippet can be added into the IFCGameCenterManager class to completely reset all achievements to the unearned state. If you are providing this functionality to users, it is a good idea to have several steps of confirmation to prevent accidental resetting.

- (void)resetAchievements
{
      [achievementDictionary removeAllObjects];

       [GKAchievement resetAchievementsWithCompletionHandler:
      ^(NSError *error)
       {
             if(error == nil)
             {
                  NSLog(@"All achievements have been successfully reset");
             }

             else
             {
                 NSLog(@"Unable to reset achievements: %@", [error localizedDescription]);
             }
       }];
}


Tip

While in development and during debugging it can be helpful to keep a call to reset achievements in the authentication successfully completed block of the ICFGameCenterManager class that can easily be commented out to assist with testing and implementing achievements.


Going Further with Achievements

Apple provides a lot for free in regard to displaying and progressing achievements. However, Apple does not allow the customization of the provided interface for viewing achievements. The look and feel of Apple’s achievement view controllers might simply not fit into the app’s design. In cases like this, the raw achievement information can be accessed for display in a customized interface. Although fully setting up custom achievements is beyond the scope of this chapter, this section contains information that will assist you.

Earlier, you learned about creating a local cache of GKAchievements. However, GKAchievement objects are missing critical data that will be needed in order to display achievement data to the user, such as the description, title, name, and number of points it is worth. Additionally, when the achievement cache is used, if an achievement has not been progressed, it will not appear in the cache. To retrieve all achievements and the required information needed to present them to the user, a new class is required.

Using the GKAchievementDescription class and the class method loadAchievementDescriptionsWithCompletionHandler:, you can gain access to an array of GKAchievementDescriptions. A GKAchievementDescription object contains properties for the titles, descriptions, images, and other critical information. However, the GKAchievementDescription does not contain any information about the current progress of the achievement for the local user; in order to determine progress, the identifier will need to be compared to the local achievement cache.

[GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:^(NSArray *descriptions, NSError *error)
{
      if(error != nil)
      {
          NSLog(@"An error occurred loading achievement descriptions: %@", [error localizedDescription]);
      }

      for(GKAchievementDescription *achievementDescription in descriptions)
      {
          NSLog(@"%@\n", achievementDescription);
      }

}];

When the preceding code is executed on Whack-a-Cac, the console will display the following information:

2012-09-01 16:38:07.754 WhackACac[48552:c07] <GKAchievementDescription:
0x1185f810>id: com.dragonforged.whackacac.100whacks Whack 100 Cacti
visible Good job! You whacked 100 of them!

2012-09-01 16:38:07.755 WhackACac[48552:c07] <GKAchievementDescription:
0x1185f980>id: com.dragonforged.whackacac.1000whacks Whack 1000!
visible You are a master at killing those cacti.

2012-09-01 16:38:07.755 WhackACac[48552:c07] <GKAchievementDescription:
0x1185f820>id: com.dragonforged.whackacac.play5 Play 5 Matches visible
Your dedication to cactus whacking is unmatched.

2012-09-01 16:38:07.755 WhackACac[48552:c07] <GKAchievementDescription:
0x1185eae0>id: com.dragonforged.whackacac.score100 Score 100 hidden
Good work on getting 100 cacti in one match!

2012-09-01 16:38:07.755 WhackACac[48552:c07] <GKAchievementDescription:
0x1185eaf0>id: com.dragonforged.whackacac.killone Kill One  hidden You
have started on your cactus killing path, there is no turning back now.

2012-09-01 16:38:07.756 WhackACac[48552:c07] <GKAchievementDescription:
0x1185eb30>id: com.dragonforged.whackacac.play5Mins Play for 5 Minutes
visible Good work! Your dedication continues to impress your peers.

2012-09-01 16:38:07.756 WhackACac[48552:c07] <GKAchievementDescription:
0x1185eb40>id: com.dragonforged.whackacac.hit5Fast Hit 5 Quick hidden
You are truly a quick gun.

A list of all the achievements has now been retrieved and can be compared to the local achievement cache to determine percentages completed. You have everything that is needed to create a customized GUI for presenting the achievement data to the user.

Summary

In this chapter, you learned about integrating Game Center Achievements into an iOS project. You continued to build up the sample game Whack-a-Cac that was introduced in Chapter 3. This chapter also continued to expand on the reusable Game Center Manager class.

You should now have a firm understanding of how to create achievements, set up your app to interact with them, post and show progress, and reset all achievement progress. Additionally, a brief look at going beyond the standard behavior was provided. With the knowledge gained in this chapter, you now have the ability to fully integrate achievements into any app.

Exercises

1. Implement a system of storing and resubmitting achievements if the Game Center servers are unreachable. Look at the approach used for leaderboards in Chapter 3 for guidance. Be sure to keep only the highest percentages complete in the offline cache.

2. Add a new achievement for hitting five cacti within one second. You will need to create some timers that track sequential hits. You can use the existing achievement identifier com.dragonforged.whackacac.hit5Fast.

3. Implement a custom display for your achievement progress using the information from the section “Going Further with Achievements” as a guide.