Saturday, April 19, 2014

Integrating Google Play Game Services into Cocos2D-X

I’m making a few mobile games, and finally decided it was time to move to a cross-platform engine. One piece that seemed daunting, and didn’t have a TON of documentation, was integrating a Cocos2D-X game with the Google Play Game Services APIs. GPGS has some great features, like cloud saves, achievements, and leaderboards, and I wanted those.

This is by no means an “expert” opinion, and I fully acknowledge that I use hacky/subpar code in places. Feel free to disagree with my design decisions, etc. This is less a “this is how it’s done” and more of a “this worked for me” article. Coder beware, and all that.

I used Cocos2D-X 2.2.3, the current non-beta version. I have no idea what might be different if you use 3.x, or any other version.

I also borrowed some from the Avalon game center example (link below the post). I kept the “Game Center” name, but little of the code beyond the concept and some JNI helpers.

I didn’t do cloud saves for this one, only Achievements and Leaderboards. If I do the cloud saves later, I’ll post an update.

I initially started looking at the C++ library available for GPGS, and decided that was a mistake. While it certainly CAN be done that way, you lose some of the handier integrations with iOS and Android for things like launching the popups/activites for viewing achievements and leaderboards. The solution I used, at a high level, looks like this:

  • A header file (GameCenter.h) that both Android and iOS see.
  • A .mm file (GameCenter.mm) stored outside the main Classes folder structure, so I can make it so only Xcode can see it.
  • A .cpp file (GameCenter.cpp) stored in the Classes folder structure, but only included in Eclipse.

...you can see where this is going, I hope. Both the .mm and the .cpp file implement the header contract. The .mm file points at the obj-C functionality, while the .cpp file uses JNI to pass code to GameCenter.java static methods.

While I know it’s complex, I’m going to skip over the initial setup of GPGS. Follow the guidelines Google publishes for adding GPGS to your iOS and Android projects. A couple tips when using Cocos2D-X:

  • Grab the BaseGameActivity (and the related classes and helpers) from the Android sample. Set your main activity to inherit from it, and change BaseGameActivity to inherit from Cocos2dxActivity. I know it’s not a FragmentActivity... that doesn’t seem to cause issues.
  • Use the RootViewController in Xcode. For me, the viewDidLoad stuff wouldn’t fire when launching a Cocos2D-X project, so I put my initial launch code in initWithNibName.
  • I used a static reference to my Activity to launch the GPGS methods from my static java Game Center. Yes, I know this is leaky and bad.

Without any further ado, the code:

GameCenter.h
 #ifndef __XXXXX__GameCenter__  
 #define __XXXXX__GameCenter__  
 class GameCenter  
 {  
 public:  
 void showAchievements();  
 void unlockAchievement(const char* achievementId);  
 void showLeaderboard(const char* leaderboardId);  
 void postToLeaderboard(const char* leaderboardId, int score);  
 bool isSignedIn();  
 void signIn();  
 };  
 #endif /* defined(__XXXXX__GameCenter__) */  

GameCenter.mm
 #include "GameCenter.h"   
 #include "RootViewController.h"   
 bool GameCenter::isSignedIn()   
 {   
 return [(RootViewController*)[[[UIApplication sharedApplication] keyWindow] rootViewController] isSignedIn];   
 }   
 void GameCenter::signIn()   
 {   
 [(RootViewController*)[[[UIApplication sharedApplication] keyWindow] rootViewController] startGoogleGamesSignIn];   
 }   
 void GameCenter::showAchievements()   
 {   
 [(RootViewController*)[[[UIApplication sharedApplication] keyWindow] rootViewController] showAchievements];   
 }   
 void GameCenter::unlockAchievement(const char* achievementId)   
 {   
 [(RootViewController*)[[[UIApplication sharedApplication] keyWindow] rootViewController] unlockAchievement: [NSString stringWithUTF8String:achievementId]];   
 }   
 void GameCenter::showLeaderboard(const char* leaderboardId)   
 {   
 [(RootViewController*)[[[UIApplication sharedApplication] keyWindow] rootViewController] showLeaderboard: [NSString stringWithUTF8String:leaderboardId]];   
 }   
 void GameCenter::postToLeaderboard(const char* leaderboardId, int score)   
 {   
 [(RootViewController*)[[[UIApplication sharedApplication] keyWindow] rootViewController] postToLeaderboard: [NSString stringWithUTF8String:leaderboardId] withScore:score];   
 }  

