cocos2d-xアプリ内課金簡単に実装 iOS篇
自社ゲームにライフを購入するというアプリ内課金要素があるので、簡単に実装してみました。
まず、xcodeプロジェクトのCapabilities項目にアプリ内課金機能をONにします。
!()[1.png]
そうすると、StoreKit
が使えるようになります。簡単にしたいので、直接ios/AppController
を利用して実装します。
できれば、
InAppPurchaseManager
のような汎用的なクラスを用意して実装するのがベストだが、この記事はあくまでサンプルなので、そこまではしません
まず、AppController
に、SKProductsRequestDelegate
とSKPaymentTransactionObserver
二つのインターフェースを追加します:
@interface AppController : NSObject <UIApplicationDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver> {
...
}
AppController.mm
にObserver
をself
にする:
- (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でも実現できますが、今回は勉強も含めて自前で実装してみました。