コマース

【PlayFab】アプリ内課金(IAP)を統合する方法

playfab-unity-iap

Unity のモバイルアプリで課金アイテムを販売するなら、アプリ内課金(IAP)の実装が必須になります。

例えば、「ジェム50個を1,000円で販売する」といったシーンや、「1ヶ月間だけ持続するアイテムを販売する」ということにも使えます。

しかし、IAP をゼロから実装するには難易度が高く、上級者向けと言えるでしょう。

PlayFab を使えば、レシート検証をやってくれるだけでなく、インベントリへの連携も簡単にしてくれます。

わりと苦戦しましたが、なんとかひと通り実装できたので、惜しみなくノウハウを公開しました。

ロジック部分については、ほぼコピペで流用できるはずです。

今回の動作確認は iOS で実施しました。

Android では動作確認できていませんが、おそらく問題なく実行できるはずです。

では、さっそく見ていきましょう。

アプリ内課金(IAP)を統合する方法

IAP の一般的な準備について解説していると、それだけで2記事くらいになるので、この記事で解説しません。

主にプログラム側を中心に解説してきます。

事前準備(Android)

Android の準備については、公式ドキュメント「PlayFab、Unity IAP、および Android の概要」を参照してください。

少し画像が古い箇所もありますが、特に迷うことなく準備できると思います。

事前準備(iOS)

iOS については、以下の素晴らしい記事を参照することでひと通り準備ができます。

(追記)
載せていた記事のリンクが切れてしまったので、課金準備で不明な点はご自身で調べていただく必要があります。

注意点としては、口座情報や税金フォームも登録しないと正常に動作しません。

あとは、PlayFab 側で Apple のアドオンを設定する必要があります。

アドオンの設定は、Sign in with Apple を「簡単に」統合する方法の事前準備を参照してください。

アイテムは以下を用意しました。

playfab-unity-iap

アイテムの準備

PlayFab 側で課金アイテムの準備をしていきます。

アイテムについて詳しく知りたい人は、ストアのアイテムを一覧表示して購入する手順をご覧ください。

今回は以下の3つを用意しました。

  • 消費型 カウント(Consumable)
  • 永続型(NonConsumable)
  • 消費型 時間(Subscription)
playfab-unity-iap

1つずつ消費したいアイテムについては、「消費型」で登録します。

あとで絞り込みをするために、アイテムクラスに「Consumable」を設定しました。

playfab-unity-iap

PlayFab 側でも価格の設定がありますが、Android または iOS 側で登録された値段が優先されるようになっています。

なので、PlayFab 側は何円になっていても関係ありません。

playfab-unity-iap

わかりやすいように合わせておくとわかりやすいですが、今回は RM 100 としました。

PlayFab では「RM を 100 で割った数値」がリアルマネーのドルとして計算されます。

なので、RM 100 というのは 1ドル のことですね。

RM 99 とすると、0.99 ドルになります。

続いて、1度買ってしまえばいいものは、「永続型」として登録します。

アイテムクラスは「NonConsumable」として登録しました。

playfab-unity-iap

最後に、一定期間が経過したらなくなるアイテムを作りたい場合は、消費型で時間を設定します。

アイテムクラスは「Subscription」として登録しました。

playfab-unity-iap

アイテムの準備はこれで完了です。

プログラムを書く

プログラムの全体は、私が作った IAPExample.cs を参照してください。

少しわかりにくいので、まずはアイテム購入の全体像を見てみましょう。

playfab-unity-iap

最後の「インベントリへの反映」は PlayFab が自動でやってくれるので、全体としては4つの手順で購入することができるようになっています。

1つずつ見ていきますね。

カタログからアイテム一覧を取得

まずは、PlayFab に登録しているアイテムを取得します。

private List<CatalogItem> Catalog;

private void RefreshIAPItems()
{
    PlayFabClientAPI.GetCatalogItems(new GetCatalogItemsRequest()
        , result =>
    {
        Catalog = result.Catalog;

        // Make UnityIAP initialize
        InitializePurchasing();
    }, error => Debug.LogError(error.GenerateErrorReport()));
}