GameCenter.cpp
 #include "GameCenter.h"  
 #include "cocos2d.h"  
 #include "platform/android/jni/JniHelper.h"  
 #include <jni.h>  
 namespace helper {  
 namespace gamecenter {  
 const char* const CLASS_NAME = "com/bloodhoundstudios/gridinfect/GameCenter";  
 void callStaticVoidMethod(const char* name)  
 {  
   cocos2d::JniMethodInfo t;  
   if (cocos2d::JniHelper::getStaticMethodInfo(t, CLASS_NAME, name, "()V")) {  
     t.env->CallStaticVoidMethod(t.classID, t.methodID);  
     t.env->DeleteLocalRef(t.classID);  
   }  
 }  
 bool callStaticBoolMethod(const char* name)  
 {  
   cocos2d::JniMethodInfo t;  
   if (cocos2d::JniHelper::getStaticMethodInfo(t, CLASS_NAME, name, "()Z")) {  
     bool result = (t.env->CallStaticBooleanMethod(t.classID, t.methodID) == JNI_TRUE);  
     t.env->DeleteLocalRef(t.classID);  
     return result;  
   } else {  
     return false;  
   }  
 }  
 void callStaticVoidMethodWithString(const char* name, const char* idName)  
 {  
   cocos2d::JniMethodInfo t;  
   if (cocos2d::JniHelper::getStaticMethodInfo(t, CLASS_NAME, name, "(Ljava/lang/String;)V")) {  
     jstring jIdName = t.env->NewStringUTF(idName);  
     t.env->CallStaticVoidMethod(t.classID, t.methodID, jIdName);  
     t.env->DeleteLocalRef(t.classID);  
     t.env->DeleteLocalRef(jIdName);  
   }  
 }  
 void callStaticVoidMethodWithStringAndInt(const char* name, const char* idName, const int score)  
 {  
   cocos2d::JniMethodInfo t;  
   if (cocos2d::JniHelper::getStaticMethodInfo(t, CLASS_NAME, name, "(Ljava/lang/String;I)V")) {  
     jstring jIdName = t.env->NewStringUTF(idName);  
     t.env->CallStaticVoidMethod(t.classID, t.methodID, jIdName, (jint)score);  
     t.env->DeleteLocalRef(t.classID);  
     t.env->DeleteLocalRef(jIdName);  
   }  
 }  
 } // namespace gamecenter  
 } // namespace helper  
 bool GameCenter::isSignedIn()  
 {  
 try  
 {  
 return helper::gamecenter::callStaticBoolMethod("isSignedIn");  
 }  
 catch (std::exception& e)  
 {  
 }  
 return false;  
 }  
 void GameCenter::signIn()  
 {  
 try  
 {  
 helper::gamecenter::callStaticVoidMethod("signIn");  
 }  
 catch (std::exception& e)  
 {  
 }  
 }  
 void GameCenter::showAchievements()  
 {  
 try  
 {  
 helper::gamecenter::callStaticVoidMethod("showAchievements");  
 }  
 catch (std::exception& e)  
 {  
 }  
 }  
 void GameCenter::unlockAchievement(const char* achievementId)  
 {  
 try  
 {  
 helper::gamecenter::callStaticVoidMethodWithString("unlockAchievement", achievementId);  
 }  
 catch (std::exception& e)  
 {  
 }  
 }  
 void GameCenter::showLeaderboard(const char* leaderboardId)  
 {  
 try  
 {  
 helper::gamecenter::callStaticVoidMethodWithString("showLeaderboard", leaderboardId);  
 }  
 catch (std::exception& e)  
 {  
 }  
 }  
 void GameCenter::postToLeaderboard(const char* leaderboardId, int score)  
 {  
 try  
 {  
 helper::gamecenter::callStaticVoidMethodWithStringAndInt("postToLeaderboard", leaderboardId, score);  
 }  
 catch (std::exception& e)  
 {  
 }  
 }  

