Unity が用意してくれている、スマートフォンアプリ内課金まわりに関する便利機能、『Unity In-App Purchase』。
最近また触れる機会があったので、少しまとめて記事にしてみようと思いました。
いわゆるゼロからの導入ガイド的なものではなく、スクリプトの中身に関する簡単な解説になります。
試行環境:Unity 5.5.6f1
Unity IAP バージョン:1.20.0
(試行環境が少し古い Unity5系 なので、最新版では一部内容が異なるかも知れません、ご注意ください)
導入については、元 Unity の中の人 伊藤 周 氏 による、こちらの資料も参考になりますので併せてどうぞ。
目次
IStoreListener:課金処理の管理用インターフェース
OnInitialized:課金管理クラスの初期化処理
Unity – Scripting API: Purchasing.IStoreListener
Unity – Scripting API: Purchasing.IStoreListener.OnInitialized
インターフェース『IStoreListener』を継承したクラスが『課金管理クラス』のような形になります。
何は無くとも、この管理クラスを作成するところから始まります。
課金が行われた際に、このクラスの特定の関数たちが呼び出され、中に記述された処理が実行されていきます。
コールバック処理を外から渡せるようにしたい場合は、Singleton クラスにしておくと便利かと思います。
初期化関数 OnInitialized の中で、課金コンテンツの登録と UnityPurchasing クラスの初期化を実行しておきます。
// あらかじめ using UnityEngine.Purchasing を宣言しているものとします.
private IStoreController mStoreController;
private IExtensionProvider mExtensionProvider;
void Start()
{
LogPurchasing("PurchasingManager Start");
// Apple / Google ストア間で, 課金コンテンツの ID が同一である前提の書き方.
// 同一でない場合はプラットフォームごとに分岐させる.
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance())
.AddProduct("jp.bardaxel.hogehogegame.item_unlock_01", ProductType.NonConsumable) // 課金コンテンツ:アイテム解放01.
.AddProduct("jp.bardaxel.hogehogegame.character_unlock_01", ProductType.NonConsumable) // 課金コンテンツ:キャラクター解放01.
.AddProduct("jp.bardaxel.hogehogegame.story_unlock_01", ProductType.NonConsumable) // 課金コンテンツ:ストーリー解放01.
.AddProduct("jp.bardaxel.hogehogegame.item_unlock_02", ProductType.NonConsumable); // 課金コンテンツ:アイテム解放02.
// この後, OnInitialized が呼ばれる.
UnityPurchasing.Initialize(this, builder);
}
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
mStoreController = controller;
mExtensionProvider = extensions;
LogPurchasing("PurchasingManager Initialized");
}
ConfigurationBuilder.Instance.AddProduct によって、『iTunesConnect』や『Google Play Console』で登録している課金コンテンツを追加していきます。
『OnInitialized』が走る際に渡される controller と extensions は、後にいろいろ使用するため、メンバ変数として保持しておくと便利です。
余談ですが、課金コンテンツの ID はストア内で唯一性が要求されるため、『アプリのバンドル ID + 課金コンテンツを表す文字列』にしておくと無難です。
また、Apple では ID に大文字が使用できますが、Google Play の方はすべて小文字になってしまうため、iOS/Android 両ストアで同じ ID にしたい場合は要注意です。
以下では、『IPurchaseListener』の中で、『OnInitialized』の他に定義されている主な関数について取り上げていきます。
OnInitializeFailed:課金管理クラスの初期化失敗時に走る処理
Unity – Scripting API: Purchasing.IStoreListener.OnInitializeFailed
初期化失敗時に呼び出される関数です。
インターネットが利用できない環境の場合については、これが呼び出されるわけではなく、オンラインになるまで継続的に初期化を試みるようです。
public void OnInitializeFailed(InitializationFailureReason error)
{
// 初期化失敗時に実行する処理.
if (mFuncInitializeFailed != null)
{
mFuncInitializeFailed();
}
LogPurchasing("PurchasingManager OnInitializeFailed : " + error.ToString());
}
IStoreController.InitiatePurchase:各プラットフォームの購入画面を呼び出す処理
Unity – Scripting API: Purchasing.IStoreController.InitiatePurchase
こちらは『IPurchaseListener』ではなく、『OnInitialized』で渡された『IStoreController』が持つ関数です。
実際にプレイヤーが「購入します」とボタン押下などで意思決定をした際に、走らせるべき関数となります。
これが実行されると、iOS/Android それぞれで用意された課金用のダイアログが表示されます。
IPurchaseListener に、これを走らせるための public 関数を作成し、ゲーム内から呼び出せるようにしておくと良いかと思います。
public void OnPurchase(string product_id)
{
LogPurchasing("PurchasingManager OnPurchase");
mPurchasedProductId = product_id;
mStoreController.InitiatePurchase(mPurchasedProductId);
}
話を広げ過ぎないために、ここでは string で ID を直接渡していますが、実際は enum 定義を用意しておいて、Dictionary<enum, string> のような形で ID 文字列を参照できるようにしておくと便利かと思います。
ProcessPurchase:購入成功時に走る処理
(なぜか『ProcessPurchase』の公式ドキュメントが無かった…)
購入が成功したときに呼び出される関数です。
『PurchaseProcessingResult』という型で戻り値を返す必要があり、返す値は
- PurchaseProcessingResult.Complete:課金処理の流れが完了済み
- PurchaseProcessingResult.Pending:課金処理の流れが未完了
のどちらかになります。
Unity – Scripting API: Purchasing.PurchaseProcessingResult
購入処理の完了は、IStoreController.ConfirmPendingPurchase の呼び出しを持って判定します。
OnInitialized において、StoreController を保持しておくと便利、と述べていたのはこの辺りにかかってきます。
課金情報をローカルに保存するだけであれば、即座に IStoreController.ConfirmPendingPurchase を呼び出し、Complete を返して良いかと思います。
(少し話が逸れますが、課金情報の保存やレシート検証をローカルで実行する場合、当然ながらチートの危険性について意識する必要があります)
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
{
LogPurchasing("PurchasingManager ProcessPurchase");
// レシート情報やトランザクション ID を保持する場合はこんな感じ.
string receipt = e.purchasedProduct.receipt;
string transaction_id = e.purchasedProduct.transactionID;
// 購入後に実行する処理.
// ここでコンテンツのアンロックや課金情報の保存を実行しておく.
if (mFuncPurchaseComplete != null)
{
LogPurchasing("PurchaseComplete!!!");
mFuncPurchaseComplete();
}
// この時点で, 一連の購入処理が完了したものとする.
// 購入したコンテンツ ID は, enum を Key とした Dictionary などで参照しやすくしておくと便利.
mStoreController.ConfirmPendingPurchase(mStoreController.products.WithID(mPurchasedProductId));
return PurchaseProcessingResult.Complete;
}
(この例では、レシート検証を行わない形となっています。レシート検証についても、まとめられそうであれば別途記事にしようかと思います。)
一方、課金情報をサーバに送って、サーバ側でレシート検証などを行う場合、サーバの応答を待っている間は Pending を返すこととなります。
Pending が返され続けて、IStoreController.ConfirmPendingPurchase が実行されずにゲーム終了などした場合、ゲーム再起動時に未完了のままだった処理が走り直す、とされています。
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
{
LogPurchasing("PurchasingManager ProcessPurchase");
// レシート情報やトランザクション ID を保持する場合はこんな感じ.
string receipt = e.purchasedProduct.receipt;
string transaction_id = e.purchasedProduct.transactionID;
// ストアでの決済が完了したら, サーバへ購入情報を送信する.
// NetworkRequest.PurchaseContent を, サーバへ購入情報を送る関数とする.
// 引数は, 送信の完了が確認できた際に実行されるコールバック処理を渡しているものとする.
NetworkRequest.PurchaseContent(() =>
{
// 購入後に実行する処理.
if (mFuncPurchaseComplete != null)
{
LogPurchasing("PurchaseComplete!!!");
mFuncPurchaseComplete();
}
// 送信完了したら, 一連の購入処理が完了したものとする.
// 購入したコンテンツ ID は, enum を Key とした Dictionary などで参照しやすくしておくと便利.
mStoreController.ConfirmPendingPurchase(mStoreController.products.WithID(mPurchasedProductId));
return PurchaseProcessingResult.Complete;
});
LogPurchasing("PurchasePending...");
// 送信が完了するまでは, Pending を返す.
return PurchaseProcessingResult.Pending;
}
OnPurchaseFailed:購入失敗時に走る処理
購入が失敗したときに呼び出される関数です。
public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
{
// 課金失敗時に実行する処理.
if (mFuncPurchaseFailed != null)
{
mFuncPurchaseFailed();
}
LogPurchasing("PurchasingManager OnPurchaseFailed : " + p.ToString());
}
『課金管理クラス』での、実際の処理の流れはこのような感じになります。
どこにどのような処理が書かれるべきなのか、コードを見て1つ1つ取り上げていくことで、少し理解しやすくなるのではないでしょうか。
レシート検証などの後編へ続く…かも…。
© Unity Technologies Japan/UCL