iOS Game Center Achievement Display

In one of my projects, while I waited for some art work, I wanted to get ahead with my Game Center integration. This tutorial is about how to get Achievements to be displayed in the Game when the achievement is sent to Game Center. I will further explore how to get the achievement images also included. A few simple frameworks will be used.

Also, I am taking it for granted that Game Center is up and running on your app. I was able to get it up and running in a few hours using the methods found in Learning Cocos2D or in numerous tutorials around, here or here.

Once you have a user authenticating, and you have setup a few iTunes Connect achievements, you might want to display an 'Achievement Attained' message when a new achievement has been accomplished. Again, this is different than just showing the Achievements window, that shows all the achievements a player has completed. We want a little notification message displayed.

In iOS 5, Apple finally gave us a prepackaged API call that will show an achievement banner when an achievement is complete (per your code giving a 100.0% to an achievement). Here is the simplest way:

- (void)sendAchievement:(GKAchievement *)achievement {

    achievement.percentComplete = 100.0;   //Indicates the achievement is done
    achievement.showsCompletionBanner = YES;    //Indicate that a banner should be shown
    [achievement reportAchievementWithCompletionHandler:
     ^(NSError *error) {
         dispatch_async(dispatch_get_main_queue(), ^(void)
                        {
                            if (error == NULL) {
                                NSLog(@"Successfully sent archievement!");               
                            } else {
                                NSLog(@"Achievement failed to send... will try again \
                                      later.  Reason: %@", error.localizedDescription);                
                            }
                        });
     }];
}

