simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

PlayFab:Unityゲーム内課金システムPayPal編

個人ゲーム制作でも何百万人にも膨れ上がるユーザーを平気で捌ける順応性のあるバックエンドサービスを短期無料で作れる時代になりました。
Amazon の GameSparks, Google の Firebase, Microsoft の PlayFab, 富士通クラウドテクノロジーズのmobile backend など様々なところから mobile backend as a service (mBaaS)というものが、ウチが一番だ、これから成長するんだと宣伝している時代です。

中でも、ゲーム内に登場するアイテムを実際のお金(USDや日本円)で購入するシステムを提供している Microsoft の PlayFab を、今回は日本語の紹介が少ない中、実際に動かして解説していきたいと思います。

初見さんで PlayFab を Unity で使えるようにする手順が気になる方はこちらを一読してなぞってみましょう。
qiita.com
要約すると PlayFab のアカウントとゲームタイトルを作成して
PlayFab SDK 使うための Unity アセットをインポート後
タイトルIDをエディタ拡張で記入すれば
アカウント作成 or ログインのクライアント API 呼び出しが成功することを確認できます。(慣れてくると数十分の手作業)

もっと具体的には次の一行を Start に置くだけの作業

    public void Start()
    {
        PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest
        { CustomId = "GettingStartedGuide", CreateAccount = true },
        OnLoginSuccess, OnLoginFailure);
    }

以降は、上記のカスタムIDにリンクしたプレイヤーとしてゲームの API を叩くことを意識しておいてください。
(何このログイン、キモ!?と思う方は前の記事の「プレイヤー認証」を読んでね)

PlayFab の他に支払い処理をするアドオンとの連携が必須

Microsoft Store, Google PlayApp Store などの配信サービスにアップロードせず、Windows 用のゲームとして審査いらずで課金システムを設けることができます。(そんな手段も取れます)
自身の Web サイトからゲーム本体をダウンロードできるような仕組みにしても良いですね。
ただしクレジットカード情報や実際のお金の移動はデリケートな処理です。そもそもカード会社が我々を信用しない…
そこで PlayFab のアプリ内課金を支援する3rdパーティ製のアドオンにその大事な情報を任せて、
金銭授受の結果だけをゲームに反映させることができる機能が PlayFab にはあります。(もちろん金の移動先はあなたの銀行口座です。)

早くそのやり方を確認したい!という方、細かい手順はすべて次の公式ドキュメントにまとめられています。(省略せず、以下は具体的な手順を日本語で解説してます。)
Non-Receipt Payment Processing
アドオンは PayPal や Steam, Amazon などいくつかありますが、中でもめちゃめちゃ簡単なのが Steam とのこと
でも Steam の開発者登録とアプリ登録って数週間は待つことになるので、本記事はすぐに確認できる PayPal のビジネスアカウントを使って動作確認してみました。
(ゲーム内処理は Steam とほとんど同じです、容易に切り替えも可能)

本当に3rdParty製のアドオンとの連携が必要なんですか?PlayFab単体でどうにかできませんか?→無理です。何かしらの 3rdParty 製のサービスで振り込み先の銀行口座を登録してください。(次が情報ソース)
community.playfab.com

PayPal のビジネスアカウントを作る

初めての人は次のリンクから
PayPal(ペイパル) - かんたん&安全なオンライン決済サービス
にサインアップ&ログインします。

アカウントの種類は2つありますが、今回の課金システムを試すなら受け取りも可能なビジネスアカウントにしましょう。

f:id:simplestar_tech:20190816213711p:plain
あなたが作りたいのはビジネスアカウントですよね

基本的に個人での登録であること、本名と住所を記入して、本人確認のフローを通せばビジネスアカウントの登録手順で詰まることはありません。
長い間パーソナルアカウントだった私は、アカウントの種類変更のアップグレードボタンを押したら、個人情報の確認いらずでビジネスアカウントに秒で昇格しました…

f:id:simplestar_tech:20190816213935p:plain
なんか、証明書類用意する前に振り込み可能なビジネスアカウントできちゃった

個人情報の入力を行ってビジネスアカウントができたなら、PlayFab の Add-onタブにて PayPal のインストールを行います。

f:id:simplestar_tech:20190816093749p:plain
Add-On タブにて PayPal をインストール

