Game Development Community

Game Center Support

by Justin Mosiman · in iTorque 2D · 11/12/2010 (8:53 pm) · 67 replies

With the sad news from InstantAction, I feel that it is more important now than ever to keep iTGB alive. With that, I am going to post instructions on how to integrate Game Center within iTGB. I know that integrating Game Center with Open Feint is easier, but I feel that the two frameworks have a lot of overlapping features and I want to reduce the amount of bloat. If this code has been useful to you, I'd appreciate a War Evolved download :)

There is a lot of setup that you have to do within iTunes Connect to get Game Center to work, so instead of repeating it I'll just point you to the iTunes Connect Game Center docs. Even though the code is here, I still recommend reading the docs to be sure you don't miss anything and can understand what is happening.

Assuming you have iTunes Connect setup, perform the following steps:

1. Create GameCenter.mm and GameCenter.h within the platformiPhone directory in the Xcode Torque project.

2. Add the following to GameCenter.h:
//
//  GameCenter.m
//  Torque2D
//
//  Created by Justin Mosiman on 10/29/10.
//  Copyright 2010 Opsive, LLC. All rights reserved.
//

#import <GameKit/GameKit.h>
#include "console/console.h"
@interface GameCenter : UIViewController <GKLeaderboardViewControllerDelegate, GKAchievementViewControllerDelegate>
{
	BOOL isAuthenticated;
	BOOL gameCenterAvailable;

	UIViewController *gameCenterViewController;
}

@property (nonatomic,retain) UIViewController *gameCenterViewController;

- (void)authenticateLocalPlayer;
- (void)registerForAuthenticationNotification;
- (void)authenticationChanged;
- (BOOL)isAuthenticated;

- (void)reportScore:(int64_t)score forCategory:(NSString*)category;
- (void)reportScore:(GKScore *)scoreReporter;
- (void)saveScoreToDevice:(GKScore *)score;
- (void)retrieveScoresFromDevice;
- (void)showLeaderboard;
- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController;

- (void)reportAchievementIdentifier:(NSString*)identifier percentComplete:(float)percent;
- (void)reportAchievementIdentifier:(GKAchievement *)achievement;
- (void)saveAchievementToDevice:(GKAchievement *)achievement;
- (void)retrieveAchievementsFromDevice;
- (void)showAchievements;
- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController;

- (void)close;

@end

static GameCenter *gameCenter;

namespace GameCenterWrapper {
	void authenticate();
	void reportScore(int score, int category);
	void reportAchievement(int achievement);
	bool isAuthenticated();
	void showLeaderboard();
	void showAchievements();
	void close();
}

3. Add the following code to GameCenter.mm:
//
//  GameCenter.mm
//  Torque2D
//
//  Created by Justin Mosiman on 10/29/10.
//  Copyright 2010 Opsive, LLC. All rights reserved.
//
#import "platformiPhone/GameCenter.h"

BOOL isGameCenterAvailable()
{
    // Check for presence of GKLocalPlayer API.	
    Class gcClass = (NSClassFromString(@"GKLocalPlayer"));	
	
    // The device must be running running iOS 4.1 or later.	
    NSString *reqSysVer = @"4.1";	
    NSString *currSysVer = [[UIDevice currentDevice] systemVersion];	
    BOOL osVersionSupported = ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending);	
	
    return (gcClass && osVersionSupported);
}

@implementation GameCenter

@synthesize gameCenterViewController;

//--------------------------------------------------------
// Static functions/variables
//--------------------------------------------------------

static NSString *getGameCenterSavePath()
{
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	return [NSString stringWithFormat:@"%@/GameCenterSave.txt",[paths objectAtIndex:0]];
}

static NSString *scoresArchiveKey = @"Scores";

static NSString *achievementsArchiveKey = @"Achievements";

//--------------------------------------------------------
// Authentication
//--------------------------------------------------------