This code creates the achievement object according to your identifier (whatever you setup in iTunes), you set the completion to 100%, set the 'showsCompletionBanner' property to YES, and shoot it off to Apple. The new iOS property is 'showsCompletionBanner' which defaults to NO, but if updated to YES, will display a cute little banner with the title and description of the achievement, as shown below (yes I am using the http://www.neuroshimahex.com/ game as inspiration, so placed as my dev background. Check it out, very fun strategy game).

But this approach has a few limitations. First off, it uses the generic Game Center icon, instead of the image that I uploaded for my game achievement. Worst off, if the player earns two achievements at the same time, only one of them will be displayed, which I think is a bummer. And lastly, this is an iOS 5 only API calls, so will crash any device not running iOS5, oops!

So to repair all of these issues, we will not be using the 'showsCompletionBanner' property, but will be implementing our own notification, or more correctly using and modifying some code of people who have gone before us.

To get started, I used the great code that Type One Error demonstrated in http://www.typeoneerror.com/articles/post/game-center-achievement-notifi.... Read that article for some background if you want, and then grab the code at https://github.com/typeoneerror/GKAchievementNotification and install into your project. We will be using the GKAchievementNotification and GKAchievementHandler classes, with some updates and modifications. First off, if you are using ARC for your game, do a quick scan of the code and remove the few release, retain and autoreleases you find in that code. If you don't want to scan, just try to build after you place the files in your project and fix wherever the compiler squawks at.

The Type One Error classes will display a notification that is similar to the one that the iOS 5 call gives, but your code needs to know what the achievement title and description is. To do this you need to populated a'GKAchievementDescription' object.

One great thing about GKAchievementDescription objects is that they are already localized according to the language setting of the user (if you support that language) and thus don't have to worry about any localization issue if you use this method.

The bummer is that you can't load just one Achievement description, you have to load them all. I believe the best time to do this is when a user has authenticated Game Center on your app, you should do an async call to get them. Galdly Apple does give an API call for this, which I place in the CompletionHandler of the user authentication call.

If you are using the code by Ray Wenderlich, then you have a method like this, which I add one line to, and a new method. Also add an NSMutableDictionary * self.achievementsDescDictionary to whatever class is processing your Game Center code, which will store the achievement data for the rest of the active session.

- (void)authenticateLocalUser { 
    
    if (!gameCenterAvailable) return;

    NSLog(@"Authenticating local user...");
    if ([GKLocalPlayer localPlayer].authenticated == NO) {     
        [[GKLocalPlayer localPlayer] 
         authenticateWithCompletionHandler:^(NSError *error) {
             if([GKLocalPlayer localPlayer].isAuthenticated){
                 [self retrieveAchievmentMetadata];         //Here is the new code
             }
         }];        
    }
}


//Here is the new method.
- (void) retrieveAchievmentMetadata
{
    self.achievementsDescDictionary = [[NSMutableDictionary alloc] initWithCapacity:2];
    
    [GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:
     ^(NSArray *descriptions, NSError *error) {
         if (error != nil) {
             NSLog(@"Error %@", error);
             
         } else {        
             if (descriptions != nil){
                 for (GKAchievementDescription* a in descriptions) {
                     [achievementsDescDictionary setObject: a forKey: a.identifier];
                 }
             }
         }
     }];
}

The 'retrieveAchievmentMetadata' method initializes the dictionary then calls to get all the Achievement descriptions for your game, and cycles through and adds them to the dictionary. This is an async call, so it shouldn't slow down the start of your game or project.

Now that we have all the titles and descriptions for all your achievements, we can modify our original code to create an iOS 4/5 friendly notification, that will also show all achievements in succession using the Type One Error code, again inside of Ray Wenderlich's code (resource links at the top and bottom of this post).

- (void)reportAchievement:(NSString *)identifier 
          percentComplete:(double)percentComplete {    
        
    GKAchievement* achievement = [[GKAchievement alloc] 
                                   initWithIdentifier:identifier];
    achievement.percentComplete = percentComplete; 
    
    if (percentComplete == 100.0) {
        //Show banners manually
        GKAchievementDescription *desc = [achievementsDescDictionary objectForKey:identifier]; //Update pull achievement description for dictionary
        
         [[GKAchievementHandler defaultHandler] notifyAchievement:desc];  //Display to user


    }
    [achievementsToReport addObject:achievement];    //Queue up the achievement to be sent
    [self save]; 
    
    if (!gameCenterAvailable || !userAuthenticated) return;
    [self sendAchievement:achievement];   //Try to send achievement 
}


- (void)sendAchievement:(GKAchievement *)achievement {
    [achievement reportAchievementWithCompletionHandler:
     ^(NSError *error) {
         dispatch_async(dispatch_get_main_queue(), ^(void)
                        {
                            if (error == NULL) {
                                NSLog(@"Successfully sent archievement!");
                                [achievementsToReport removeObject:achievement];   //Remove Achievement from queue.             
                            } else {
                                NSLog(@"Achievement failed to send... will try again \
                                      later.  Reason: %@", error.localizedDescription);                
                            }
                        });
     }];
}

The additional checks to see if the achievement is 100% complete, and if so, grabs the correct achievement description object and displays it to the user. Then it continues on with telling Apple about the new achievement, and if you have a landscape game, it will look something like this.

But, I think it would be even better to also have the achievement image displayed, instead of the default image. So to do that, update the code with the notification part to this:


    if (percentComplete == 100.0) {
        //Show banners manually
        GKAchievementDescription *desc = [achievementsDescDictionary objectForKey:identifier];

        [desc loadImageWithCompletionHandler:^(UIImage *image, NSError *error) {
            if (error == nil)
            {
                [[GKAchievementHandler defaultHandler] setImage:desc.image];   //If image found, updates the image to the achievement image.
            } 
            [[GKAchievementHandler defaultHandler] notifyAchievement:desc];
        }];

    }

Since that code is inside a completion handler block, it will only execute once the handler returns. If it returns an image, then the notification is updated with that image, and then it displays to the user.

Yeah, yeah, I have temporary art in there. I told you the art was falling behind. Anyway, using this method out of the box you will notice that if you have a landscape game, that the notifications are off to the side and sideways (the Type One Error guys made it for portrait only). To fix this for landscape you need to do two things, adjust the frame and the rotation.

Find the 'notifyAchievement' method in the GKAchievementHandler class and update to:

- (void)notifyAchievement:(GKAchievementDescription *)achievement
{

    GKAchievementNotification *notification = [[GKAchievementNotification alloc] initWithAchievementDescription:achievement];
    notification.frame = kGKAchievementFrameStart;
    notification.handlerDelegate = self;
    
    //Adjusting rotation.
    
    if ([[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationLandscapeLeft) {
        notification.transform = CGAffineTransformRotate(notification.transform, degreesToRadian(-90));
    } else {
        notification.transform = CGAffineTransformRotate(notification.transform, degreesToRadian(90));
    }
    
    notification.frame = kGKAchievementFrameStartLandscape;  //Update the frame, you need to create this definition.
    
    [_queue addObject:notification];
    if ([_queue count] == 1)
    {
        [self displayNotification:notification];
    }
}

Also adjust the 'animateIn' and 'animateOut' frames as needed. I found these frames to be useful:

#define kGKAchievementFrameStartLandscape    CGRectMake(-53.0f, 350.0f, 104.0f, 284.0f);
#define kGKAchievementFrameEndLandscape      CGRectMake(20.0f, 350.0f, 104.0f, 284.0f);

So, by using the Game Center code from Ray Wenderlich's
tutorial as a starting point, along with the Type One Error notification code, we then added adjustments so our final solution does the following:

1) Display one or more achievement notifications (the Type One Error code queues the display of many notifications)
2) Displays the notifications with images and full title and descriptions.
3) In a manner that is iOS version independent.

