CocosCreator communication with iOS GameCenter
2019年5月5日 更新
开启更多功能,提升办公效能

#0 Introduction


Cocos2dx has already created a layer of C++ codes which

- are called by Java/OC when certain app lifecycle events should be coped with in game, e.g. appWillEnterBackground,

- can call methods provided in JavaByteCodes(.class)/ObjcFramework.


In order to do IAP and GameCentre related development for iOS in JS, we'll be using the 2nd feature to call an existing Objc method which contains a param of a "CompletionHandlerType", by "properly associating type systems in C++ & Objc", to be specific, the important problem is just "how to represent an Objc completion handler in C++".


To practice, the only good reference by 2018-12-24 is provided by this CocosCreator manual, which suggests that among the two directions

  • "#1 JavaScript calls C++ function", and
  • "#2 C++ calls JavaScript function"

, we should try #1 first.


#1 JavaScript calls C++ function

#1.1 Writes the C++ function to be called

What is NOT mentioned explicitly by the CocosCreator manual for JSB2.0 is that such C++ functions should be "static without any namespace/class", that is, like a Clang built static/shared lib, e.g. a "*.a"/"*.so" file for Unix.

bool foo(se::State& s)
{
...
...
}
SE_BIND_FUNC(foo) // Binding a JS function as an example


#1.2 Specifying the namespace/class for encapsulation

However in real applications it's terrible to not use any namespace/class, therefore the "static functions without any namespace/class" are often wrapped in a so called "bridge file", which calls the underlying namespace/class rich implementations.

#ifndef invitation_bridge_hpp
#define invitation_bridge_hpp

#pragma once
#include "base/ccConfig.h"
#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"

extern se::Object* __jsb_MyGame_Invitation_proto;
extern se::Class* __jsb_MyGame_Invitation_class;

bool register_MyGame_Invitation(se::Object* obj);

SE_DECLARE_FUNC(invitation_send);

#endif /* invitation_bridge_hpp */


/*
invitation_bridge.cpp
*/
#include "invitation_bridge.hpp"
#include "invitation.hpp"
#include "base/ccMacros.h"
#include "scripting/js-bindings/manual/jsb_conversions.hpp"

bool invitation_send(se::State& s)
{
    const auto& args = s.args();
    size_t argc = args.size();
    CC_UNUSED bool ok = true;

    if (argc == 2) {
        // Hereby assume that the expected argc is 2 and that "typeof(args[0]) == 'int' && typeof(args[1]) == 'string'.
        int playerId = 0;
        std::string msg;
        ok &= seval_to_int32(args[0], &playerId);
        ok &= seval_to_std_string(args[1], &msg);
        SE_PRECONDITION2(ok, false, "invitation_send : Error processing arguments");
        MyGame::Invitation::send(playerId, msg);
        return true;
    }
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 2);
    return false;
}
SE_BIND_FUNC(invitation_send)

static bool invitation_finalize(se::State& s)
{
    CCLOGINFO("jsbindings: finalizing JS object %p (MyGame::Invitation)", s.nativeThisObject());

    auto iter = se::NonRefNativePtrCreatedByCtorMap::find(s.nativeThisObject());

    if (iter != se::NonRefNativePtrCreatedByCtorMap::end()) {
        se::NonRefNativePtrCreatedByCtorMap::erase(iter);
        MyGame::Invitation* cobj = (MyGame::Invitation*)s.nativeThisObject();
        delete cobj;
    }
    return true;
}
SE_BIND_FINALIZE_FUNC(invitation_finalize)


#ifndef invitation_hpp
#define invitation_hpp

#include <string>

namespace MyGame {
    class Invitation {
    public:
        static bool send(int playerId, std::string msg);
static bool MyGame::Invitation::onGameCenterIdentityObtained(char const * const playerId, char const * const publicKeyUrl, char const * const signature, char const * const salt, uint64_t timestamp, void* error); // Don't read this function now!
    };
}

#endif /* invitation_hpp */


/*
invitation.cpp
*/

#include "invitation.hpp"
#include "base/ccMacros.h"
#include <stdio.h>

bool MyGame::Invitation::send(int playerId, std::string msg) {
    printf("MyGame::Invitation::send, to playerId %d with msg %s", playerId, msg.c_str());
    return true;
}

