iOS/Androidとの相互API呼び出しを行う際のコツ:C++でクロスプラットフォームを実現するCocos2dx入門(後編)(2/3 ページ)
スマホ向けゲームアプリ開発フレームワークの概要や使い方を解説する特集です。今回は、C++とJava/Objectve-Cとのバインドコードを使って拡張する方法や注意点、メモリ解放機能などを解説します。
プラットフォーム依存オブジェクトのメソッド呼び出し
先ほどのStaticな関数呼び出しは、メソッド呼び出しのバインドコードだけ書いてやれば良いので簡単でしたが、場合によってはJavaもしくはObjective-Cのインスタンスに紐付いた情報をCocos2dx側で保持しておき、どこかのタイミングでそれらのインスタンスのメソッド呼び出しが必要になる場合があります。
この場合には、JavaもしくはObjective-CのインスタンスをCocos2dx内に保持する機構、保持したインスタンスから、そのインスタンスの保持するメソッドを呼び出す機構が必要です。
筆者の場合ですと、先ほどのヘッダを共通にしたいという希望があったので、Cocos2dxに公開する拡張モジュールのクラスに(void *)型のprivateなメンバを準備しておき、ここにそれぞれのインスタンス情報を保持することにしました。
また、保持したインスタンスからメソッドを呼び出すコードも準備しました。
具体例として、ターゲットのライブラリのDialogインスタンスを生成し、先ほどのBitmapを渡すための機能の実装例を示します。
CCGreeShareDialog *dialogA = CCGreeShareDialog::create(); dialogA->setParam(bmpA); CCGreeShareDialog *dialogB = CCGreeShareDialog::create(); dialogB->setParam(bmpB);
class CCGreeShareDialog : public CCObject { public: static CCGreeShareDialog* create(); void setParam(void *bmp); x x private: x void* mShareDialog; // プラットフォーム依存のインスタンスの保持を行う };
CCGreeShareDialog* CCGreeShareDialog::create(){ jobject obj = createShareDialogJni(); CCGreeShareDialog* dialog = NULL; if(obj != NULL){ dialog = new CCGreeShareDialog((void *)obj); } return dialog; }
CCGreeShareDialog* CCGreeShareDialog::create(){ GreeSharePopup* sharePopup = [GreeSharePopup popup]; CCGreeShareDialog *dialog = NULL; if(sharePopup != nil){ dialog = new CCGreeShareDialog((void*)sharePopup); } return dialog; }
それぞれ、プラットフォーム依存のインスタンスを保持したCocos2dxのインスタンスを生成し、ユーザーにそのインスタンスを返しています。
ユーザーはCocos2dx上でインスタンスに対して各種制御が行え、メソッドを呼び出した場合には、インスタンス内で保持している各プラットフォームのインスタンスに紐付いたAPIを呼び出すわけです。
jobject dialog = (jobject)mShareDialog; // Javaのインスタンスを取得 if(GreeJniHelper::getInstanceMethodInfo(t, dialog, "setParams", "(Landroid/graphics/Bitmap;)V")){ t.env->CallVoidMethod(dialog, t.methodID, bmp); t.env->DeleteLocalRef(t.classID); }
残念ながら、インスタンスメソッドを呼び出すためのAPIがCocos2dxのJniHelperには存在しなかったので、ここでは「GreeJniHelper」という名前でJni制御用クラスを定義し、インスタンスメソッド情報取得用にgetInstanceMethodInfoを追加実装しています。
CCGreeShareDialog* dialog = (CCGreeShareDialog*)mShareDialog; // Objective-Cのインスタンスを取得 [dialog setParams:bmp];
コールバックのハンドリング
最後に、Cocos2dxからAPIを同期的に呼び出すだけではなく、プラットフォーム側から非同期でコールバックを受け付けるような場合を紹介します。
先ほどの処理でも、Dialog表示を行った際に、そのコールバック通知を受ける必要がありました。
このような場合は、Objective-Cだと、すんなりと対応できますが、Javaの場合は、いくつが対応が難しかった点がありますので、その辺りを説明します。
プラットフォーム共通の部分
まずプラットフォーム共通の簡単なところからですが、拡張モジュールからGameに対してのコールバックは、EventDispatcherなどでも使われているDelegateを利用する手法を使用しました。
具体的には、各機能グループごとに以下のようなコールバック設定用メンバ/メソッドと、さらにコールバックで呼ばれる関数をvirtual methodで定義したDelegateクラスを準備しました。
#define CREATE_DELEGATE(module) \ public: \ static void set##module(CCGree##module *pDelegate) { s_p##module = pDelegate; } \ static CCGree##module *get##module() { return s_p##module; }; \ static void remove##module(CCGree##module *pDelegate) { s_p##module = NULL; } \ private: \ static CCGree##module *s_p##module; class CCGreeShareDialogDelegate { public: virtual void shareDialogOpened(CCGreeShareDialog *dialog){}; virtual void shareDialogCompleted(CCGreeShareDialog *dialog, CCArray *userArray){}; virtual void shareDialogCanceled(CCGreeShareDialog *dialog){}; };
これでユーザーがsetDelegate関数を実行し、さらに上記のコールバック用のメソッドを実装していればコールバックを受信できるようになります。
細かいですが、コールバックには対応するインスタンス情報も保持されているので、どのユーザーのインスタンスからのコールバックなのか明確に判断可能です。
Javaでプラットフォームからコールバックを受信する際の2つの問題点
ここからは、肝心のプラットフォームからコールバックを受信する部分ですが、特にJavaの場合について説明します。Javaの場合、特に問題になりそうなのが、以下の2つです。
- コールバックはAPI実行時にリスナを生成し登録する形態が多い。リスナはインターフェイスとして実装されていることが多く、Native側から直接生成できない
- JavaからNativeコードを呼ぶ場合、Native側はStaticなコードで受ける必要がある。このStaticなコードと関連するインスタンスを結び付ける手法が必要
1の対応策としては、JavaでインターフェイスをラップするようなHelperクラスを定義し、必要に応じてそのクラスのインスタンスを生成して、APIに渡すようにしました。
また2の対応策として、Cocos2dxのインスタンス情報をdelegateとして、上記クラスのインスタンス生成時に渡し、コールバックが呼ばれた際には、Native側にその値を戻すようにしました。
Dialog表示の場合の例とは、ちょっと異なりますが、上記1と2への対応が良く分かる例として以下以下のような実装があります。
package org.cocos2dx.lib.gree; import net.gree.asdk.api.wallet.Payment.PaymentListener; import org.apache.http.HeaderIterator; public class NativePaymentListener implements PaymentListener { private static final String TAG = NativePaymentListener.class.getSimpleName(); private long mDelegate; public NativePaymentListener(long delegate){ this.mDelegate = delegate; } private native void nativePaymentOnSuccess(long delegate, int responseCode, String paymentId); private native void nativePaymentOnCancel(long delegate, int responseCode, String paymentId); private native void nativePaymentOnFailure(long delegate, int responseCode, String paymentId, String response); @Override public void onSuccess(int responseCode, HeaderIterator headers, String paymentId){ nativePaymentOnSuccess(this.mDelegate, responseCode, paymentId); } @Override public void onCancel(int responseCode, HeaderIterator headers, String paymentId){ nativePaymentOnCancel(this.mDelegate, responseCode, paymentId); } @Override public void onFailure(int responseCode, HeaderIterator headers, String paymentId, String response){ nativePaymentOnFailure(this.mDelegate, responseCode, paymentId, response); } }
JniMethodInfo t; if(GreeJniHelper::getInstanceMethodInfo(t, obj, "request", "(Landroid/content/Context;Lnet/gree/asdk/api/wallet/Payment$PaymentListener;)V")){ JniMethodInfo f; if(JniHelper::getMethodInfo(f, "org/cocos2dx/lib/gree/NativePaymentListener", "", "(J)V")){ jobject listener = f.env->NewObject(f.classID, f.methodID, (unsigned long long)delegate); jobject context = getPlatformContext(); t.env->CallVoidMethod(obj, t.methodID, context, listener); } }
Nativeから上記のコールバック受信用クラスを生成し、そこに受信対象のインスタンス情報を登録します。このクラスを引数として、実際の処理対象の関数を呼び出しています。コールバック発生時上記のクラスは、セットされているmDelegateを引数としてNative関数のコールを行います。
これでコールバックを受信したNative関数は、コールバック受信対象のインスタンスが明示的に分かります。
Copyright © ITmedia, Inc. All Rights Reserved.