You determine a player had completed an achievement, sprinkle in:

[GCHelper sharedInstance] reportAchievement:identifier 
          percentComplete:percentComplete];  

Or whatever way you access your Game Center achievements

Lastly, if you believe pulling all the achievement data early in the authentication call is a performance or data issue, you can wait and place the 'retrieveAchievmentMetadata' call until the player earns an achievement, and place in its completion handler, add the code to display the achievement notification. But there is a little extra legwork needed to save the achievement until you have retrieved all the description data. I leave it to the reader to finish off that particular use case.

Resources:
http://developer.apple.com/library/ios/#documentation/NetworkingInternet...
http://www.typeoneerror.com/articles/post/game-center-achievement-notifi...
https://github.com/typeoneerror/GKAchievementNotification
http://www.raywenderlich.com/3276/how-to-make-a-simple-multiplayer-game-...

Comment below, or with Twitter,

Very usefull code, similar to

Very usefull code, similar to the one I coded for "Guess the Character".

I will use your improvements in my next game.

Thank's for sharing. Jose A, http://www.jandusoft.com

Thanks for the sweet

Thanks for the sweet tutorial! I have also been working off of Ray's excellent tutorials and this put the icing on the cake ;)

Is there a way to make the notifications stay visible for a longer period of time? They seem to pop in and out quickly. Sometimes even before I can read the whole msg.

Sorry for not responding

Sorry for not responding earlier. Check for and modify these in GKAchievementNotification.h:

#define kGKAchievementAnimeTime 0.4f
#define kGKAchievementDisplayTime 1.75f

Thanks for the tutorial! I

Thanks for the tutorial!

I added this GKAchievementHandler and GKAchievementNotification to my project.
But I got an error when run: (I added GameKit.framework to my project already)

Undefined symbols for architecture i386:
"_OBJC_CLASS_$_GKAchievementHandler", referenced from:
objc-class-ref in GameKitHelper.o
ld: symbol(s) not found for architecture i386
clang: error: linker command failed with exit code 1 (use -v to see invocation)

How can I solve this problem?? Thx

Most likely you haven't added

Most likely you haven't added a framework that is needed. Probably GameKit.

I added GameKit.framework to

I added GameKit.framework to my project already, because my game project is already included Gamecenter.

Hence, I don't know what is the problem.

I added GameKit.framework to

I added GameKit.framework to my project already, because my game project is already included Gamecenter.

Hence, I don't know what is the problem.

Hi, Same problem than Calvin.

Hi,

Same problem than Calvin. Has anybody a fix for this ??