bool MyGame::Invitation::onGameCenterIdentityObtained(char const * const playerId, char const * const publicKeyUrl, char const * const signature, char const * const salt, uint64_t timestamp, void* error) {
// Don't read this function now!
    printf("playerId: %s\npublicKeyUrl: %s\nsignature(b64encoded): %s\nsalt(b64encoded): %s\ntimestamp: %llu\n", playerId, publicKeyUrl, signature, salt, timestamp);

    // Params of type "char const * const" don't need be freed.
    // TODO: Invoke a specified JS global function.
    return true;
}


You might wonder how the underlying "SE_DECLARE_FUNC" and "SE_BIND_FUNC" work. When "invitation_send" is first declared in "invitation_bridge.hpp", you actually only declares the function named "invitation_send##Registry(...)", whose param list is shown in detail below.


That said, when the JavaScript codes try to invoke "invitation_send". it actually invokes "invitation_send##Registry(...)" which will convert the long list of params into a single "se:State& s" param for you to handle in that simplified function "invitation_send(se:State& s)" which is only declard and defined in "invitation_bridge.cpp", i.e. unseen by the JavaScript codes. The convertion part is highlighted below.


#1.3 Registering the "C++ registry functions" to the "JavaScript engine _globalObject"


The "C++ registry functions" are not yet recognized by the "JavaScript engine", until we invoke "register_MyGame_Invitation()".


/*
invitation_bridge.cpp
*/

se::Object* __jsb_MyGame_Invitation_proto = nullptr;
se::Class* __jsb_MyGame_Invitation_class = nullptr;
bool register_MyGame_Invitation(se::Object* obj)
{
    // Get the ns
    se::Value nsVal;
    if (!obj->getProperty("MyGame", &nsVal))
    {
        se::HandleObject jsobj(se::Object::createPlainObject());
        nsVal.setObject(jsobj);
        obj->setProperty("MyGame", nsVal);
    }

    se::Object* ns = nsVal.toObject();
    auto cls = se::Class::create("Invitation", ns, nullptr, nullptr);

    /*
     A call in JavaScript codes "MyGame.Invitation.send" will call "invitation_send##Registry" declared in "invitation_bridge.h", see the screenshot of MACROs above.
     */

    cls->defineStaticFunction("send", _SE(invitation_send));
    cls->defineFinalizeFunction(_SE(invitation_finalize));
    cls->install();

    JSBClassType::registerClass<MyGame::Invitation>(cls);
    __jsb_MyGame_Invitation_proto = cls->getProto();
    __jsb_MyGame_Invitation_class = cls;
    se::ScriptEngine::getInstance()->clearException();
    return true;
}


This "register_MyGame_Invitation" will finally be called with param "ScriptEngine::getInstance()->_globalObj", but with CocosCreator JSB2.0 you don't directly relate them together, instead you can use "ScriptEngine::getInstance()->addRegisterCallback(register_MyGame_Invitation)" to accomplish the job.


/*
AppDelegate.cpp
*/
#include "invitation_bridge.hpp"

...
    se->addRegisterCallback(register_MyGame_Invitation);
se->start();
...

#1.4 Trying out the invocation in JavaScript


#1.5 Creating an OC-C++ bridge to allow C++ calling OC classes/methods

Add a method declaration in "invitation.hpp" to be implemented in OC.

#ifndef invitation_hpp
#define invitation_hpp

#include <string>

namespace MyGame {
    class Invitation {
    public:
        static bool send(int playerId, std::string msg);
        /**
         * The implementation of the following method "authenticateGameCenter" will be finally carried out in "InvitationOC.mm".
         */
        static bool authenticateGameCenter(void* aParam);
static bool MyGame::Invitation::onGameCenterIdentityObtained(char const * const playerId, char const * const publicKeyUrl, char const * const signature, char const * const salt, uint64_t timestamp, void* error); // Don't read this function now!
    };
}

#endif


Just keep "invitation.cpp" untouched.


Add the following 2 OC files into "Classes".

/*
InvitationOC.h
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#import "invitation.hpp"

NS_ASSUME_NONNULL_BEGIN

@interface InvitationOC : NSObject
- (bool) initGameCenterLocalPlayer:(void *) aParameter;
@end

NS_ASSUME_NONNULL_END


/*
Invitation.mm
*/

#import "InvitationOC.h"

@implementation InvitationOC

+ (InvitationOC*)sharedInstance
{
    static InvitationOC *sharedInstance;
    @synchronized(self)
    {
        if (!sharedInstance) {
            sharedInstance = [[InvitationOC alloc] init];
        }
        return sharedInstance;
    }
}

