Posted at

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

More than 5 years have passed since last update.


概要

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


メリット


  • 一度組み込んだら似たような振る舞いのSDKを組み込むのが楽

  • 定義されているプロトコルで表現できそうなら、独自のプラグインを作って追加するのも楽

  • プラグインの再利用性が高い

  • 公式が作ってる仕組み


デメリット


  • 定義されているプロトコルで表現できないことはやりにくい

  • アプリのライフサイクルのメソッドに処理を入れたい場合(onResumeでなくonStartに入れたい場合)など、かゆいところに手が届かない

  • 公式があまり活発でない

  • 使ってる人をあまり見かけない


仕組み

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


plugin/protocols/include/ProtocolAds.h

// 広告を表示するメソッド

void showAds(TAdsInfo info, AdsPos pos = kPosCenter);

// 広告を非表示にするメソッド
void hideAds(TAdsInfo info);



plugin/plugins/admob/proj.ios/AdsAdmob.h

@interface AdsAdmob : NSObject <InterfaceAds> // ObjC側ではInterfaceAdsと呼ばれる

- (void) showAds: (NSMutableDictionary*) info position:(int) pos;
- (void) hideAds: (NSMutableDictionary*) info;



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) { ... }

}


公式に概要図が載っています。現状、Plugin-Xでは次のプロトコルが提供されています。

プロトコル
用途

Ads
広告
Admob

Analytics
分析
Flurry

IAP
アプリ内課金

Share
シェア
Twitter

Social
スコアランキング、アチーブメント

User
ログイン


導入

リポジトリは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


  2. ターゲットの設定 => Build Phasesからライブラリをリンクします。

    ss02.png


  3. プロジェクトの設定 => Build Settingsからヘッダのパスにframeworks/cocos2d-x/plugin/protocols/includeを追加します。

    ss03.png


  4. リンカフラグに-ObjCを追加します。この影響でcocos本体がビルドできなくなるので、GameController.frameworkもリンクします(詳細)。

    ss04.png

    ss05.png


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


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

    ss07.png


  2. プラグインごとに必要なライブラリを適宜追加します。Admobだと以下のようになります。

    ss08.png


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


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に組み込む手順はやや複雑で、いくつかの補助ツールが提供されています。やや古いですが公式のドキュメントもあります。



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

$ 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も書き換えておきます。


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": [
{


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)



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箇所ずつ書き換えます。


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"



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です。

cp frameworks/cocos2d-x/plugin/publish/protocols/android/libPluginProtocol.jar frameworks/runtime-src/proj.android/libs

iOSのときと同じく、この時点で以下の様な確認コードが期待通り動作すればokです。


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を使うための設定が必要ですが、ここでは割愛します。具体的な手順は公式のドキュメントにあります。


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

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

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


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では年齢を設定するための専用メソッドが定義されています。


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];
}


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から以下のように呼び出すことができます。

flurry = PluginManager::getInstance()->loadPlugin("AnalyticsFlurry");

PluginParam age(17);
flurry->callFuncWithParam("setAge", &age, NULL);

戻り値を受け取る場合callIntFuncWithParamcallStringFuncWithParamなどが使えます。


初期化やメソッド呼び出しをしてる箇所

iOSではそれぞれNSClassFromStringperformSelectorを使っています。


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];

...

}



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のメソッドを呼び出すのでその仕組みをそのまま使っています。


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);

...

}



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;

...

}



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);
}
}



リンク