About
My game project Ghost Jump Go! on iOS shall offer access to Apple’s Game Center features. As my game is written in Javascript and deployed using Cordova I like to share how I integrated a plugin to bring Game Center into my game.
The goal
Game Center from a player’s perspective is a service that allows the player to sign in, enter scores in a shared leaderboard, collect achievements and share all that with friends.
For Ghost Jump Go!’s I like to have at least one leaderboard so the players can beat each others scores online.
Cordova plugin
Cordova does not directly support the usage of Game Center but there are many Game Center plugins available on GitHub and NPM.
To access the Game Center a native app needs Apple’s GameKit Framework. The API offers many features but for my game I only need access to the following parts:
- Game Center sign in
- Leaderboard API and UI
- Achievements API and UI
Searching NPM and GitHub I found this one: https://github.com/leecrossley/cordova-plugin-game-center. It offers most features I need but when looking at the last commit date I’m a bit worried: 2015
means it is five years old. The big question now is - does this still work with Apple’s current iOS?
In such a case I use one of GitHubs best features: GitHub Insights. If we open the network view for Lee’s plugin project we can see if and when it was forked by other developers. If you are lucky you can find other developers forking the plugin and continue working on the project.
Looking at that graph we can see that Lee stopped working on the plugin 2015 but others continue to modify the code. Knowing that it is a pretty safe bet to expect that the plugin still works to some degree. In the next step I inspect the commits of other devs and see if there are any drastic changes which would imply that Apple’s post 2015 iOS’ are somehow incompatible. Luckily I only found minor changes and most of them were just simple additions. I inspected the graph and decided to fork from github: DiRaiks/cordova-plugin-game-center.
DiRaiks commits aren’t visible in the screenshot. I forked his project and now the graph shows my name instead of his for some reason (see screenshot: CSchnackenberg). Luckily inspecting the commit history it becomes clear again who did what.
I forked DiRaiks’ plugin and merged this commit from maximo72. DiRaiks’ version of the plugin adds a function to check if the player is authenticated. It also allows to query some user data. maximo72 commit adds the function getScore()
.
Please keep in mind that I won’t maintain the plugin and cannot give any support or take care of issue requests.
Inspecting the plugin API
I am, by no means, a Cordova plugin expert. I also never learn to enjoy Objective-C. Keep that in mind when browsing through my commits.
Looking at the file-list it looks simple enough. We have three files:
www/gamecenter.js
ios/GameCenter.h
ios/GameCenter.m
Script side
The script file gamecenter.js
is the API definition of the plugin. It is the script-facing part. The other two files are the native-facing part and do the actual work and communicate with GameKit.
Taking a look at the API we see the plugin already has all the good stuff. We can use auth
to initialize the connection of the game with the Game Center. Using isLogedIn
I can fine tune the in-game UI. Things like showLeaderboard
and submitScore
should cover everything we need for the initial release.
If we take a look at the function signature we can see a pattern and deduce certain information. First we can assume that everything is aync because the parameter names success and failure imply that. It also makes a lot of sense. Some functions also have a data parameter. That is a good indicator for that those functions support parameters while the others don’t.
Let’s pick some examples from the docs (README.md
):
var successCallback = function (user) {
alert(user.alias);
// user.alias, user.playerID, user.displayName
};
gamecenter.auth(successCallback, failureCallback);
var data = {
score: 10,
leaderboardId: "board1"
};
gamecenter.submitScore(successCallback, failureCallback, data);
So, yes…
- ✅ async
- ✅ data is the container for parameters
And while we’re on that topic let’s dig deeper. On the script-facing side Cordova plugins have access to the function exec
which is key when it comes to the communication with the native-side.
In (a) we have the first example of the exec
usage. What it says is basically: call the function auth
on the native object GameCenter
. To have a checker if this has been done it stores the variable _loggedin
in the script-facing state and allows quick access to that through isLoggedIn
(see (c)). In (a) we can also see how the result value is passed through to the success callback. Both functions shown in (b) are simple function calls with no parameters. They both simply pass through the callbacks and let the Cordova plugin API handle the parameters.
Native header
Let’s have a look at the native part.
Here the native GameCenter is defined. My Objective-C understanding is limited but what I understand is that the functions between @interface
and @end
are connected to the term GameCenter
. Asking around in the community and reading into the docs it turned out the class GameCenter inherits from the class CDVPlugin
and implements the interface GKGameCenterControllerDelegate
(though in objective-c it is called a protocol
not interface). All functions written here are available through the exec
function on the script-side.
The implementation happens in the GameCenter.m
file. Being very long let’s just have a look at the auth
function.
Native implementation
In this file we have access to both worlds. We can read data provided by the script-side and have full access to the native SDK/Framework. In the given code the most important lines are the one concerned with CDVPluginResult
. They are kind of the return
value of the call and they can either return with CDVCommandStatus_OK
or CDVCommandStatus_ERROR
attached with a message. The message is either a dictionary
or a string
. That message is provided to the callback function on script side. If the native function results with a CDVCommandStatus_OK
the Cordova plugin API will invoke the success-handler on script side. In case of CDVCommandStatus_ERROR
the failure handler will be invoked.
For example if a native implementation concludes like this:
// simplified
- (void) someFunc:(CDVInvokedUrlCommand*)command;
{
CDVPluginResult* pluginResult = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK
messageAsString:"abc"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
In our application we would receive this:
window.cordova.plugin.someFunc(
(result) => {
console.log(result)
// => "abc"
},
(err) => {
}
);
That means if we really want to understand how exactly the plugin-functions answer we need to inspect the m-file and check for all result calls. This is especially important as we can hardly test all cases beforehand. It is also good if we like to merge commits from other forks of the project. Not all of them updated their README.md
.
Now that I figured that all out let me do the integration itself.
Integration of Game Center
Writing this in 2020 I need to mention that Apple is updating the Game Center a lot. All the details can be found here at wwdc2020. They added Game Center features to the App Store app directly. For example when opening an App in App store one can see a list with friends playing that game already. Within the running game they offer a new Game Center Access Point which can be placed by the game in one of the four device corners. That serves as an entry point for players. Also they added support for recurring leaderboards and redesigned the interface.
All this is obviously not yet part of the plugin and while writing this iOS 14 is not yet a thing. However, it’s good to know what is coming and once iOS 14 has settled down a bit some I might integrate some of the features into the plugin.
For now I only integrate the well known Leaderboard feature of iOS 13.
Authentication
To enable Game Center I first need to make sure that it’s enabled in XCode and also on App Store Connect. Once enabled all parties involved are informed and Apple’s server should give my game access to the Game Center services. The details can be found here.
Within the game the first thing I need to do is to call the auth
function. It internally uses the GameKit API to communicate with Apple’s game servers. At this point iOS’s Game Center UI takes over and my game has to respond to the events provided by it. If the user is not yet signed in it should prompt the sign-in UI. Eventually I have to deal with the following results depending on the players behavior:
- The player already signed in and happily uses the Game Center
- The player decides to signed in and uses the Game Center
- The player failed to sign in or decided against it
- The player disabled the Game Center
- An error occurred during sign in
Unfortunately, the states are not reflected like this in the plugin-API. The way I understand the auth
function code it responds like this:
- (1) success handler, with the player data
- (2) error handler, with a string containing a localized error description
- (3) error handler, with the string
already set
Case (1) is pretty obvious. If (2) happens I assume it’s something like a real error like no: internet connection or Game Center disabled. In all cases it basically means that I do not need to display any Game Center UI. For me case (3) is a strange API decision. The way I read the code it’s not really an error but instead it might mean the player was already authenticated. Me personally would expect that not to be an error but a success. But it’s not 100% clear when this is called and might be only an issue if the app itself calls auth
more than once.
Combined it means: in case of (1) and in case of (3) I can expect the player to be connected with the Game Center.
Hm… but if we go up and look at the implementation of isLoggedIn
and auth
in gamecenter.js
it would suggest that case (3) does not mean the user is logged in. But maybe it’s just a quick hack by some of the plugin contributors to just have a quick look up variable and this detail has been missed.
An alternative would be to use the checkAuth
method. It returns true
if the player is already authenticated. We could do e.g. only call auth
if the player is not yet authenticated. However, the way I understand the docs it’s better to always call auth
to make sure GKLocalPlayer.localPlayer.authenticateHandler
is called. I think that also defines when/how the Game Center banner at the top is presented.
The way I read the plugin code my approach is this:
- Call
auth
on App start - In both handlers (success and error) call
checkAuth
- Store the result of
checkAuth
in a variable:gameCenterAvailable
- Act on that variable in the rest of the app session
This should be solid in all of the given cases. The only thing I miss is the player-data. But if needed I could just call getUserData()
.
Create a Leaderbaord
In my first release I intend to support a leaderboard. When a player in my game ends a level session the score will be submitted.
The Game Center service allows me to create multiple Leaderboards for a single game. That way I could count different aspects of the players progress. For each Leaderboard I define an ID. That ID is a simple string I can use in my game to reference the given board.
For Ghost Jump Go! there are three values which are good candidates for a Leaderboard:
- floor level: what was the highest floor level the player reached
- combo chain: you can build up a combo chain. The longer the better
- score: the general score you get while playing
I’m not yet sure if I create a board for all of them. Score, however, is a no brainer. The score is an integer
value and I configured the Appstore Connect configuration in a meaningful way.
With the upcoming release of iOS 14 I already have the option to create a recurring Leaderboard. I could have a weekly changing Leaderbaord and given the event of many active players it’s something I might consider. If I understand the wwdc2020 video correctly the oldish API I use should already be compatible with it. But thats a topic for later.
Integrate the plugin
Now, having the Leaderboard created on Appstore connect, I need to go over to the source code and write some code to submit the score and show the leaderbaord.
As mentioned above I created my own fork of the plugin. To add a Cordova plugin from a git-project I only need to do one line in the project directory. Note that my repo uses develop
as main branch and differs from the forked repo.
cordova plugin add --save https://github.com/CSchnackenberg/cordova-plugin-game-center.git#develop
Without paste my entire game code here let me share the auth-part.
export class MyGameCenter {
// {...} variables, constructor and other functions
supportedPlatform():boolean {
return (window.cordova && window.gamecenter) ? true : false;
}
isAuthenticated():boolean {
return this.authenticated;
}
public initialize():void {
if (!this.supportedPlatform())
return;
console.log("MyGameCenter::initialize()");
window.gamecenter.auth(
() => {
this._checkAuthAfterInit();
}, (err:string) => {
this._checkAuthAfterInit(err);
}
);
}
private _checkAuthAfterInit(recentError?:string):void {
console.log("MyGameCenter::_checkAuthAfterInit()");
window.gamecenter.checkAuth(
(authState:boolean) => {
this.authenticated = authState;
console.log("MyGameCenter::checkAuth result:", authState);
}, (err) => {
if (recentError) {
console.error("Failed to authenticate GameCenter. Reason 1:", recentError, ". Reason 2:", err);
}
else {
console.error("Failed to authenticate GameCenter:", err);
}
}
);
}
public submitToLeaderboard(boardID:string, scoreValue:number):void {
if (!this.isAuthenticated())
return;
console.log("MyGameCenter::submitToLeaderboard()");
window.gamecenter.submitScore(
() => {
console.log("MyGameCenter::submitToLeaderboard() ok", scoreValue);
}, err => {
console.error("MyGameCenter::submitToLeaderboard() failed:", err);
}, {
score: scoreValue,
leaderboardId: boardID,
}
);
}
public showLeaderbaord(boardID):void {
if (!this.isAuthenticated())
return;
console.log("MyGameCenter::showLeaderbaord()");
window.gamecenter.showLeaderboard(
() => {
console.log("MyGameCenter::showLeaderboard() ok");
}, (err) => {
console.log("MyGameCenter::showLeaderboard() failed:", err);
}, {
leaderboardId: boardID,
}
);
}
}
So, hopefully this combination of auth
and checkAuth
will cover all edge-cases and make solid use of the plugin API. I removed the feedback part in the example to pack it a bit here. Using submitToLeaderboard
I should be able to send the score. Finally using showLeaderbaord
I can give the player access to the leaderbaord UI.
If anything goes wrong simply nothing happens. This is intentional as it is not common in games to notify the player if the game center fails. Also note the console.log stuff is removed during production-build process.
I can also recommend having a look at Apple Design Resources to get your hands on a proper icon. On the bottom of the page I found an extra *.dmg
for Game Center. With that I was able to setup something for my game.
Test on device
Before the test I made sure that the following things are in place:
- ✅ Game Center added as capability in the Xcode project
- ✅ App Store Connect ➡️ Features ➡️ Game Center ➡️ Leaderbaord created with ID
- ✅ App Store Connect ➡️ App Store ➡️ iOS-App (version) ➡️ Game Center ➡️ Leaderboard with ID is added
- ✅ The Cordova plugin is activated and
auth
andsubmitScore
are actually called - ✅ The Game code uses the exact leaderbaord ID I generated
It seems not necessary to have the leaderbaord flagged as live
and the App is not required to be released.
Well… what can I say: it worked. The Game Center starts the moment I call auth
and from that on the score is transmitted to the server. The isAuthenticated
function works as expected and when calling showLeaderbaord
the native UI opens. All this without issues on iOS SDK 13.4, while targeting iOS 11.
Once iOS 14 is released and once Cordova signaled compatibility I really hope to write a followup and improve the plugins so I can provide support for the new Access Point feature of Game Center in my game.
Final thoughts
I showed how one can pick up an oldish Cordova plugin and how find more recent contributions to a project. Also I hope it shows that one can quickly update seemingly outdated plugins and make use of existing work.
Let me know what you think @xtoffsgamestuff or via cschnack@gmx.de