取得したカタログは後で使うので、変数として保持しておきます。

ここは特に難しいところはないですね。

最後に、Unity IAP の初期化処理を呼び出しています。

Unity IAP の初期化

次に、Unity IAP の初期化をしていきます。

ConfigurationBuilder builder;

public void InitializePurchasing()
{
    if (IsInitialized) return;

    builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

    // Register each item from the catalog
    foreach (var item in Catalog.FindAll(x => x.ItemClass == "Consumable"))
        builder.AddProduct(item.ItemId, ProductType.Consumable);

    foreach (var item in Catalog.FindAll(x => x.ItemClass == "NonConsumable"))
        builder.AddProduct(item.ItemId, ProductType.NonConsumable);

    foreach (var item in Catalog.FindAll(x => x.ItemClass == "Subscription"))
        builder.AddProduct(item.ItemId, ProductType.Subscription);

    // Trigger IAP service initialization
    UnityPurchasing.Initialize(this, builder);
}

IAP のお作法は、Unity IAP の公式ドキュメントが参考になると思います。

builder に商品を追加して初期化をしなければならないのですが、アイテムの種類ごとに追加が必要になります。

ここで、先ほど付けておいたアイテムクラスが活きてくるんですね。

取得したカタログから、アイテムクラスごとに抽出してあげれば、簡単に追加することが可能です。

最後に、IAP の初期化メソッドを呼び出しています。

で、初期化に成功した場合は OnInitialized 、失敗した場合は OnInitializedFailed が呼ばれる仕組みです。

private IStoreController storeController;
private IExtensionProvider extensionProvider;

public bool IsInitialized
{
    get => storeController != null && extensionProvider != null && Catalog != null;
}

public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
    storeController = controller;
    extensionProvider = extensions;
}

public void OnInitializeFailed(InitializationFailureReason error)
{
    Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
}

初期化されたかどうかは、各変数が null かどうかで判断するようにしています。

初期化についてはこんなところですね。

アイテムを購入

次に、実際にアイテムを購入する手順です。

void BuyProductID(string productId)
{
    if (!IsInitialized)
        throw new Exception("IAP Service is not initialized!");

    storeController.InitiatePurchase(productId);
}

InitiatePurchase を呼び出すことで、iOS や Android に搭載されている機能で購入処理を進めることができます。

productId には、PlayFab 側のアイテム ID を渡してあげます。

iOS と Android のストアでも同じ ID で登録している前提なので、どちらでも正常に動作するはずです。

購入の結果は、ProcessPurchase で受け取ることができます。

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
{
    if (!IsInitialized)
    {
        return PurchaseProcessingResult.Complete;
    }

    // Test edge case where product is unknown
    if (e.purchasedProduct == null)
    {
        Debug.LogWarning("Attempted to process purchase with unknown product. Ignoring");
        return PurchaseProcessingResult.Complete;
    }

    // Test edge case where purchase has no receipt
    if (string.IsNullOrEmpty(e.purchasedProduct.receipt))
    {
        Debug.LogWarning("Attempted to process purchase with no receipt: ignoring");
        return PurchaseProcessingResult.Complete;
    }

    Debug.Log("Processing transaction: " + e.purchasedProduct.transactionID);

    // ----------
    // -- 中略 --
    // ----------
}

「購入した」という情報は、やろうと思えば改ざんすることもできます。

実際に購入していないのに、有料のアイテムをたくさん手に入れられる、といった不正は防がないといけません。

そこで用意されている機能が、次で紹介する「レシート検証」です。

レシート検証

レシート検証は自前で実装すると大変ですが、ありがたいことに PlayFab 側で API が用意されています。

iOS の検証 Validate IOS Receipt
Android の検証 Validate Google Play Purchase

つまり、メソッドの引数に適切な値を渡してあげるだけで、いろいろとサーバー側で検証してくれるということです。