記入するのはマーチャントIDだけ、これは PayPal のアカウント情報として確認可能(この情報は公開しちゃいけない)
もう一つの必須項目の return link ってなんでしょう?(世界中から同じ質問が湧いています)
Questions on PayPal payments - Playfab Community
何のことはなく、支払いが完了したときに「お買い上げありがとうございます!」とユーザに示すあなた独自の Web ページの url を指定するとのことでした。(紛らわしい!)
可能ならそのページから、Unity ゲームにフォーカスを戻せたら良いですね。

現在は次の状態

f:id:simplestar_tech:20190816102412p:plain
PayPal ページをアクティブ化

PayPal のビジネスアカウントが必要なのは理解できたと思います。
そのビジネスアカウントのときに現れる歯車マークの設定ボタンから、サードパーティのアクセスを管理する項目「アカウント設定」が初めて選べるようになります。
ここは PlayFab の Add-on の説明にあるとおり、API を 3rd Party ここでいう PlayFab を信頼して処理を代行させる許可を与えるため API 利用者を探す欄に "billing_api1.playfab.com" を記入して
たくさんの許可項目があるなか一つ process Express Checkout payments に該当する「エクスプレス チェックアウトを使って支払いを処理します。」にだけ許可チェックを入れて登録を完了させます。

あと、この先もしこのビジネスアカウントで、支払いをしようとログインしようとしたら、「支払い先とは別のアカウントにしてよ」とメッセージが出てログインに失敗しますので
支払われる様子を最後まで試したい人は、別のメールアドレスでパーソナルアカウントを作っておくと良いでしょう。

ゲーム内の「カタログ」に現実のお金「RM」単位の金額を設定する

これは PlayFab の決まりごとなのですが、セキュアにアイテムの購入・販売をするための仕組みとして、必ず PlayFab ではカタログと呼ばれる価格設定されたアイテムを複数内包する、まさにカタログを定義して利用することになります。
ここで、PayPal を使った購入対象としてカタログとアイテムを指定する時、カタログにてアイテムに価格設定として RM (実際のお金、アメリカドルの0.01つまり1セント1RMという)単位を指定しておかないと PayPal の支払いオプションが入手できないエラーが発生します。
ので、ちゃんとカタログを作るときに RM で、そうですね 1000 RM とすれば 10 ドルのアイテムとして購入することが試せるようになります。

PlayFab のカタログの作り方、理解は次の公式ドキュメントを読むだけでバッチリでした。
カタログ - PlayFab | Microsoft Docs

材料は揃ったのでコードの動作確認をする

先に紹介済みですが、コード例と意味は次の公式ドキュメントが全てを漏れなく解説済みです。
Non-Receipt Payment Processing

これに沿って動かしたときの絵を見てみましょう。
実際にテストに使ったコードがこちら

using PlayFab;
using PlayFab.ClientModels;
using System.Collections.Generic;
using UnityEngine;

public class PlayFabSample : MonoBehaviour
{
    public void Start()
    {
        PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest
        { CustomId = "GettingStartedGuide", CreateAccount = true },
        OnLoginSuccess, OnLoginFailure);
    }

    private void OnLoginSuccess(LoginResult result)
    {
        Debug.Log("Congratulations, you made your first successful API call!");
    }

    private void OnLoginFailure(PlayFabError error)
    {
        Debug.LogWarning("Something went wrong with your first API call.  :(");
        Debug.LogError("Here's some debug information:");
        Debug.LogError(error.GenerateErrorReport());
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            PlayFabClientAPI.StartPurchase(new StartPurchaseRequest()
            {
                CatalogVersion = "main",
                Items = new List<ItemPurchaseRequest>() {
                    new ItemPurchaseRequest() {
                        ItemId = "apple",
                        Quantity = 1,
                        Annotation = "Purchased via in-game store"
                    }
                }
            }, OnSuccessOfStartPurchase, 
            error =>
            {
                // Handle error
            });
        }
    }

    void OnSuccessOfStartPurchase(StartPurchaseResult r)
    {
        Debug.Log($"result.OrderId = {r.OrderId}");
        PlayFabClientAPI.PayForPurchase(new PayForPurchaseRequest()
        {
            OrderId = r.OrderId,
            ProviderName = "PayPal",
            Currency = "RM"
        }, result => {
            // Handle success
            Debug.Log($"result.PurchaseConfirmationPageURL = {result.PurchaseConfirmationPageURL}");
            Application.OpenURL(result.PurchaseConfirmationPageURL);
            // 何らかの待機処理を入れて、ここで「お買い上げありがとうございました」の return url からゲームにフォーカスが戻ってくるまで待機する
            OnSuccessOfPayForPurchase(result);
        }, error => {
            // Handle error
        });
    }

    void OnSuccessOfPayForPurchase(PayForPurchaseResult r)
    {
        PlayFabClientAPI.ConfirmPurchase(new ConfirmPurchaseRequest()
        {
            OrderId = r.OrderId
        }, result => {
            // Handle success
            Debug.Log($"result.Items[0].DisplayName = {result.Items[0].DisplayName}");
        }, error => {
            // Handle error
        });;
    }
}