- (void)authenticateLocalPlayer {
	isAuthenticated = NO; // assume the player isn't authenticated
	gameCenterAvailable = isGameCenterAvailable();
	
	if(!gameCenterAvailable){
		return;
	}
	
	gameCenterViewController = [[UIViewController alloc] init];
	
    [[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:^(NSError *error) {		
		if (error == nil){
			// Insert code here to handle a successful authentication.
			isAuthenticated = YES;
			[self registerForAuthenticationNotification];
			
			// report any unreported scores or achievements
			[self retrieveScoresFromDevice];
			[self retrieveAchievementsFromDevice];
			
			// let the scripts know
			Con::executef(2,"gameCenterAuthenticationChanged","1");
		}else{
			Con::executef(2,"gameCenterAuthenticationChanged","0");
		}
	}];
}

- (void)registerForAuthenticationNotification
{
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver: self selector:@selector(authenticationChanged) name:GKPlayerAuthenticationDidChangeNotificationName object:nil];
}

- (void)authenticationChanged
{
	isAuthenticated = NO; // assume the player isn't authenticated
	gameCenterAvailable = isGameCenterAvailable();
	
	if(!gameCenterAvailable){
		return;
	}
	
    if ([GKLocalPlayer localPlayer].isAuthenticated){		
        // Insert code here to handle a successful authentication.
		isAuthenticated = YES;
		
		// report any unreported scores or achievements
		[self retrieveScoresFromDevice];
		[self retrieveAchievementsFromDevice];
		
		// let the scripts know
		Con::executef(2,"gameCenterAuthenticationChanged","1");
	}else{
		Con::executef(2,"gameCenterAuthenticationChanged","0");
	}
}

- (BOOL)isAuthenticated
{
	return gameCenterAvailable && isAuthenticated;
}

//--------------------------------------------------------
// Leaderboard
//--------------------------------------------------------

- (void)reportScore:(int64_t)score forCategory:(NSString*)category
{
	if(!gameCenterAvailable)
		return;
	
    GKScore *scoreReporter = [[[GKScore alloc] initWithCategory:category] autorelease];
	if(scoreReporter){
		scoreReporter.value = score;	
		
		[scoreReporter reportScoreWithCompletionHandler:^(NSError *error) {	
			if (error != nil){
				// handle the reporting error
				[self saveScoreToDevice:scoreReporter];
			}
		}];	
	}
}

- (void)reportScore:(GKScore *)scoreReporter
{
	if(!gameCenterAvailable)
		return;
	
	if(scoreReporter){
		[scoreReporter reportScoreWithCompletionHandler:^(NSError *error) {	
			if (error != nil){
				// handle the reporting error
				[self saveScoreToDevice:scoreReporter];
			}
		}];	
	}
}

- (void)saveScoreToDevice:(GKScore *)score
{
	NSString *savePath = getGameCenterSavePath();
	
	// If scores already exist, append the new score.
	NSMutableArray *scores = [[[NSMutableArray alloc] init] autorelease];
	NSMutableDictionary *dict;
	if([[NSFileManager defaultManager] fileExistsAtPath:savePath]){
		dict = [[[NSMutableDictionary alloc] initWithContentsOfFile:savePath] autorelease];
		
		NSData *data = [dict objectForKey:scoresArchiveKey];
		if(data) {
			NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
			scores = [unarchiver decodeObjectForKey:scoresArchiveKey];
			[unarchiver finishDecoding];
			[unarchiver release];
			[dict removeObjectForKey:scoresArchiveKey]; // remove it so we can add it back again later
		}
	}else{
		dict = [[[NSMutableDictionary alloc] init] autorelease];
	}
	
	[scores addObject:score];
	
	// The score has been added, now save the file again
	NSMutableData *data = [NSMutableData data];	
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
	[archiver encodeObject:scores forKey:scoresArchiveKey];
	[archiver finishEncoding];
	[dict setObject:data forKey:scoresArchiveKey];
	[dict writeToFile:savePath atomically:YES];
	[archiver release];
}