実際のコードは以下の通り。

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
{
    // ----------
    // -- 中略 --
    // ----------
#if UNITY_IOS
    var wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(e.purchasedProduct.receipt);
     
    var store = (string)wrapper["Store"];
    var payload = (string)wrapper["Payload"]; // For Apple this will be the base64 encoded ASN.1 receipt

    PlayFabClientAPI.ValidateIOSReceipt(new ValidateIOSReceiptRequest
    {
        CurrencyCode = e.purchasedProduct.metadata.isoCurrencyCode,
        PurchasePrice = (int)e.purchasedProduct.metadata.localizedPrice * 100,
        ReceiptData = payload
    }, result => {
        Debug.Log("Validation successful!");
    },
        error => {
            Debug.Log("Validation failed: " + error.GenerateErrorReport());
        }
    );

#elif UNITY_ANDROID

    var googleReceipt = GooglePurchase.FromJson(e.purchasedProduct.receipt);

    PlayFabClientAPI.ValidateGooglePlayPurchase(new ValidateGooglePlayPurchaseRequest()
    {
        CurrencyCode = e.purchasedProduct.metadata.isoCurrencyCode,
        PurchasePrice = (uint)(e.purchasedProduct.metadata.localizedPrice * 100),
        ReceiptJson = googleReceipt.PayloadData.json,
        Signature = googleReceipt.PayloadData.signature
    }, result =>
    {
        Debug.Log("Validation successful!");
    },
        error =>
        {
            Debug.Log("Validation failed: " + error.GenerateErrorReport());
        }
    );
#endif
    return PurchaseProcessingResult.Complete;
}
PurchasePrice に ×100 をしているのは何で?

上の方でも書きましたが、PlayFab では「RM を 100 で割った数値」がリアルマネーのドルとして計算されます。

なので、RM 100 というのは 1ドル のことですね。

つまり、価格を RM に変換しているというわけです。

ペンギンくん
ペンギンくん
なるほど、わかりにくい。
ねこじょーかー
ねこじょーかー
その辺が詳しく解説されているわけじゃないから、読み取るのが大変…

あとは、PurchaseEventArgs の中身に詰まっている情報を渡してあげればOKです。

余談ですが、iOS と Android の PurchasePrice の型が、int と uint で違っているのが謎すぎます。

レシート検証が無事に通れば、自動的にインベントリにアイテムが入るという仕組みです。

これでひと通り実装が完了しました。

動作確認

動作確認前に iPhone からサインアウトが必要です。

実行して左上のボタンを押すことで、アイテムを購入できます。

ボタンを押すと、サインインの画面が出るので、サンドボックス用のアカウントでログインします。

起動画面
playfab-unity-iap
サインイン
playfab-unity-iap

購入するアイテムによって、アイテム名と金額が変わることがわかります。

アイテムの細かい説明は表示されないようですね。

消耗品
playfab-unity-iap
非消耗品
playfab-unity-iap
サブスクリプション
playfab-unity-iap

ここで「購入する」を押すと、無事に購入が完了します。

画面中央に「Validation Successfull!」が表示されたら正常に完了した証拠です。

完了メッセージ
playfab-unity-iap
レシート検証
playfab-unity-iap

以下は PlayStream のログです。

playfab-unity-iap

また、「プレイヤー > 購入」と進んでいくと、購入履歴の確認も可能です。

playfab-unity-iap

PlayFab ではドル表記になるので、円から逆算された RM が価格として入っていることがわかりますね。

これで、ひと通りの動作を確認することができました。

実際の運用はどうすべきか

ここで少し踏み込んで、実際の運用ではどうすべきかについて考えていきます。

ユーザーがリアルマネーを使うということは、一番バグがないよう気をつけないといけない部分になりますよね。

「課金したのにアイテムが反映されない」などのトラブルになると、ゲームの存続に関わる可能性が高いです。

トラブルのリスクを少しでも減らすため、「できるだけ課金をする回数を減らしてもらう工夫」が必要になると思います。

例えば、以下のような感じですね。

  1. リアルマネーでしか買えない仮想通貨を買ってもらう
  2. その仮想通貨を使用して、専用アイテムを購入してもらう

こうしておけば、以下のように変わります。

playfab-unity-iap

