[Cocos2d-x]Plugin-XでObjC/Java連携

## 概要
Plugin-XはCocos2d-xでC++とObjective-C/Javaの連携を簡単にするための仕組みです。一度組み込んでしまえば似たようなグルーコードを書かず、ObjC/Javaを書くだけで機能を追加できるようになるのが特徴です。

### メリット
* 一度組み込んだら似たような振る舞いのSDKを組み込むのが楽
* 定義されているプロトコルで表現できそうなら、独自のプラグインを作って追加するのも楽
* プラグインの再利用性が高い
* 公式が作ってる仕組み

### デメリット
* 定義されているプロトコルで表現できないことはやりにくい
* アプリのライフサイクルのメソッドに処理を入れたい場合（onResumeでなくonStartに入れたい場合）など、かゆいところに手が届かない
* 公式があまり活発でない
* 使ってる人をあまり見かけない

## 仕組み
Plugin-Xはプロトコルとプラグインから成っています。プロトコルには期待する振る舞いが定義されており、プラグインはその中身を持ちます。例えば広告用プロトコルを実装したAdmobプラグインは次のようになります。

```cpp:plugin/protocols/include/ProtocolAds.h
// 広告を表示するメソッド
void showAds(TAdsInfo info, AdsPos pos = kPosCenter);

// 広告を非表示にするメソッド
void hideAds(TAdsInfo info);
```
```objc:plugin/plugins/admob/proj.ios/AdsAdmob.h
@interface AdsAdmob : NSObject <InterfaceAds> // ObjC側ではInterfaceAdsと呼ばれる

- (void) showAds: (NSMutableDictionary*) info position:(int) pos;
- (void) hideAds: (NSMutableDictionary*) info;
```
```java:plugin/plugins/admob/proj.android/src/org/cocos2dx/plugin/AdsAdmob.java
public class AdsAdmob implements InterfaceAds { // Java側も

    @Override
    public void showAds(Hashtable<String, String> info, int pos) { ... }

    @Override
    public void hideAds(Hashtable<String, String> info) { ... }

}
```
公式に[概要図](http://www.cocos2d-x.org/wiki/Plugin-X_Architecture)が載っています。現状、Plugin-Xでは次のプロトコルが提供されています。

| プロトコル | 用途 | 例 |
|:-:|:-:|:-:|
| Ads | 広告 | Admob |
| Analytics | 分析 | Flurry |
| IAP | アプリ内課金 |
| Share | シェア | Twitter |
| Social | スコアランキング、アチーブメント |
| User | ログイン |

## 導入
リポジトリは[cocos2d-x/plugin-x](https://github.com/cocos2d-x/plugin-x)です。Cocos2d-x本体からsubmoduleとして参照されているので`submodule update`で勝手に入ってきます。cocos2d-x/pluginディレクトリに展開されていればokです。

## iOSに実装する
まずはプロトコルをリンクします。以下、Cocos Code IDEで新規作成したluaプロジェクトを前提に記述するので、パス等は適宜読み替えて下さい。

1. `frameworks/cocos2d-x/plugin/protocols/proj.ios/PluginProtocol.xcodeproj`をプロジェクトに追加します。
![ss01.png](https://qiita-image-store.s3.amazonaws.com/0/2490/db549b3e-4208-7d0a-9303-5caab683993e.png)

2. ターゲットの設定 => Build Phasesからライブラリをリンクします。
![ss02.png](https://qiita-image-store.s3.amazonaws.com/0/2490/50aea0ad-891d-00dc-b01d-859b7f23fe3b.png)

3. プロジェクトの設定 => Build Settingsからヘッダのパスに`frameworks/cocos2d-x/plugin/protocols/include`を追加します。
![ss03.png](https://qiita-image-store.s3.amazonaws.com/0/2490/9417627d-bf6c-7164-c870-0b189ddc9455.png)

4. リンカフラグに-ObjCを追加します。この影響でcocos本体がビルドできなくなるので、GameController.frameworkもリンクします（[詳細](http://stackoverflow.com/questions/24844766/linking-errors-when-adding-admob-to-ios-cocos2d-x-3-2)）。
![ss04.png](https://qiita-image-store.s3.amazonaws.com/0/2490/aca28b96-861a-cd9c-afa1-f75347c431d6.png)
![ss05.png](https://qiita-image-store.s3.amazonaws.com/0/2490/3091f473-9f10-15de-9f9a-6dc6aa6cad37.png)

これでプロトコルのリンクは完了です。applicationDidFinishLaunching等で以下のコードが期待通りに動作すればokです。

```cpp:AppDelegate.cpp
#include "PluginManager.h"
using namespace plugin;

// ...

    auto hoge = PluginManager::getInstance()->loadPlugin("hoge");
    if (!hoge) {
        log("hogeという名前のプラグインは存在しないはずなのでNULLが返ってきたらOK");
    }
```
一度プロトコルをリンクしてしまえば各プラグインのリンクは簡単です。ここでは例としてAdmobを追加してみます。

1. プロトコルのときと同じく、`frameworks/cocos2d-x/plugin/plugins/admob/proj.ios/PluginAdmob.xcodeproj`をプロジェクトに追加してライブラリをリンクします。
![ss06.png](https://qiita-image-store.s3.amazonaws.com/0/2490/b279b39e-e3fc-8898-849e-cbb70817cf6f.png)
![ss07.png](https://qiita-image-store.s3.amazonaws.com/0/2490/8df60f88-c61f-bad9-ee3e-f10e3fac304b.png)

2. プラグインごとに必要なライブラリを適宜追加します。Admobだと以下のようになります。
![ss08.png](https://qiita-image-store.s3.amazonaws.com/0/2490/ec196dae-91e4-b6cd-0d24-78b7fec53170.png)

これでAdmobのリンクも完了です。以下のようなコードでバナー広告が表示されたらokです。

```cpp:AppDelegate.cpp
#include "ProtocolAds.h"

// ...

    auto admob = dynamic_cast<ProtocolAds*>(PluginManager::getInstance()->loadPlugin("AdsAdmob"));
    admob->setDebugMode(true);
    TAdsDeveloperInfo devInfo;
    devInfo["AdmobID"] = "your_admob_id";
    admob->configDeveloperInfo(devInfo);
    TAdsInfo info;
    info["AdmobType"] = "1"; // banner
    info["AdmobSizeEnum"] = "1"; // banner
    admob->showAds(info, ProtocolAds::AdsPos::kPosBottom);
```

## Androidに組み込む
Androidに組み込む手順はやや複雑で、いくつかの補助ツールが提供されています。やや古いですが公式の[ドキュメント](http://www.cocos2d-x.org/wiki/Plugin-X_Integration_Guide_for_Android)もあります。

* `frameworks/cocos2d-x/plugin/tools`以下に`publish.sh`があるので使いたいプラグイン名を引数にして実行します（引数を省略すると同ディレクトリの`config.sh`で指定されたプラグインを全てビルドします）。初回実行時はandroid-ndk/sdkとantのパスを聞かれるので、答えるとツールのディレクトリに`environment.sh`として保存し、次回以降それを参照します。成功すると`frameworks/cocos2d-x/plugin/publish`以下にビルドされたファイルが生成されます。

```shell-session
$ tools/publish.sh admob

...

Please input the android-ndk path:
/usr/local/opt/android-ndk
Get ANDROID_NDK_ROOT=/usr/local/opt/android-ndk

Please input the andoid-sdk path:
/usr/local/opt/android-sdk
Get ANDROID_SDK_ROOT=/usr/local/opt/android-sdk

Please input the ant tool path(such as '/Users/MyAccount/tools/ant/bin'):
/usr/local/opt/ant/bin
Get ANT_PATH=/usr/local/opt/ant/bin
```

* JNIのライブラリをリンクします。`build-cfg.json`でndk_module_pathを追加して`Android.mk`でモジュールを読み込みます。また、テンプレートのままだとビルドが通らない箇所があるので、ついでに`Application.mk`も書き換えておきます。

```diff:frameworks/runtime-src/proj.android/build-cfg.json
@@ -3,7 +3,8 @@
         "../../cocos2d-x",
         "../../cocos2d-x/cocos/",
         "../../cocos2d-x/external",
-        "../../cocos2d-x/cocos/scripting"
+        "../../cocos2d-x/cocos/scripting",
+        "../../cocos2d-x/plugin/publish"
     ],
     "copy_resources": [
         {
```
```diff:frameworks/runtime-src/proj.android/jni/Android.mk
@@ -40,9 +40,10 @@ $(LOCAL_PATH)/../../Classes

 LOCAL_STATIC_LIBRARIES := curl_static_prebuilt

-LOCAL_WHOLE_STATIC_LIBRARIES := cocos_lua_static
+LOCAL_WHOLE_STATIC_LIBRARIES := cocos_lua_static PluginProtocolStatic

 include $(BUILD_SHARED_LIBRARY)

 $(call import-module,scripting/lua-bindings)
+$(call import-module,protocols/android)
```
```diff:frameworks/runtime-src/proj.android/jni/Application.mk
@@ -1,4 +1,4 @@
-APP_STL := c++_static
+APP_STL := gnustl_static
 NDK_TOOLCHAIN_VERSION=clang

 APP_CPPFLAGS := -frtti -DCC_ENABLE_CHIPMUNK_INTEGRATION=1 -std=c++11 -fsigned-char
```

* プラグインの初期化処理を書きます。C++とJavaでそれぞれ1箇所ずつ書き換えます。

```diff:frameworks/runtime-src/proj.android/jni/lua/main.cpp
@@ -4,6 +4,7 @@
 #include <jni.h>
 #include <android/log.h>
 #include "ConfigParser.h"
+#include "PluginJniHelper.h"

 #define  LOG_TAG    "main"
 #define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
@@ -13,6 +14,7 @@ using namespace cocos2d;
 void cocos_android_app_init (JNIEnv* env, jobject thiz) {
     LOGD("cocos_android_app_init");
     AppDelegate *pAppDelegate = new AppDelegate();
+    PluginJniHelper::setJavaVM(JniHelper::getJavaVM());
 }

 extern "C"
```
```diff:frameworks/runtime-src/proj.android/src/org/cocos2dx/lua/AppActivity.java
@@ -33,6 +33,8 @@ import java.util.Enumeration;
 import java.util.ArrayList;

 import org.cocos2dx.lib.Cocos2dxActivity;
+import org.cocos2dx.lib.Cocos2dxGLSurfaceView;
+import org.cocos2dx.plugin.PluginWrapper;

 import android.app.AlertDialog;
 import android.content.Context;
@@ -95,6 +97,18 @@ public class AppActivity extends Cocos2dxActivity{
                }
                hostIPAdress = getHostIpAddress();
        }
+
+  @Override
+  public Cocos2dxGLSurfaceView onCreateView() {
+      Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
+      PluginWrapper.init(this);
+      PluginWrapper.setGLSurfaceView(glSurfaceView);
+      return glSurfaceView;
+  }
+
        private boolean isNetworkConnected() {
                ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
                if (cm != null) {
```

* 最後にjarファイルをコピーします。jarはpublish時に同時に作られているのでプロジェクトにコピーするだけでokです。

```shell-session
cp frameworks/cocos2d-x/plugin/publish/protocols/android/libPluginProtocol.jar frameworks/runtime-src/proj.android/libs
```
iOSのときと同じく、この時点で以下の様な確認コードが期待通り動作すればokです。

```cpp:AppDelegate.cpp
#include "PluginManager.h"
using namespace plugin;

// ...

    auto hoge = PluginManager::getInstance()->loadPlugin("hoge");
    if (!hoge) {
        log("hogeという名前のプラグインは存在しないはずなのでNULLが返ってきたらOK");
    }
```
後はAdmobをリンクさせるだけです。plugin-xからAdmobを呼び出す形になるため、普通にAndroidでAdmobを使うための設定が必要ですが、ここでは割愛します。具体的な手順は[公式のドキュメント](https://developers.google.com/mobile-ads-sdk/docs/)にあります。

* protocolと同じようにjarファイルをコピーします。

```shell-session
cp frameworks/cocos2d-x/plugin/publish/plugins/admob/android/*.jar frameworks/runtime-src/proj.android/libs
```

これでiOSのときと同じ確認コードが動作するはずです。

```cpp:AppDelegate.cpp
#include "ProtocolAds.h"

// ...

    auto admob = dynamic_cast<ProtocolAds*>(PluginManager::getInstance()->loadPlugin("AdsAdmob"));
    admob->setDebugMode(true);
    TAdsDeveloperInfo devInfo;
    devInfo["AdmobID"] = "your_admob_id";
    admob->configDeveloperInfo(devInfo);
    TAdsInfo info;
    info["AdmobType"] = "1"; // banner
    info["AdmobSizeEnum"] = "1"; // banner
    admob->showAds(info, ProtocolAds::AdsPos::kPosBottom);
```

## カスタムメソッドの呼び出し
Plugin-Xではプロトコルで定義されていないメソッドを呼び出すこともできます。例えばFlurryでは年齢を設定するための専用メソッドが定義されています。

```objc:frameworks/cocos2d-x/plugin/plugins/flurry/proj.ios/AnalyticsFlurry.m
- (void) setAge: (NSNumber*) age
{
    int nAge = [age integerValue];
    OUTPUT_LOG(@"Flurry setAge invoked (%d)", nAge);
    [Flurry setAge:nAge];
}
```
```java:frameworks/cocos2d-x/plugin/plugins/flurry/proj.android/src/org/cocos2dx/plugin/AnalyticsFlurry.java
    protected void setAge(int age) {
        LogD("setAge invoked!");
        final int curAge = age;
        PluginWrapper.runOnMainThread(new Runnable() {
            @Override
            public void run() {
                try {
                    FlurryAgent.setAge(curAge);
                } catch(Exception e){
                    LogE("Exception in setAge", e);
                }
            }
        });
    }
```
これらはcppから以下のように呼び出すことができます。

```cpp
flurry = PluginManager::getInstance()->loadPlugin("AnalyticsFlurry");
PluginParam age(17);
flurry->callFuncWithParam("setAge", &age, NULL);
```
戻り値を受け取る場合`callIntFuncWithParam`や`callStringFuncWithParam`などが使えます。

## 初期化やメソッド呼び出しをしてる箇所
iOSではそれぞれ`NSClassFromString`と`performSelector`を使っています。

```objc++:frameworks/cocos2d-x/plugin/protocols/platform/ios/PluginFactory.mm
PluginProtocol* PluginFactory::createPlugin(const char* name)
{

...

    NSString* className = [NSString stringWithUTF8String:name];
    Class theClass = NSClassFromString(className);
    if (theClass == nil)
    {
        PluginUtilsIOS::outputLog("Unable to load class '%s'. Did you add the -ObjC linker flag?", name);
        break;
    }

    id obj = [[NSClassFromString(className) alloc] init];

...

}
```
```objc++:frameworks/cocos2d-x/plugin/protocols/platform/ios/PluginUtilsIOS.mm
void PluginUtilsIOS::callOCFunctionWithName(PluginProtocol* pPlugin, const char* funcName)
{

...

    NSString* strFuncName = [NSString stringWithUTF8String:funcName];
    SEL selector = NSSelectorFromString(strFuncName);
    if ([pOCObj respondsToSelector:selector]) {
        [pOCObj performSelector:selector];
    } else {
        outputLog("Can't find function '%s' in class '%s'", [strFuncName UTF8String], pData->className.c_str());
    }

...

}
```
Androidは`PluginFactory.cpp`でパッケージ名を指定し、`Class.forName`でクラスを作ります。そのため、プラグインとして追加するクラスのパッケージは`org/cocos2dx/plugin`にします。メソッド呼び出しは、そもそもJNIが文字列からjavaのメソッドを呼び出すのでその仕組みをそのまま使っています。

```cpp:frameworks/cocos2d-x/plugin/protocols/platform/android/PluginFactory.cpp
#define ANDROID_PLUGIN_PACKAGE_PREFIX "org/cocos2dx/plugin/"

PluginProtocol* PluginFactory::createPlugin(const char* name)
{

...

    std::string jClassName = ANDROID_PLUGIN_PACKAGE_PREFIX;
    jClassName.append(name);

...

}
```
```java:frameworks/cocos2d-x/plugin/protocols/proj.android/src/org/cocos2dx/plugin/PluginWrapper.java
protected static Object initPlugin(String classFullName)
{

...

    // 読みやすくするため一部改変しています
    Class<?> c = null;
    String fullName = classFullName.replace('/', '.');
    c = Class.forName(fullName);
    Context ctx = getContext();
    Object o = c.getDeclaredConstructor(Context.class).newInstance(ctx);
    return o;

...

}
```
```cpp:frameworks/cocos2d-x/plugin/protocols/platform/android/PluginUtils.h
static void callJavaFunctionWithName(PluginProtocol* thiz, const char* funcName)
{

...

    PluginJniMethodInfo t;
    if (PluginJniHelper::getMethodInfo(t
        , pData->jclassName.c_str()
        , funcName
        , "()V"))
    {
        t.env->CallVoidMethod(pData->jobj, t.methodID);
        t.env->DeleteLocalRef(t.classID);
    }
}
```

## リンク
* [Plugin-X | Cocos2d-x](http://www.cocos2d-x.org/wiki/Plugin-X)
* [cocos2d-x/plugin-x](https://github.com/cocos2d-x/plugin-x)
* [Cocos2dx v3.0 で plugin-x / AdMob を利用する](http://laboyukai.blogspot.jp/2014/06/cocos2dx-v30-plugin-x-admob.html)