エンジニア

スマートフォンアプリでのUnityIAPの導入と実装

投稿日:2021年7月8日 更新日:

はじめに

こんにちは。タノシムスタジオでクライアントエンジニアをしている山城です。
今回は、UnityのスマートフォンアプリでのUnityIAPという機能を使ったアプリ内課金の導入と実装について解説しようと思います。
本記事で使用するUnityのバージョンは2019.4.6fになります。

UnityIAPの導入

Servicesの有効化

Unityメニューの「Window」→「General」→「Services」からServicesウィンドウを開きます
UnityIAP_SelectServices

UnityのServesを使用するにはUnityDashboard上にプロジェクトを追加する必要があります。
「Select organization」から新しくプロジェクトを作成する組織を選択するか、「I already have a Unity Project ID」から作成済みのプロジェクトを選択してください。
UnityIAP_SelectOrganize

その後、サービス一覧が表示されるので、「In-App Purchasing」を選択してください。
※注釈にもありますが、In-App Purchasingを有効にすると、UnityAnalyticsも一緒に有効化されます
UnityIAP_Enable

アプリが13歳未満を対象とするアプリかどうかきかれますので、開発中のアプリに応じて選択してください。今回は選択せずに進めます。
※アメリカのCOPPA(児童オンラインプライバシー保護法)に対応するためのものです
UnityIAP_COPPA

Packageのインストール

選択後、メニューからEnableボタンもしくはトグルボタンを押すことで有効化が完了します。
本来なら有効化後のメニューからPackageをインポート出来るのですが、現在メニューからインポートするPackageは古いバージョンなので、PackageManager経由でインストールしてください(執筆時点ではcom.unity.purchasingのv3.2.2)
Unityメニューの「Window」→「Package Manager」からPackageManagerメニューを開き、「In App Purchasing」からInstallを押します。

UnityIAP_InstallIAP

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導入の際に本記事が参考になれば幸いです。

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-, , ,

© WonderPlanet Inc.