アイテムに仮想通貨を入れて販売するには、バンドルを使うことで実現できます。

バンドルについては、ストアでバンドルを売る手順を参照してください。

最後に

Unity IAP と PlayFab を連携する方法について解説してきました。

IAP は実機でしか動作確認ができないので、毎回ビルドするのがわりと面倒でした。

アプリ開発の終盤になると、容量が増えてビルド時間が長くなるので、容量が小さい最初のうちに実装するのもありかもしれません。

この記事を参考に、みなさんが IAP を統合する助けになれば幸いです。

PlayFab の書籍も好評発売中!
playfab-book

PlayFab のことをもっと皆さんに知ってもらいたくて、合計500ページ以上にもなる書籍を5冊に分けて執筆しました。

私の知識をすべて詰め込んでいるので、ゲーム開発をさらに加速させたい方はぜひご覧ください。

POSTED COMMENT

  1. sato-c より:

    参考になるプログラムをありがとうございます。

    起動画面のBUYボタンの位置はスクリプトから変更できますか?できるのであれば中央に移動したいと思い、OnGUIを編集しているのですが、うまく移動できませんでした。

    • ねこじょーかー より:

      パラメーターを何か操作すればできるとは思いますが、この記事ではアプリ内課金の動作確認を目的としているので、特に移動しなくても問題なくご確認いただけると思います。

  2. エリ より:

    ねこじょーかーさん、お世話になります。
    unityでこちらのサブスクリプション課金を実装したいと思い
    記事を参考に一通り実装できました。
    有難うございます。
    アイテム購入後のその後の運用についてどうしたらよいか悩んでいます。
    playFabでアイテムがイベントで有効になっているか確認して処理を分岐していくのでしょうか?
    アドバイスいただけましたら幸いです。
    お本やYouTubeも有難いです。
    よろしくお願い致します。

    • ねこじょーかー より:

      エリさん、こんにちは!
      ブログだけでなく書籍や動画もご覧いただき、とても嬉しいです〜!

      >アイテム購入後のその後の運用についてどうしたらよいか悩んでいます。
      >playFabでアイテムがイベントで有効になっているか確認して処理を分岐していくのでしょうか?
      こちらのご質問についてですが、アイテムを購入後「アイテムが無効になっている」というのは時間でアイテムを消費する設定にした場合のみで、カウントで消費する場合や永続型のアイテムのときはインベントリに残っていないでしょうか?
      また、すでに消費されたアイテムについては、インベントリを取得したときに自動で除外されていたような記憶があり、特にアイテムの有効無効を気にする必要がないように思いましたが、いかがでしょう?

  3. エリ より:

    ねこじょーかーさん、こんにちは。
    さっそくのご回答ありがとうございます
    実はこちらの記事のサブスク型1ヶ月消費型の課金実装したい!と思い
    購入処理まではできたのですがアイテム消費後にUnityでどう処理したらいいのか悩んでいます。
    使用回数やなん分など短い期間の効果の付与はイメージできるのですが1ヶ月の期間をどう追跡したらいいのかな。。と。
    まぎらわしい質問ですいません。
    よろしくお願い致します。

    • ねこじょーかー より:

      なるほど、サブスクアイテムの期間の追跡についてですね!
      「アクションとルール」の機能は使用されたことありますか?
      アイテムが消費された場合、com.playfab.player_consumed_item のイベントが発火されるため、このイベントを検知することで実現できるかと思います。
      ただし、以下のフォーラムにある通り、インベントリを参照したり追加したりしたタイミングで有効期限が評価されるようなので、有効期限が切れたら即時でイベントが発火される仕組みではないようです。
      https://community.playfab.com/questions/5874/item-expiration-and-events.html

      本をお持ちでしたら、自動化編で Azure Functions を使ったイベントの検知方法をご紹介しているので、そのあたりが参考になるかと思います〜!

      • エリ より:

        アドバイスありがとうございます!
        アプリ起動時のインベントリの取得だけでも有効性が確認できると思いました。
        有難うございます!

COMMENT

メールアドレスが公開されることはありません。