Unity IAPを試してみた

こんにちは。ネイティブエンジニアのszです。

Unity5.3で公式サポートされたUnity IAP(In App Purchasing)の機能について、動作を確認してみました。
メモ程度の情報ですが、まだあまり情報がなさそうなのでご紹介したいと思います。

いくつか日本語で説明されている記事もありますが、やってみた所感では以下のUnityのチュートリアルとスクリプトリファレンスを地道に追っていくのが無難でした。

http://unity3d.com/jp/learn/tutorials/topics/analytics/integrating-unity-iap-your-game

「Making a Purchase Script」あたりにあるスクリプトをベースに動かしてみました。
また、「Assets/Plugins/UnityPurchasing/script/IAPDemo.cs」あたりも参考になりそうです。

試したのはiOSでの消費型アイテムの購入と、SKPaymentQueueあたり(購入結果を安全にサーバに反映するために必要な所)の動作確認です。

導入や全体の説明は以下のスライドがわかりやすいと思います。

http://www.slideshare.net/KeizoNagamine/unity53-56088769

サンプルコード

この記事ではここから引用して簡単な説明を書きます。

using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using System;

/*
   Unityチュートリアルのスクリプトベースのコードです。
http://unity3d.com/jp/learn/tutorials/topics/analytics/integrating-unity-iap-your-game
*/

namespace sample{

    public class StoreListenerTest : IStoreListener{

        private const string kProductNameConsumable = "プロダクトID";//今回はstoreSpecificIdとidを同じにする

        private static IStoreController m_StoreController; // Reference to the Purchasing system.
        //private static IExtensionProvider m_StoreExtensionProvider;

        //中断テスト用
        private bool m_ProcessComplete = true;

        public bool ProcessComplete{
            set { m_ProcessComplete = value; }
            get { return m_ProcessComplete; }
        }

        public void Init(){

            var module = StandardPurchasingModule.Instance();
            module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
            var builder = UnityEngine.Purchasing.ConfigurationBuilder.Instance(module);

            // Productの登録
            builder.AddProduct( kProductNameConsumable, ProductType.Consumable, new IDs
                    {
                    { kProductNameConsumable, GooglePlay.Name, AppleAppStore.Name }
                    });

            UnityPurchasing.Initialize(this, builder);
        }

        //Test UI Click
        public void ChangeProcessComplete(){
            m_ProcessComplete = !m_ProcessComplete;
            Debug.Log("m_ProcessComplete:" + m_ProcessComplete);
        }

        //Test UI Click
        public void BuyConsumable(){
            // Buy the consumable product using its general identifier. Expect a response either through ProcessPurchase or OnPurchaseFailed asynchronously.
            BuyProductID(kProductNameConsumable);
        }

        //Test UI Click
        public void ConfirmPendingPurchase(){
            Product product = m_StoreController.products.WithID(kProductNameConsumable);
            if(product != null){
                Debug.Log("ConfirmPendingPurchase()");
                m_StoreController.ConfirmPendingPurchase(product);
            }
        }

        void BuyProductID(string productId){

            if(m_StoreController == null){
                Debug.LogError("Not Initialize !!!");
                return;
            }

            // If the stores throw an unexpected exception, use try..catch to protect my logic here.
            try{
                // ... look up the Product reference with the general product identifier and the Purchasing system's products collection.

                foreach(Product pd in m_StoreController.products.all){
                    Debug.Log("availableToPurchase:" + pd.availableToPurchase);

                    Debug.Log("id:" + pd.definition.id);
                    Debug.Log("type:" + pd.definition.type);
                    Debug.Log("storeSpecificId:" + pd.definition.storeSpecificId);

                    Debug.Log("transactionID:" + pd.transactionID);

                    Debug.Log("isoCurrencyCode:" + pd.metadata.isoCurrencyCode);
                    Debug.Log("localizedTitle:" + pd.metadata.localizedTitle);
                    Debug.Log("localizedDescription:" + pd.metadata.localizedDescription);
                    Debug.Log("localizedPrice:" + pd.metadata.localizedPrice);
                }

                Product product = m_StoreController.products.WithID(productId);

                // If the look up found a product for this device's store and that product is ready to be sold ...
                if (product != null && product.availableToPurchase){
                    Debug.Log (string.Format("Purchasing product asychronously: '{0}'", product.definition.id));// ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed asynchronously.
                    m_StoreController.InitiatePurchase(product);
                }
                // Otherwise ...
                else{
                    // ... report the product look-up failure situation
                    Debug.Log ("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
                }
            }
            // Complete the unexpected exception handling ...
            catch (Exception e){
                // ... by reporting any unexpected exception for later diagnosis.
                Debug.Log ("BuyProductID: FAIL. Exception during purchase. " + e);
            }
        }

        //
        // --- IStoreListener
        //

        public void OnInitialized(IStoreController controller, IExtensionProvider extensions){
            // Purchasing has succeeded initializing. Collect our Purchasing references.
            Debug.Log("OnInitialized: PASS");

            // Overall Purchasing system, configured with products for this application.
            m_StoreController = controller;
            // Store specific subsystem, for accessing device-specific store features.
            //m_StoreExtensionProvider = extensions;
        }

        public void OnInitializeFailed(InitializationFailureReason error){
            // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
            Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
        }

        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args){
            // A consumable product has been purchased by this user.
            if (String.Equals(args.purchasedProduct.definition.id, kProductNameConsumable, StringComparison.Ordinal)){
                Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));//If the consumable item has been successfully purchased, add 100 coins to the player's in-game score.
                if(args.purchasedProduct.hasReceipt){
                    Debug.Log("receipt:" +args.purchasedProduct.receipt);
                }
            }
            else {
                Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
            }// Return a flag indicating wither this product has completely been received, or if the application needs to be reminded of this purchase at next app launch. Is useful when saving purchased products to the cloud, and when that save is delayed.