- (void)retrieveScoresFromDevice
{
	NSString *savePath = getGameCenterSavePath();
	
	// If there are no files saved, return
	if(![[NSFileManager defaultManager] fileExistsAtPath:savePath]){
		return;
	}
	
	// First get the data
	NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:savePath];
	NSData *data = [dict objectForKey:scoresArchiveKey];
	
	// A file exists, but it isn't for the scores key so return
	if(!data){
		return;
	}
	
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
	NSArray *scores = [unarchiver decodeObjectForKey:scoresArchiveKey];
	[unarchiver finishDecoding];
	[unarchiver release];
	
	// remove the scores key and save the dictionary back again
	[dict removeObjectForKey:scoresArchiveKey];
	[dict writeToFile:savePath atomically:YES];
	
	
	// Since the scores key was removed, we can go ahead and report the scores again
	for(GKScore *score in scores){
		[self reportScore:score];
	}
}

- (void)showLeaderboard
{
	if(!isAuthenticated)
		return;
	
    GKLeaderboardViewController *leaderboardController = [[GKLeaderboardViewController alloc] init];	
    if (leaderboardController != nil) {
        leaderboardController.leaderboardDelegate = self;
		
		UIWindow* window = [UIApplication sharedApplication].keyWindow;
		[window addSubview: gameCenterViewController.view];
        [gameCenterViewController presentModalViewController: leaderboardController animated: YES];
    }	
}

- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController
{
    [gameCenterViewController dismissModalViewControllerAnimated:YES];
	[viewController.view removeFromSuperview];
	[viewController release];
}

//--------------------------------------------------------
// Achievements
//--------------------------------------------------------

- (void)reportAchievementIdentifier:(NSString*)identifier percentComplete:(float)percent
{
	if(!gameCenterAvailable)
		return;
	
    GKAchievement *achievement = [[[GKAchievement alloc] initWithIdentifier: identifier] autorelease];	
    if (achievement){		
		achievement.percentComplete = percent;		
		[achievement reportAchievementWithCompletionHandler:^(NSError *error){
			if (error != nil){
				[self saveAchievementToDevice:achievement];
			}		 
		}];
    }
}

- (void)reportAchievementIdentifier:(GKAchievement *)achievement
{	
	if(!gameCenterAvailable)
		return;
	
    if (achievement){		
		[achievement reportAchievementWithCompletionHandler:^(NSError *error){
			if (error != nil){
				[self saveAchievementToDevice:achievement];
			}		 
		}];
    }
}

- (void)saveAchievementToDevice:(GKAchievement *)achievement
{
	
	NSString *savePath = getGameCenterSavePath();
	
	// If achievements already exist, append the new achievement.
	NSMutableArray *achievements = [[[NSMutableArray alloc] init] autorelease];
	NSMutableDictionary *dict;
	if([[NSFileManager defaultManager] fileExistsAtPath:savePath]){
		dict = [[[NSMutableDictionary alloc] initWithContentsOfFile:savePath] autorelease];
		
		NSData *data = [dict objectForKey:achievementsArchiveKey];
		if(data) {
			NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
			achievements = [unarchiver decodeObjectForKey:achievementsArchiveKey];
			[unarchiver finishDecoding];
			[unarchiver release];
			[dict removeObjectForKey:achievementsArchiveKey]; // remove it so we can add it back again later
		}
	}else{
		dict = [[[NSMutableDictionary alloc] init] autorelease];
	}
	
	
	[achievements addObject:achievement];
	
	// The achievement has been added, now save the file again
	NSMutableData *data = [NSMutableData data];	
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
	[archiver encodeObject:achievements forKey:achievementsArchiveKey];
	[archiver finishEncoding];
	[dict setObject:data forKey:achievementsArchiveKey];
	[dict writeToFile:savePath atomically:YES];
	[archiver release];	
}

