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
RootViewController.mm (the relevant parts)
GameCenter.java (the toast doesn't work, I just hadn't pulled it yet)
That should be enough to get you going! Happy Coding!
Google Play Game Services
Cocos2D-X
Avalon (GitHub)
#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)