はじめに
こんにちは。タノシムスタジオでクライアントエンジニアをしている山城です。
今回は、UnityのスマートフォンアプリでのUnityIAPという機能を使ったアプリ内課金の導入と実装について解説しようと思います。
本記事で使用するUnityのバージョンは2019.4.6fになります。
UnityIAPの導入
Servicesの有効化
Unityメニューの「Window」→「General」→「Services」からServicesウィンドウを開きます
UnityのServesを使用するにはUnityDashboard上にプロジェクトを追加する必要があります。
「Select organization」から新しくプロジェクトを作成する組織を選択するか、「I already have a Unity Project ID」から作成済みのプロジェクトを選択してください。
その後、サービス一覧が表示されるので、「In-App Purchasing」を選択してください。
※注釈にもありますが、In-App Purchasingを有効にすると、UnityAnalyticsも一緒に有効化されます
アプリが13歳未満を対象とするアプリかどうかきかれますので、開発中のアプリに応じて選択してください。今回は選択せずに進めます。
※アメリカのCOPPA(児童オンラインプライバシー保護法)に対応するためのものです
Packageのインストール
選択後、メニューからEnableボタンもしくはトグルボタンを押すことで有効化が完了します。
本来なら有効化後のメニューからPackageをインポート出来るのですが、現在メニューからインポートするPackageは古いバージョンなので、PackageManager経由でインストールしてください(執筆時点ではcom.unity.purchasingのv3.2.2)
Unityメニューの「Window」→「Package Manager」からPackageManagerメニューを開き、「In App Purchasing」からInstallを押します。
UnityIAPの実装
UnityIAPの実装方法にはCodelessIAPというスクリプトを書かずに実装する方法と、スクリプトを書いて実装する方法があります。
今回はスクリプトを書く方法の実装例を紹介します。
IStoreListenerの継承
UnityIAPはIStoreListenerを継承したクラスを元にストアのイベントを処理します。
必要な内容は以下になります。それぞれの詳細な実装は次の項目で解説します。
using UnityEngine.Purchasing;
public class MyInAppPurchase : IStoreListener
{
// 初期化の成功
void IStoreListener.OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
}
// 初期化の失敗
void IStoreListener.OnInitializeFailed(InitializationFailureReason error)
{
}
// ストア上での購入の成功
PurchaseProcessingResult IStoreListener.ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
return PurchaseProcessingResult.Complete;
}
// ストア上での購入の失敗
void IStoreListener.OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
}
}
初期化イベント
UnityIAPは初期化時にストアのプロダクト(商品)IDを設定する必要があります。
AppStoreとGooglePlayでプロダクトIDが異なる場合、それぞれをひとまとめにして登録することも可能です。
public class MyInAppPurchase : IStoreListener
{
IStoreController storeController = null;
IExtensionProvider extensionProvider = null;
// 初期化
public void Initialize()
{
var module = StandardPurchasingModule.Instance();
module.useFakeStoreAlways = false;
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(), module);
// プロダクトIDの登録
builder.AddProduct("com.sample.unityiap.product01", ProductType.Consumable);
// ストア毎にプロダクトIDが異なる場合
builder.AddProduct("com.sample.unityiap.product02", ProductType.Consumable, new IDs
{
{"com.sample.unityiap.ios_product02", AppleAppStore.Name},
{"com.sample.unityiap.android_product02", GooglePlay.Name}
});
UnityPurchasing.Initialize(this, builder);
}
// 初期化の成功
void IStoreListener.OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
// 購入処理に必要なので保持しておく
storeController = controller;
// リストア処理に必要なので保持しておく
extensionProvider = extensions;
}
// 初期化の失敗
void IStoreListener.OnInitializeFailed(InitializationFailureReason error)
{
// 初期化失敗時の処理
switch (error)
{
case InitializationFailureReason.PurchasingUnavailable: // デバイス設定でアプリ内購入が無効になっている
break;
case InitializationFailureReason.NoProductsAvailable: // 購入可能なプロダクトがない
break;
case InitializationFailureReason.AppNotKnown: // 不明なアプリ
break;
}
}
...
}
購入処理
購入処理には、IStoreListener.OnInitializedの引数で渡されたIStoreControllerを使用します。
購入にはプロダクトIDか、IStoreController.productsから取得できるProductデータを使用します。
public class MyInAppPurchase : IStoreListener
{
..
// 購入処理開始
public void InitiatePurchase(string productId)
{
if (storeController == null) return;
storeController.InitiatePurchase(productId);
}
// ストア上での購入の成功
PurchaseProcessingResult IStoreListener.ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
// TODO:アイテムの付与や機能の開放など、購入されたプロダクトに応じた処理
return PurchaseProcessingResult.Complete; // 購入処理を完了する
}
// ストア上での購入の失敗
void IStoreListener.OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
// 購入失敗時の処理
switch (failureReason)
{
case PurchaseFailureReason.PurchasingUnavailable: // 購入機能が無効になっている
break;
case PurchaseFailureReason.ExistingPurchasePending: // 既に購入処理が進行している
break;
case PurchaseFailureReason.ProductUnavailable: // 購入不可能なプロダクト
break;
case PurchaseFailureReason.SignatureInvalid: // レシートの署名検証に失敗
break;
case PurchaseFailureReason.UserCancelled: // ユーザーが購入をキャンセルした
break;
case PurchaseFailureReason.PaymentDeclined: // 支払いに問題があった
break;
case PurchaseFailureReason.DuplicateTransaction: // トランザクションの重複
break;
case PurchaseFailureReason.Unknown: // 上記以外の認識されていないエラー
break;
}
}
}
また、開発中のアプリによっては購入時にレシート検証を別途サーバで行ったり、購入情報をクラウドに保存する場合があると思います。
その場合は、IStoreListener.ProcessPurchaseの戻り値をPurchaseProcessingResult.Pendingにし、処理の完了時にIStoreController.ConfirmPendingPurchaseを呼び出す必要があります。
public class MyInAppPurchase : IStoreListener
{
..
Action<string> onPurchaseComplete = null;
Product pendingPurchasedProduct = null;
// 購入処理開始
public void InitiatePurchase(string productId, Action<string> onComplete)
{
if (storeController == null) return;
onPurchaseComplete = onComplete;
storeController.InitiatePurchase(productId);
}
// ストア上での購入の成功
PurchaseProcessingResult IStoreListener.ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
pendingPurchasedProduct = purchaseEvent.purchasedProduct;
onPurchaseComplete?.Invoke(purchaseEvent.purchasedProduct.receipt);
return PurchaseProcessingResult.Pending; // 購入処理をIStoreController.ConfirmPendingPurchaseが呼ばれるまでPendingする
}
// Pending中の購入処理を完了させる
public void ConfirmPendingPurchase()
{
if (null == pendingPurchasedProduct)
{
Debug.LogError("ProcessPurchaseが呼ばれる前にConfirmPendingPurchaseが呼ばれた");
return;
}
storeController.ConfirmPendingPurchase(pendingPurchasedProduct);
pendingPurchasedProduct = null;
}
}
リストア
プロダクト購入後の処理をサーバなどで行う場合、通信の切断やアプリの再起動などによって購入処理が中断されることがあります。
その際に、購入処理を復元し、再度購入フローをやり直すことをリストアと呼びます。
UnityIAPの処理としては、IStoreListener.ProcessPurchaseで戻り値をPendingにしたあと、IStoreController.ConfirmPendingPurchaseを呼び出せなかった時にリストア処理が必要になります。
UnityIAPでリストア処理を行う場合、IStoreListener.OnInitializedの引数で渡されるIExtensionProviderからリストア処理を呼び出すことができます。
RestoreTransactionsを実行後、pending中のプロダクトがあった場合IStoreListener.ProcessPurchaseが再度呼び出されます。
public class MyInAppPurchase : IStoreListener
{
..
public void RestoreTransactions(Action<string> onPurchaseComplete)
{
if (extensionProvider == null) return;
this.onPurchaseComplete = onPurchaseComplete;
#if UNITY_IOS
extensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(RestoreCallback);
#elif UNITY_ANDROID
extensionProvider.GetExtension<IGooglePlayStoreExtensions>().RestoreTransactions(RestoreCallback);
#endif
}
// リストア実行結果
void RestoreCallback(bool restoreResult)
{
// trueの場合、リストアが完了した or pending中のプロダクトがなかった
if (restoreResult)
{
onPurchaseComplete = null;
}
}
}
[補足]UnityAnalyticsを無効にする方法
UnityIAPはUnityAnalyticsに依存しており、Services上でUnityAnalyticsを有効にしておく必要がありますが、アプリによってはUnityAnalyticsが有効だと不都合な場合があると思います。
その場合、アプリの起動時に以下のコードを実行すれば内部でUnityAnalyticsを無効化することができます
UnityEngine.Analytics.Analytics.enabled = false;
UnityEngine.Analytics.Analytics.deviceStatsEnabled = false;
UnityEngine.Analytics.Analytics.limitUserTracking = true;
#if UNITY_2018_3_OR_NEWER
UnityEngine.Analytics.Analytics.initializeOnStartup = false;
#endif
まとめ
今回はスマートフォンアプリでのUnityIAPの導入・実装方法を解説してみました。
私は過去にiOS・Androidのネイティブアプリでの課金実装と、Unityのサードパーティ製アセットでの課金実装を行ったことがありますが、UnityIAPはそのどちらよりも導入が簡単でシンプルな実装が可能でした。Unity公式の機能ですので各OSのアップデートに伴う煩わしいメンテナンスも必要ありません(UnityIAP自体のアップデート情報はキャッチアップする必要がありますが)。
なので、アプリ内課金を予定されている場合はUnityIAPを検討してみてはいかがでしょうか。
もしUnityIAP導入の際に本記事が参考になれば幸いです。