このコードが最後まで成功するまで色々エラーを見てきました。

まず、カタログが無いと、不正なアイテム情報の指定だ、カタログ作れと怒られたり
正しいアイテムを指定しても、それには価格設定が無いから購入対象外だの
PayPal での購入処理をキャンセルしたら ConfirmPurchase の結果を取得してみるとエラー処理に入り
FailedByPaymentProvider で FinalizeTransaction failed, Ack=FailureExpress Checkout PayerID is missing. The PayerID value is invalid. と出るわ
支払いを逃げたからかな?

そこで、支払い用の別の PayPal アカウントを作って実際に 10 USD のアイテムを支払ってみた。
確かに支払われている様子。

f:id:simplestar_tech:20190816231203j:plain
ゲーム内操作で表示されたWebページを承認すると、自身のビジネスアカウントに振り込まれる

この時、ConfirmPurchase は実行せずとも、 StartPurchase で指定したアイテムと個数が Player のインベントリのデータベースに記録されていることを確認できました。

f:id:simplestar_tech:20190816231853j:plain
インベントリに購入したアイテムが自動で追加されている様子
カタログの仕組みを利用すれば、課金完了と同時にアイテムは自動でプレイヤーの手に渡ってくれる親切設計です。

ConfirmPurchase は、あくまで、ちゃんと購入が完了できましたよ、あなたの買ったアイテムはこれこれですよと表示するために行うものでしょう。
(クライアントからアイテム追加のイベントはキックできないので…)

USD 表示のものを登録した銀行口座に出金してみたが…処理が数分では終わらず、どうも数日かかる模様?
いいえ、後で確認した感じ JPY 換算を実行して USD 表示から JPY 表示にしてから資金の移動を行うと正常に処理されました(円で手数料が引かれて)
資金の移動には換算が必要な工程であることを覚えておくと良いかもしれません

f:id:simplestar_tech:20190816232221p:plain
PayPal を使うたびに手数料がかかります。
PayPal の決済手数料はここに書かれている通り
決済手数料|ビジネス向け-PayPal(ペイパル)

ゲーム内通貨を現実のお金で購入してもらい、ゲーム内のお金のやりとりは PlayFab が用意している購入のフローを実行するというのが PlayFab のデザインのようです。
ゲーム内の店舗でのアイテム購入の説明はこちらを参照します。
ストアのクイックスタート - PlayFab | Microsoft Docs

その他、プレイヤー間でアイテムや金銭のトレードを行うようにも API が設計されています。
取り引き - PlayFab | Microsoft Docs
これを利用すれば、不正が行われること無く、活発にゲーム内でプレイヤー同士の交易が行われそうです。

まとめ

バックエンドサービスの群雄割拠時代の PlayFab の位置付け
Unity から PlayFab を1プレイヤーとして利用する手順
3rd パーティ製のアプリ内購入システムを利用しなければならないこと
PayPal と PlayFab との連携手順
実際の購入フローの実演と、登録した口座に振り込まれることの確認
ゲーム内仮想通貨を利用した購入と交易のデザインがあること
を順に確認してきました。

ずっとこういう実際のお金が動くゲームシステムってどうやって作るんだろうと、スキル Zero の状態でしたが
これでゼロが1になった感じです。
まだゲームコンテンツをこれから作っていく段階ですし、そもそもまだそういうお金が実際に動くゲームにするか決めていませんが
選択肢として PlayFab を Unity に導入すれば、課金システムを容易に組み込めることがわかって、個人的に満足です。

みなさんのバックエンド開発・サービス選びの参考になれば幸いです。
ここまで読んで頂きありがとうございました。