- (void)retrieveAchievementsFromDevice
{
	NSString *savePath = getGameCenterSavePath();
	
	// If there are no files saved, return
	if(![[NSFileManager defaultManager] fileExistsAtPath:savePath]){
		return;
	}
	
	// First get the data
	NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:savePath];
	NSData *data = [dict objectForKey:achievementsArchiveKey];
	
	// A file exists, but it isn't for the achievements key so return
	if(!data){
		return;
	}
	
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
	NSArray *achievements = [unarchiver decodeObjectForKey:achievementsArchiveKey];
	[unarchiver finishDecoding];
	[unarchiver release];
	
	// remove the achievements key and save the dictionary back again
	[dict removeObjectForKey:achievementsArchiveKey];
	[dict writeToFile:savePath atomically:YES];
	
	// Since the key file was removed, we can go ahead and try to report the achievements again
	for(GKAchievement *achievement in achievements){
		[self reportAchievementIdentifier:achievement];
	}
}

- (void)showAchievements
{	
	if(!isAuthenticated)
		return;
	
    GKAchievementViewController *achievements = [[GKAchievementViewController alloc] init];	
    if (achievements != nil){
        achievements.achievementDelegate = self;
		
		UIWindow* window = [UIApplication sharedApplication].keyWindow;
		[window addSubview: gameCenterViewController.view];
        [gameCenterViewController presentModalViewController: achievements animated: YES];
    }	
}

- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController
{
    [gameCenterViewController dismissModalViewControllerAnimated:YES];
	[viewController.view removeFromSuperview];
	[viewController release];
}

//--------------------------------------------------------
// Goodbye
//--------------------------------------------------------

- (void)close
{
	[gameCenterViewController release];
}

@end


//--------------------------------------------------------
// Wrapper
//--------------------------------------------------------

void GameCenterWrapper::authenticate(){
	if(isGameCenterAvailable()){
		gameCenter = [[GameCenter alloc] init];
		[gameCenter authenticateLocalPlayer];
	}
}

// 
// If the category names are updated, make sure they are also updated in iTunes Connect.
// 
void GameCenterWrapper::reportScore(int score, int category){
	NSString *categoryID = [[NSString alloc] autorelease];
	switch (category){
		case 0:
			categoryID = @"YOUR_CATEGORY_ID_0";
			break;
		case 1:
			categoryID = @"YOUR_CATEGORY_ID_1";
			break;
	}
	
	[gameCenter reportScore:score forCategory:categoryID];
}

void GameCenterWrapper::reportAchievement(int achievement){
	NSString *achievementID = [[NSString alloc] autorelease];
	switch (achievement){
		case 0:
			achievementID = @"YOUR_ACHIEVEMENT_ID_0";
			break;
		case 1:
			achievementID = @"YOUR_ACHIEVEMENT_ID_1";
			break;
	}
	
	[gameCenter reportAchievementIdentifier:achievementID percentComplete:100.0f]; // no partial achievements
}

bool GameCenterWrapper::isAuthenticated(){
	return [gameCenter isAuthenticated];
}

void GameCenterWrapper::showLeaderboard(){
	[gameCenter showLeaderboard];
}

void GameCenterWrapper::showAchievements(){
	[gameCenter showAchievements];
}

void GameCenterWrapper::close(){
	if(isGameCenterAvailable()){
		[gameCenter close];
		[gameCenter release];
	}
}

ConsoleFunction(isGameCenterAvailable,bool,1,1,"()")
{
	return isGameCenterAvailable();
}

ConsoleFunction(authenticateGameCenter,void,1,1,"")
{
	GameCenterWrapper::authenticate();
}

ConsoleFunction(reportGameCenterScore,void,3,3,"(score, category id)")
{
	GameCenterWrapper::reportScore(dAtoi(argv[1]),dAtoi(argv[2]));
}

ConsoleFunction(unlockGameCenterAchievement,void,2,2,"(achievement id)")
{
	GameCenterWrapper::reportAchievement(dAtoi(argv[1]));
}

