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

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

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

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

!()[1.png]

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

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

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

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

AppController.mmObserverselfにする:

1
2
3
4
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    ...

プロパティを追加:

1
@property(nonatomic, weak) SKPaymentTransaction* pendingTransaction;

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

1
2
3
4
5
6
- (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;

内容はこうなります:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
- (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の終わるところに下記のコード追加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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イベントを発行すれば処理が走ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 課金成功処理
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でも実現できますが、今回は勉強も含めて自前で実装してみました。