            PurchaseProcessingResult ret = PurchaseProcessingResult.Complete;
            if(!m_ProcessComplete){
                ret = PurchaseProcessingResult.Pending;
            }
            return ret;
        }

        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason){
            // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing this reason with the user.
            Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}",product.definition.storeSpecificId, failureReason));
        }

    }
}

初期化

public void Init(){

    var module = StandardPurchasingModule.Instance();
    module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    var builder = UnityEngine.Purchasing.ConfigurationBuilder.Instance(module);

UnityEngine.Purchasing.ConfigurationBuilderのインスタンスを取得します。StandardPurchasingModuleのuseFakeStoreUIModeにFakeStoreUIMode.StandardUserを設定することで、UnityエディタでもダミーUIが表示されます。

// Productの登録
builder.AddProduct( “ID”, ProductType.Consumable, new IDs
        {
        { ”プロダクトID”, GooglePlay.Name, AppleAppStore.Name }
        });

プロダクトを登録します。

“ID”はUnity上で使用する任意のID(UnityEngine.Purchasing.Product -> ProductDefinition -> id)で、iOSとAndroidなどプラットフォーム毎にプロダクトIDが異なる場合にUnity上では統一したIDで使う、という用途になりそうです。

“プロダクトID”はAppStoreやGooglePlayに登録するプロダクトの
ID(UnityEngine.Purchasing.Product -> ProductDefinition -> storeSpecificId)です。

UnityPurchasing.Initialize(this, builder);
}
.
.
.

public void OnInitialized(IStoreController controller, IExtensionProvider extensions){
.

UnityPurchasing.Initialize(IStoreListener, ConfigurationBuilder)で初期化し、OnInitializedで通知されるIStoreControllerを保持しておきます。

OnInitializedはiOS(Objective-C)側でSKProductsRequestDelegateのdidReceiveResponseの後に呼ばれます。つまりストアからプロダクト情報を取得した後に呼ばれるので、このタイミングからIStoreControllerのproductsにはストアから取得した情報が格納されているようです。

購入

Product product = m_StoreController.products.WithID(“プロダクトID”);

if (product != null && product.availableToPurchase){
    m_StoreController.InitiatePurchase(product);
}

IStoreController.InitiatePurchase(“ID” or UnityEngine.Purchasing.Product)で購入処理が実行されます。(iOSだとAppleの購入ダイアログが表示されます)

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args){
    if (String.Equals(args.purchasedProduct.definition.id,
                kProductNameConsumable, StringComparison.Ordinal)){
        if(args.purchasedProduct.hasReceipt){
            Debug.Log("receipt:" +args.purchasedProduct.receipt);
        }
    } else {
…
}

return PurchaseProcessingResult.Complete;
}

ProcessPurchaseで購入成功が通知されます。通知されたargs.purchasedProductのreceiptでレシート情報が取得できます。

returnでPurchaseProcessingResult.Completeを返した場合、購入完了扱いになり、iOSだとSKPaymentQueueのfinishTransactionが実行されます。

returnでPurchaseProcessingResult.Pendingを返した場合、再度アプリを起動した場合にProcessPurchaseが呼ばれます。また、IStoreControllerのConfirmPendingPurchaseを実行することで完了になり、iOSだとSKPaymentQueueのfinishTransactionが実行されます。

//Test UI Click
public void ConfirmPendingPurchase(){
    Product product = m_StoreController.products.WithID(kProductNameConsumable);
    if(product != null){
        m_StoreController.ConfirmPendingPurchase(product);
    }
}

サーバ側でレシートチェックやアイテム付与などを行う場合はこのあたりを活用する形になりそうです。

以上の実装でiOSの実機での購入まで確認できました。
異常系などまだ考えなければいけないところはありそうですが、ここまでの内容はAndroidでも共通のソースでいけそうです。