ConsoleFunction(isGameCenterAuthenticated,bool,1,1,"")
{	
	return GameCenterWrapper::isAuthenticated();
}

ConsoleFunction(showGameCenterLeaderboard,void,1,1,"")
{
	GameCenterWrapper::showLeaderboard();
}

ConsoleFunction(showGameCenterAchievements,void,1,1,"")
{
	GameCenterWrapper::showAchievements();
}

ConsoleFunction(closeGameCenter,void,1,1,""){
	GameCenterWrapper::close();
}

4. Update GameCenterWrapper::reportScore() and GameCenterWrapper::reportAchievement() to include the score category/achievement id that was set in iTunes Connect.

5. To initialize Game Center, call the following Torque Script function:
authenticateGameCenter();
I put it in the initializeProject() function within game/main.cs. It needs to be one of the first things called once iTGB has initialized.

6. To close Game Center, call:
closeGameCenter();
I put it within shutdownProject() of game/main.cs.

7. To add a high score, call:
reportGameCenterScore(%score,%category);

8. To award an achievement, call:
unlockGameCenterAchievement(%achievementID);

9. To show the leaderboard GUI, call:
showGameCenterLeaderboard();

10. To show the achievements GUI, call:
showGameCenterAchievements();

11. The Torque Script function gameCenterAuthenticationChanged(%isAvailable) is called whenever the authentication changes. Add this function somewhere in your game to handle logging into Game Center, such as showing the achievements or leaderboard.

Note that it is up to you to decide how you want to indicate that an achievement was achieved. There are more Game Center related functions exposed, and you can decide if you need to use them, but I haven't found a use for them.

If this code has been useful to you, I'd appreciate a War Evolved download :)

Justin
Page «Previous 1 2 3 4 Last »
#1
11/12/2010 (9:38 pm)
Good stuff, Justin. OpenFeint also has a migration system where you can ween users off it, then transition fully to GC. Your code there would be the second phase :)
#2
11/13/2010 (12:50 am)
Thank you for posting this. This will save me a good amount of time.
#3
11/15/2010 (5:43 pm)
This is awesome! I'll need this for my next game (as soon as I can get this one out!)

Thanks for sharing!
#4
11/30/2010 (6:24 am)
First off, thank you for posting this, it's a big help to see a full fledged example.

Everything works well for me on iPhone 3Gs with OS 4.1, but on the iPad with OS 4.2.1 the showLeaderboard and showAchievements functions aren't popping up a Game Center view for me.

What's really strange about it is that I put some console print statements between each line of code in the function and everything prints out up until "[window addSubview: leaderboardController.view];". I don't see any output after that, yet my program continues running.

Any idea why the achievement and leaderboard views might not be showing under 4.2.1? Has anyone else experienced that?
#5
11/30/2010 (2:34 pm)
That's weird. I don't think I can help you right now, I haven't tried it on the iPad yet. I am porting War Evolved over to the iPad though and I'll probably get to Game Center mid/late December. If you haven't figured out an answer by then I'll try to help. But if you do figure out an answer please post back so I can update what is wrong.
#6
12/03/2010 (7:18 am)
Just an update: I haven't figured out what the issue is yet, but it is related to OS 4.2.1, not the iPad. Once I updated the phone to 4.2.1 the leaderboard and achievement views didn't show up there either. I suppose it's some problem with my program specifically or lots of apps would have broken with this latest iOS release :(.
#7
12/03/2010 (9:01 am)
Wow,
thank you.
#8
12/05/2010 (6:53 pm)
Thank you very much. Truly generous of you to post this.
#9
12/09/2010 (4:31 am)
The issue I had with OS 4.2.1 and the Game Center views not showing was fixed by re-creating the project from XCode's newer OpenGL template that has a view controller in it (the old one did not).

The old OpenGL template looked like this (worked only in 4.1):
Window
--OpenGL View
--Game Center View