RootViewController.mm (the relevant parts)
 #import "RootViewController.h"   
 #import <GooglePlayGames/GooglePlayGames.h>   
 @implementation RootViewController   
 static NSString * const kDeclinedGooglePreviously = @"UserDidDeclineGoogleSignIn";   
 static NSInteger const kErrorCodeFromUserDecliningSignIn = -1;   
 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {   
 if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) {   
 // Custom initialization   
 GPPSignIn *signIn = [GPPSignIn sharedInstance];   
 signIn.clientID = kClientID;   
 signIn.scopes = [NSArray arrayWithObjects:   
 @"https://www.googleapis.com/auth/games",   
 @"https://www.googleapis.com/auth/appstate",   
 nil];   
 signIn.language = [[NSLocale preferredLanguages] objectAtIndex:0];   
 signIn.delegate = self;   
 signIn.shouldFetchGoogleUserID =YES;   
 self.currentlySigningIn = [[GPPSignIn sharedInstance] trySilentAuthentication];   
 }   
 return self;   
 }   
 (...)   
 -(void)finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error   
 {   
 self.currentlySigningIn = NO;   
 if (error.code == 0 && auth) {   
 NSLog(@"Success signing in to Google! Auth is %@", auth);   
 [self startGoogleGamesSignIn];   
 } else {   
 NSLog(@"Failed to log into Google\n\tError=%@\n\tAuthObj=%@", [error localizedDescription],   
 auth);   
 if ([error code] == kErrorCodeFromUserDecliningSignIn) {   
 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kDeclinedGooglePreviously];   
 [[NSUserDefaults standardUserDefaults] synchronize];   
 }   
 }   
 }   
 -(bool)isSignedIn   
 {   
 return self.currentlySigningIn || [[GPGManager sharedInstance] isSignedIn];   
 }   
 -(void)startGoogleGamesSignIn   
 {   
 if ([[GPGManager sharedInstance] isSignedIn]) {   
 [[GPGManager sharedInstance] signOut];   
 } else {   
 [[GPGManager sharedInstance] signIn:[GPPSignIn sharedInstance]   
 reauthorizeHandler:^(BOOL requiresKeychainWipe, NSError *error)   
 {   
 if (requiresKeychainWipe)   
 {   
 [[GPPSignIn sharedInstance] signOut];   
 }   
 [[GPPSignIn sharedInstance] authenticate];   
 }];   
 }   
 }   
 - (void)showAchievements {   
 GPGAchievementController *achController = [[GPGAchievementController alloc] init];   
 achController.achievementDelegate = self;   
 [self presentViewController:achController animated:YES completion:nil];   
 }   
 - (void)achievementViewControllerDidFinish:(GPGAchievementController *)viewController {   
 [self dismissModalViewControllerAnimated:YES];   
 }   
 - (void)unlockAchievement:(NSString*)achievementId {   
 GPGAchievement *unlockMe = [GPGAchievement achievementWithId:achievementId];   
 [unlockMe unlockAchievementWithCompletionHandler:^(BOOL newlyUnlocked, NSError *error)   
 {   
 if (error)   
 {   
 }   
 else if (!newlyUnlocked)   
 {   
 }   
 else   
 {   
 NSLog(@"Hooray! Achievement unlocked!");   
 }   
 }];   
 }   
 - (void)showLeaderboard:(NSString*)leaderboardId {   
 GPGLeaderboardController *leadController = [[GPGLeaderboardController alloc] initWithLeaderboardId:leaderboardId];   
 leadController.leaderboardDelegate = self;   
 [self presentViewController:leadController animated:YES completion:nil];   
 }   
 - (void)leaderboardViewControllerDidFinish:(GPGLeaderboardController *)viewController {   
 [self dismissModalViewControllerAnimated:YES];   
 }   
 - (void)postToLeaderboard:(NSString*)leaderboardId withScore:(int)score {   
 GPGScore *myScore = [[GPGScore alloc] initWithLeaderboardId:leaderboardId];   
 myScore.value = score;   
 [myScore submitScoreWithCompletionHandler: ^(GPGScoreReport *report, NSError *error)   
 {   
 if (error)   
 {   
 }   
 else   
 {   
 }   
 }];   
 }   
 @end  

GameCenter.java (the toast doesn't work, I just hadn't pulled it yet)
 import android.widget.Toast;  
 import com.google.android.gms.games.Games;  
 public class GameCenter {  
   static void signIn() {  
    try {  
    if (XXXXX.Instance != null) {  
     if (!XXXXX.Instance.isSignedIn()) {  
     XXXXX.Instance.beginUserInitiatedSignIn();  
     } else {  
     XXXXX.Instance.signOut();  
     Toast.makeText(XXXXX.Instance, "Signed out", Toast.LENGTH_SHORT).show();  
     }  
    }  
    } catch (Exception ex) {  
    }  
   }  
  static void showAchievements() {  
  try {  
    if (XXXXX.Instance != null) {  
     if (XXXXX.Instance.isSignedIn()) {  
     XXXXX.Instance.startActivityForResult(  
      Games.Achievements.getAchievementsIntent(  
       XXXXX.Instance.getApiClient()),   
       41267);  
     }  
    }  
    } catch (Exception ex) {  
    }  
  }  
   static void unlockAchievement(String achievementId) {  
    try {  
    if (XXXXX.Instance != null) {  
     if (XXXXX.Instance.isSignedIn()) {  
     Games.Achievements.unlock(XXXXX.Instance.getApiClient(), achievementId);  
     }  
    }  
    } catch (Exception ex) {  
    }  
   }  
   static void showLeaderboard(String leaderboardId) {  
  try {  
    if (XXXXX.Instance != null) {  
     if (XXXXX.Instance.isSignedIn()) {  
     XXXXX.Instance.startActivityForResult(  
      Games.Leaderboards.getLeaderboardIntent(  
       XXXXX.Instance.getApiClient(), leaderboardId),  
       76789);  
     }  
    }  
    } catch (Exception ex) {  
    }  
   }  
   static void postToLeaderboard(String leaderboardId, int score) {  
  try {  
    if (XXXXX.Instance != null) {  
     if (XXXXX.Instance.isSignedIn()) {  
     Games.Leaderboards.submitScore(XXXXX.Instance.getApiClient(), leaderboardId, score);  
     }  
    }  
    } catch (Exception ex) {  
    }  
   }  
 }  

That should be enough to get you going! Happy Coding!
Google Play Game Services
Cocos2D-X
Avalon (GitHub)

No comments:

Post a Comment