bool MyGame::Invitation::authenticateGameCenter(void* aParameter) {
    return [[InvitationOC sharedInstance] initGameCenterLocalPlayer:aParameter];
}

- (bool) initGameCenterLocalPlayer:(void *) aParameter
{
    // Reference https://developer.apple.com/documentation/gamekit/gklocalplayer/1515399-authenticatehandler?language=objc.
    [GKLocalPlayer localPlayer].authenticateHandler = ^(UIViewController *viewController, NSError *error) {
        if (viewController != nil)
        {
            // TBD.
        } else if ([GKLocalPlayer localPlayer].authenticated)
        {
            [[GKLocalPlayer localPlayer] generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL * _Nullable publicKeyUrl, NSData * _Nullable signature, NSData * _Nullable salt, uint64_t timestamp, NSError * _Nullable error) {
                if (nil == error) {
                    char const * const playerIdStr = [[[GKLocalPlayer localPlayer] playerID] UTF8String];
                    char const * const publicKeyUrlStr = [publicKeyUrl.absoluteString UTF8String];
                    char const * const signatureB64Str = [[signature base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength] UTF8String];
                    char const * const saltB64Str = [[salt base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength] UTF8String];
                    // NSLog(@"playerId: %s\npublicKeyUrl: %s\nsignature(b64encoded): %s\nsalt(b64encoded): %s\ntimestamp: %llu\n", playerIdStr, publicKeyUrlStr, signatureB64Str, saltB64Str, timestamp);
MyGame::Invitation::onGameCenterIdentityObtained(playerIdStr, publicKeyUrlStr, signatureB64Str, saltB64Str, timestamp, error);
                }
            }];
        } else {
            // TODO: Alert to tell the player to turn on GameCenter in system preferences.
            NSLog(@"GameCenter is not yet turned on. Go to iPhone `Settings->Game Center` to turn it on!");
        }
    };
    return true;
}

@end


An example printed bunch is as follows.

playerId: G:1484743399
------------------------------
publicKeyUrl: https://static.gc.apple.com/public-key/gc-prod-4.cer
------------------------------
signature(b64encoded): cBuIQHSxMJL/B5E9ACtiXfX3injVZJbM2SNlS6UpC/YE8i+GBB+NrFlOVJzncqag

gA02BMs22D/fxdoFtkw+veR8fMUziR3JZ36iaxdKXOwEJMP3VE6pbZ81V++ni/TZ

2dOJ5CHXC7bMWh7fVi8Ttajv/rBX2004cUQLls0eXFNak1/NfM8RahfFLqWiGqaG

OEzwY5dtrFGtyWvQsM3O+ESyQxbBbmC520ys86t9gXdknn5I5NMTtxPuST1TjyEg

EwG7bgY5WpH1wJHqA5EWUZ6yq2EHrSvQE35QWa04uYF6cMGKfP1m828S3GkKrWI+

G/Vgpfyzbi8fPXpxWon9Gg==
------------------------------
salt(b64encoded): MtzZYA==
------------------------------
timestamp: 1545979696626

#2 C++ calls JavaScript function

#2.1 Writes the JavaScript function to be called

TBD.


#3 Troubleshooting

#3.1 Manually adding cpp files into XCode project virtual directory named `Classes`


#3.2 Choosing a right dev team

#3.3 Symbol not found for "_OBJC_CLASS_$_GKLocalPlayer"

Just enable "Game Center" in "Capabilities" and re-build.

#4 Full example

https://git.red0769.com/lock-interactive/CCJsbIapPrac


#4.1 游戏请求玩家GameCenter授权Case-1

GameCenter_Disabled_At_Beginning_1.mov6.3MBGameCenter_Disabled_At_Beginning_2.mov1.4MB

#4.2 游戏请求玩家GameCenter授权Case-2

GameCenter_Disabled_During_Game_1.mov8.7MBGameCenter_Disabled_During_Game_2.mov2.8MB

#4.3 游戏请求玩家GameCenter授权Case-3

最复杂的情况,因为苹果的默认“3次cancel将直到下次授权前block此应用”约束,是不能让玩家重复尝试授权的。

GameCenter_Disabled_At_Beginning_And_Denied_To_Authenticate_1.mov7.2MBGameCenter_Disabled_At_Beginning_And_Denied_To_Authenticate_2.mov3.2MB