cocos2d-xアプリ内課金簡単に実装 iOS篇

ChainZ(クリエイター)
いろいろやってます。

自社ゲームにライフを購入するというアプリ内課金要素があるので、簡単に実装してみました。

まず、xcodeプロジェクトのCapabilities項目にアプリ内課金機能をONにします。

!()[1.png]

そうすると、StoreKitが使えるようになります。簡単にしたいので、直接ios/AppControllerを利用して実装します。

できれば、InAppPurchaseManagerのような汎用的なクラスを用意して実装するのがベストだが、この記事はあくまでサンプルなので、そこまではしません

まず、AppControllerに、SKProductsRequestDelegateSKPaymentTransactionObserver二つのインターフェースを追加します:

@interface AppController : NSObject <UIApplicationDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver> {
...
}

AppController.mmObserverselfにする:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
...

プロパティを追加:

@property(nonatomic, weak) SKPaymentTransaction* pendingTransaction;

続いてAppController.hに下記のメソードを追加:

- (void) purchase: (NSString*) productId
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response;
- (void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
- (void) purchaseOk: (SKPaymentTransaction*) transaction;
- (void) purchaseFail: (SKPaymentTransaction*) transaction;

内容はこうなります:

- (void) purchase:(NSString *)productId
{
NSLog(@"%@", productId);
// 製品情報を取得、複数のID渡せますが、この記事では単品のみ対応します
NSSet* products = [[NSSet alloc] initWithObjects: productId, nil];
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: products];
// AppControllerはSKProductsRequestDelegateを継承しているので、selfにします
productsRequest.delegate = self;
// リクエスト出す
[productsRequest start];
}
// このメソードはSKProductsRequestDelegateの一部
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
// もし返ってきた配列が空だったら
if (response.products.count == 0) {
// 不正の製品IDをprint
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
NSLog(@"Invalid product id: %@" , invalidProductId);
}
return;
}
NSArray* products = response.products;
// 頭の要素だけ
SKProduct *product = [products firstObject];
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
// 数量も1固定
payment.quantity = 1;
// paymentの処理が始まる
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
// 全てのtransactionをイテレートする
// この記事では成功と失敗しか処理をしませんが、全て考慮する必要がある
for (SKPaymentTransaction *transaction in transactions) {
NSString* productId = transaction.payment.productIdentifier;
switch (transaction.transactionState) {
// Call the appropriate custom method for the transaction state.
case SKPaymentTransactionStatePurchasing:
NSLog(@"purchasing!!!");
break;
case SKPaymentTransactionStateDeferred:
NSLog(@"deferred!!!");
break;
// 処理失敗
case SKPaymentTransactionStateFailed:
[self purchaseFail:transaction];
NSLog(@"failed!!! %i %@", [transaction.error code], [transaction.error localizedDescription]);
break;
// 購入成功
case SKPaymentTransactionStatePurchased:
NSLog(@"purchased!!!");
[self purchaseOk:transaction];
break;
case SKPaymentTransactionStateRestored:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(@"restored!!!");
break;
default:
// For debugging
NSLog(@"Unexpected transaction state %@", @(transaction.transactionState));
break;
}
}
}
- (void) purchaseFail:(SKPaymentTransaction *)transaction
{
// cocos2d-xのDirectorで「purchase:fail」のeventを発行
// idをUserDataとしてeventに保存
auto event = new cocos2d::EventCustom("purchase:fail");
NSString* error = [transaction.error localizedDescription];
std::string err = [error UTF8String];
event->setUserData(&err);
cocos2d::Director::getInstance()->getEventDispatcher()->dispatchEvent(event);
}
- (void) purchaseOk:(SKPaymentTransaction *)transaction
{
// cocos2d-xのDirectorで「purchase:ok」のeventを発行
// idをUserDataとしてeventに保存
auto event = new cocos2d::EventCustom("purchase:ok");
NSString* productId = transaction.payment.productIdentifier;
std::string str = [productId UTF8String];
event->setUserData(&str);
// 現在実行中のtransactionを保存
_pendingTransaction = transaction;
cocos2d::Director::getInstance()->getEventDispatcher()->dispatchEvent(event);
}

AppController.mm- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptionsの終わるところに下記のコード追加:

cocos2d::Director::getInstance()->getEventDispatcher()->addCustomEventListener("purchase:life", [self](cocos2d::EventCustom* event){
// 購入メソードを呼び出し
[self purchase: @"jp.co.befool.xxxxxx.life"];
});
cocos2d::Director::getInstance()->getEventDispatcher()->addCustomEventListener("purchase:done", [self](cocos2d::EventCustom* event){
if (!self.pendingTransaction) return;
// transactionを終了させる
[[SKPaymentQueue defaultQueue] finishTransaction:self.pendingTransaction];
self.pendingTransaction = nil;
});
return YES;
}

アプリ内課金機能を使う

cocos2d::EventDispatcherを利用して、ライフの購入ボタンでpurchase:lifeイベントを発行すれば処理が走ります。

// 課金成功処理
Director::getInstance()->getEventDispatcher()->addCustomEventListener("purchase:ok", [&, scene](EventCustom* event){
std::string *id_ptr = static_cast<std::string*>(event->getUserData());
std::string id = *id_ptr;
if (id.compare(PRODUCT_LIFE) == 0){
// TODO: アイテム付与など
// transactionを終了させる
Director::getInstance()->getEventDispatcher()->dispatchCustomEvent("purchase:done");
return;
}
CCLOG("invalid product id %s", id.c_str());
});
// 課金失敗処理
Director::getInstance()->getEventDispatcher()->addCustomEventListener("purchase:fail", [&, scene](EventCustom* event){
std::string *err_ptr = static_cast<std::string*>(event->getUserData());
std::string err = *err_ptr;
CCLOG("err %s", err.c_str());
});
// 購入ボタン
auto label = ui::Text::create("購入", "", 18);
label->setTouchEnabled(true);
label->setPosition(Vec2(visible_size.width/2, visible_size.height/2));
label->addClickEventListener([&](Ref* target){
Director::getInstance()->getEventDispatcher()->dispatchCustomEvent("purchase:life");
});
overlay_->addChild(label);

まとめ

簡単に実装したかったんですが、書き終わったらちょっとひどいなと思いました(笑)。やはり課金部分は大事なので、この記事の実装は参考程度にしてください。

製品版になると、レシートの管理や、トランザクションが失敗した場合の復帰処理など、考慮すべきなところいっぱいあります。時間をかけて汎用性のある実装にしたほうがいいと思います。

ちなみに、cocos2d-xのpluginでも実現できますが、今回は勉強も含めて自前で実装してみました。