概要
Plugin-XはCocos2d-xでC++とObjective-C/Javaの連携を簡単にするための仕組みです。一度組み込んでしまえば似たようなグルーコードを書かず、ObjC/Javaを書くだけで機能を追加できるようになるのが特徴です。
メリット
- 一度組み込んだら似たような振る舞いのSDKを組み込むのが楽
- 定義されているプロトコルで表現できそうなら、独自のプラグインを作って追加するのも楽
- プラグインの再利用性が高い
- 公式が作ってる仕組み
デメリット
- 定義されているプロトコルで表現できないことはやりにくい
- アプリのライフサイクルのメソッドに処理を入れたい場合(onResumeでなくonStartに入れたい場合)など、かゆいところに手が届かない
- 公式があまり活発でない
- 使ってる人をあまり見かけない
仕組み
Plugin-Xはプロトコルとプラグインから成っています。プロトコルには期待する振る舞いが定義されており、プラグインはその中身を持ちます。例えば広告用プロトコルを実装したAdmobプラグインは次のようになります。
// 広告を表示するメソッド
void showAds(TAdsInfo info, AdsPos pos = kPosCenter);
// 広告を非表示にするメソッド
void hideAds(TAdsInfo info);
@interface AdsAdmob : NSObject <InterfaceAds> // ObjC側ではInterfaceAdsと呼ばれる
- (void) showAds: (NSMutableDictionary*) info position:(int) pos;
- (void) hideAds: (NSMutableDictionary*) info;
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 | シェア | |
Social | スコアランキング、アチーブメント | |
User | ログイン |
導入
リポジトリはcocos2d-x/plugin-xです。Cocos2d-x本体からsubmoduleとして参照されているのでsubmodule update
で勝手に入ってきます。cocos2d-x/pluginディレクトリに展開されていればokです。
iOSに実装する
まずはプロトコルをリンクします。以下、Cocos Code IDEで新規作成したluaプロジェクトを前提に記述するので、パス等は適宜読み替えて下さい。
-
frameworks/cocos2d-x/plugin/protocols/proj.ios/PluginProtocol.xcodeproj
をプロジェクトに追加します。
-
プロジェクトの設定 => Build Settingsからヘッダのパスに
frameworks/cocos2d-x/plugin/protocols/include
を追加します。
-
リンカフラグに-ObjCを追加します。この影響でcocos本体がビルドできなくなるので、GameController.frameworkもリンクします(詳細)。
これでプロトコルのリンクは完了です。applicationDidFinishLaunching等で以下のコードが期待通りに動作すればokです。
#include "PluginManager.h"
using namespace plugin;
// ...
auto hoge = PluginManager::getInstance()->loadPlugin("hoge");
if (!hoge) {
log("hogeという名前のプラグインは存在しないはずなのでNULLが返ってきたらOK");
}
一度プロトコルをリンクしてしまえば各プラグインのリンクは簡単です。ここでは例としてAdmobを追加してみます。
-
プロトコルのときと同じく、
frameworks/cocos2d-x/plugin/plugins/admob/proj.ios/PluginAdmob.xcodeproj
をプロジェクトに追加してライブラリをリンクします。
これでAdmobのリンクも完了です。以下のようなコードでバナー広告が表示されたらokです。
#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
も書き換えておきます。
@@ -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": [
{
@@ -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)
@@ -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箇所ずつ書き換えます。
@@ -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"
@@ -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です。
#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のときと同じ確認コードが動作するはずです。
#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では年齢を設定するための専用メソッドが定義されています。
- (void) setAge: (NSNumber*) age
{
int nAge = [age integerValue];
OUTPUT_LOG(@"Flurry setAge invoked (%d)", nAge);
[Flurry setAge:nAge];
}
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);
戻り値を受け取る場合callIntFuncWithParam
やcallStringFuncWithParam
などが使えます。
初期化やメソッド呼び出しをしてる箇所
iOSではそれぞれNSClassFromString
とperformSelector
を使っています。
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];
...
}
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のメソッドを呼び出すのでその仕組みをそのまま使っています。
#define ANDROID_PLUGIN_PACKAGE_PREFIX "org/cocos2dx/plugin/"
PluginProtocol* PluginFactory::createPlugin(const char* name)
{
...
std::string jClassName = ANDROID_PLUGIN_PACKAGE_PREFIX;
jClassName.append(name);
...
}
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;
...
}
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);
}
}