And the new one looks like this (works in 4.1 and 4.2.1):
Window
--View Controller
----OpenGL View
----Game Center View
#10
12/23/2010 (8:48 pm)
here is the fix for 4.2.1 about the error of modal-view present:

- (void)showLeaderboard
{
if(!isAuthenticated)
return;

GKLeaderboardViewController *leaderboardController = [[GKLeaderboardViewController alloc] init];
if (leaderboardController != nil) {
leaderboardController.leaderboardDelegate = self;
[self presentModalViewController: leaderboardController animated: YES];

UIViewController *glView = [[UIViewController alloc] init];
UIWindow* window = [UIApplication sharedApplication].keyWindow;


glView.view.frame = CGRectMake(0.0f, 0.0f, 480.0, 320.0);

[window addSubview: glView.view];

[glView.view addSubview: leaderboardController.view];

// [window addSubview: leaderboardController.view];



}
}

hope, it will be useful for someone
#11
12/28/2010 (9:19 pm)
huh, now I need advice : ) submitted scores are presented if to launch GameCenter from iphone's desktop icon, but the default leaderboard is empty when I call it from game :(
#12
12/29/2010 (1:26 am)
Hi, yes, I am experiencing an empty leaderboard too. In the Game Center app, my leaderboard shows up just fine with reported scores and everything. Losing hair on this. :) Anyone have any advice for when the leaderboard in game is empty but just fine in the game center app itself?

Thanks, and REALLY appreciated the code post.
#13
12/29/2010 (3:41 am)
Got it:

- (void)showLeaderboard
{
if(!isAuthenticated)
return;

GKLeaderboardViewController *leaderboardController = [[GKLeaderboardViewController alloc] init];
if (leaderboardController != nil) {
leaderboardController.timeScope = GKLeaderboardTimeScopeAllTime;
leaderboardController.category = @"HighScores";
leaderboardController.leaderboardDelegate = self;

tempGameCenterVC = [[UIViewController alloc] init];
UIWindow* window = [UIApplication sharedApplication].keyWindow;

[window addSubview: tempGameCenterVC.view];

[tempGameCenterVC presentModalViewController:leaderboardController animated:YES];
}
}

- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController
{
[tempGameCenterVC dismissModalViewControllerAnimated:YES];
[tempGameCenterVC.view.superview removeFromSuperview];
[tempGameCenterVC release];
}
#14
12/29/2010 (6:32 am)
Lowell, I'm getting this when I try to replicate your fix (I am having the same issue).

'tempGameCenterVC' was not declared in this scope

#15
12/30/2010 (2:17 am)
Nevermind just had to define it in the header.
#16
12/31/2010 (7:29 am)
Keith, where/how did you define it in the header? (Sorry if this should be obvious; I know next to nothing about Obj-C.)
#17
12/31/2010 (3:58 pm)
Sorry for my silence, I just updated my original post to include the view controller. I tested by showing the achievements, not showing the leaderboard so somebody please let me know if that method doesn't work. They are basically the same though so I think it'll work.

Lowell, your solution will work except your leaking memory every time leaderboardViewControllerDidFinish is called (notice [[GKLeaderboardViewController alloc] init];).
#18
01/01/2011 (4:43 pm)
This is great Justin. Consider one more War Evolved purchased. Would like to buy it more... well you deserve more than 99 cents for this. Cheers.
#19
01/02/2011 (3:09 am)
Thanks Rennie!
#20
01/05/2011 (6:17 pm)
a function how to "get" highscores with players' names from leaderboard (with scripts interaction)? - and we are done : )

btw I had issues, that after closing leaderboard I wasn't able to click anything in game (probably because something invisible overlaped screen : )

anyway changing this:
[gameCenterViewController dismissModalViewControllerAnimated:YES];
to this:
[self dismissModalViewControllerAnimated:YES];
helped. But it cause not appearing leaderboard anymore - seems its deleting root-obj...hm.. time to research more...

have someone issues that app is freezing when you're closing leaderboard screen ?

Page «Previous 1 2 3 4 Last »