Unity IAP をゲームに統合する

確認済のバージョン: 5.3

-

難易度: 中級

Unity IAP を使用すると、プレミアムコンテンツ、仮想アイテム、サブスクリプションなどの様々なアイテムを、有料・無料両方のゲーム内で直接販売することができます。ほとんどの人気アプリストアに対応するアプリ内購入(IAP)の仕組みをアプリケーションに簡単に実装できます。現段階で Unity IAP は、 iOS App Store、 Mac App Store、 Google Play、 Windows ストア (Universal)、 Amazon アプリストアなどに対応しています。

ベースとなるプロジェクトの説明

本記事では、既存のゲームプロジェクトに IAP を追加する方法を解説します。記事内では サバイバルシュータープロジェクト の変更を加えたバージョンを使用します。このゲームプロジェクトのオリジナルのチュートリアルはこちらでご覧いただけます。本記事で使用するバージョンのサバイバルシュータープロジェクトは、ゲーム内武器ショップを作成するトレーニングセッションを行った際に拡張されたものです。この武器ショップでは、プレイヤーがスコアに応じてゲーム内通貨を使って、開始時の武器をアップグレードすることができます。このサバイバルシュータープロジェクトにおける武器ショップの作成方法についての詳細は、ライブトレーニングセッション "Creating an in-game shop (ゲーム内ショップの作成)" をご覧ください。

必要なダウンロード

このチュートリアルでは、この武器屋作成のトレーニングセッションから更に一歩進め、ユーザーが実際の金銭を支払ってゲーム内通貨を買うことのできる IAP の実装を行っていきます。まずは、このチュートリアルを進めるに当たっては、 Survival Shooter IAP Demo の .zip ファイルをダウンロードしてください。

ダウンロードが完了したら、プロジェクトを解凍して Unity 上で開いてください。次に Project パネルで IAPDemo/Scenes からシーン Level 01 5.x IAP を開いてください。

Unity Services のセットアップ

Unity IAP をプロジェクトにセットアップするためのスクリプト作業を始める前に、プロジェクトを Unity Services と連携させる必要があります。 トップメニューから Window > Services と進み、Services ウィンドウを開いてください。

まだログインしていない 場合、以下のメッセージが表示されます。

description

Sign In をクリックして Unity ID にログインしてください。

Project ID

Unity IAP を使用するプロジェクトには、 Unity Services から発行される Project ID が 1 つ必ず必要です。

プロジェクトにまだ ID がない場合は、作成する必要があります。まず Select Organization のドロップダウンメニューから Organization (組織)を選択してください。Organization は個人(1 人)のユーザーでも団体でも問題ありません。デフォルトでは、あなたのログインした Unity Account は、個人 1 人から成る Organization として登録されています。 Organization の選択が完了したら、 Create (作成)ボタンをクリックして Project ID を作成してください。

プロジェクトで使用する Unity Project ID の作成が既に完了している場合は 'I already have a Unity Project ID' を選択してください。

Organization の選択

Project ID を既に持っている場合、 Organization が選択された段階で、以前に作成した Project ID の一覧から選択できるようになります。 Select Project のドロップダウンメニューから既存の Project ID を選択してください。

既存の Project ID の選択

アプリ内購入(In-App Purchasing)を有効にする

Services の一覧から、 In-App Purchasing (アプリ内購入)を選択してください。

Services ウィンドウ ― IAP がオフの状態

次に、アプリ内購入(In-App Purchasing)を有効化するために Enable ボタンをクリックしてください。

description

COPPA への準拠

Children's Online Privacy Protection Act は、13 才以下の子供の個人情報のオンラインにおける収集に適用されます。新しい規約には、プライバシーポリシーに含めなければいけない事項や、親による明確な同意をいつ、どのように求めるか、またオンラインでの子供のプライバシーや安全を守るためにどのような義務があるかが明記されています。 COPPA への準拠 を確実にするため、アプリケーションのユーザーのターゲット年齢に関して質問するダイアログが表示されます。既に Analytics の設定で COPPA の設定の選択が完了している場合、このダイアログは表示されません。適切な回答を選択して "Save Changes" をクリックしてください。

COPPA 準拠に関する画面

IAP パッケージを追加する

Unity IAP は、統合の土台として パッケージのインポート を必要とします。これは、以下の画像内にある Import ボタンによってプロジェクトに追加する必要があります。

description

パッケージのインポートが完了したら、プロジェクトに Plugins というフォルダーが追加されているのを確認できるはずです。このフォルダーには、 Unity IAP を使用する上で必要な UnityPurchasing アセットが含まれています。

Plug In フォルダー

「Back to services」をクリックし、Services パネルを確認してください。

description

以下のように、Analytics と In-App Purchasing の両方が ON になっているはずです。

IAP および Analytics が ON になった Services パネル

購入スクリプトの作成

Services のセットアップが完了し、必要なコードをゲームに追加する準備が整いました。まず、 Purchaser というスクリプトを追加します。 Purchaser はこのプロジェクト用のサンプルスクリプトで、Unity IAP を扱うためのものです。 Purchaser には、以下を可能にする関数が含まれています。

  • InitializePurchasing : IAP ビルダーを初期化し、購入可能な商品を追加し、購入イベントをハンドルするリスナーを提供します。
  • BuyProductID : 追加したプロダクト(商品)を、プロダクト ID 文字列を使って購入可能にする private 関数です。
  • BuyConsumable, BuyNonConsumable, BuySubscription :BuyProductID に各種商品の文字列をパスすることでそれらを購入可能にする public 関数です。
  • RestorePurchases : iOS では RestorePurchases を呼び出して、以前購入された商品を復元することができます。
  • OnInitialize : アプリケーションが Unity IAP に接続できるかどうか確認するために呼び出されます。 OnInitialize はバックグラウンドで繰り返し試行し続け、復元不可能な設定問題がある場合にのみ失敗します。
  • OnInitializeFailed : IAP が初期化に失敗した時に呼び出され、コンソールにメッセージを記録します。
  • ProcessPurchase : 商品の購入が成功に終わったかどうか確認し、コンソールに結果を表示します。
  • OnPurchaseFailed : 購入が失敗した時に、その旨を通知するメッセージをコンソールに表示します。

Project パネルで IAPDemo フォルダーを選択し、 Create ボタンをクリックして Purchaser という C# スクリプトを新規作成し、次の内容を丸ごと上書きペーストしてください。

Code snippet

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;

// Purchaser クラスは CompleteProject 名前空間に配置すると、既存の Survival Shooter のスクリプトのひとつである
//  ScoreManager と通信できるようになります。
namespace CompleteProject
{
    // Purchaser クラスは IStoreListener から派生させると Unity Purchasing からのメッセージを受領できるようになります。
    public class Purchaser : MonoBehaviour, IStoreListener
    {
        private static IStoreController m_StoreController;          // Unity Purchasing システムです。
        private static IExtensionProvider m_StoreExtensionProvider; // ストア毎の Purchasing サブシステムです。
        
        // 購入可能なプロダクトのためのプロダクト識別子: 
        // Purchasing と共に使用する「便宜的な」汎用識別子および、それらに対応する、 Unity Purchasing 以外の 
        // 各種ストア専用の識別子。各パブリッシャーのダッシュボードでも、ストア別の識別子を 
        // 定義してください(iTunes Connect、Google Play Developer Console など)。

        // 消耗型、非消耗型、サブスクリプション型商品の汎用の商品識別子。
        // コード内でこれらのハンドルを使用してどの商品を購入するか指定します。 またストアで Product Identifier を定義する際にも 
        // これらの値を使用します。ただし便宜上、
        // kProductIDSubscription はカスタムの Apple および Google 識別子を持っています。ストア別の 
        // Unity Purchasing の AddProduct へのマッピングを以下に宣言します。
        public static string kProductIDConsumable =    "consumable";   
        public static string kProductIDNonConsumable = "nonconsumable";
        public static string kProductIDSubscription =  "subscription"; 
         
        // Apple App Store 専用の、サブスクリプション商品のプロダクト識別子
        private static string kProductNameAppleSubscription =  "com.unity3d.subscription.new";
        
        // Google Play Store 専用の、サブスクリプション商品のプロダクト識別子
        private static string kProductNameGooglePlaySubscription =  "com.unity3d.subscription.original"; 
        
        void Start()
        {
            // Unity Purchasing の参照がまだ設定されていない場合は
            if (m_StoreController == null)
            {
                // Purchasing への接続の設定を開始する。
                InitializePurchasing();
            }
        }
        
        public void InitializePurchasing() 
        {
            // Purchasing に既に接続していたら ...
            if (IsInitialized())
            {
                // ... ここで終わりです。
                return;
            }
            
            // ビルダーを作成し、最初に Unity 提供のストアのパッケージをパスする。 
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
            
            // 販売するプロダクトを追加する / 識別子を利用して 
            // (汎用識別子をそのストア別識別子と関連付けて)復元する。
            builder.AddProduct(kProductIDConsumable, ProductType.Consumable);
            // 非消耗型プロダクトの追加を続ける。
            builder.AddProduct(kProductIDNonConsumable, ProductType.NonConsumable);
            // そして、サブスクリプション商品の追加を終了する。 ここでは、Product ID の設定が Apple ストアと Google ストアで
            // 異なる場合に判別できるよう、ストア別 ID が使用されていることにご注目ください。 また、
            // 片方は汎用 kProductIDSubscription ハンドルをゲーム内で使用しています。ストア別 ID は 
            // ここのみで参照される必要があります。 
            builder.AddProduct(kProductIDSubscription, ProductType.Subscription, new IDs(){
                { kProductNameAppleSubscription, AppleAppStore.Name },
                { kProductNameGooglePlaySubscription, GooglePlay.Name },
            });
            
            // 非同期呼び出しで、設定およびこのクラスのインスタンスをパスして 
            // セットアップの残りを開始する。OnInitialized か OnInitializeFailed 内でレスポンスが起こる。
            UnityPurchasing.Initialize(this, builder);
        }
        
        
        private bool IsInitialized()
        {
            // 両方の Purchasing 参照が設定されて初めて初期化されたことになる。
            return m_StoreController != null && m_StoreExtensionProvider != null;
        }
        
        
        public void BuyConsumable()
        {
            // 消耗型プロダクトを、その汎用識別子を使用して購入します。 
            // レスポンスは ProcessPurchase または OnPurchaseFailed 経由で非同期的に発生します。
            BuyProductID(kProductIDConsumable);
        }
        
        
        public void BuyNonConsumable()
        {
            // 非消耗型プロダクトを、その汎用識別子を使用して購入します。
            // レスポンスは ProcessPurchase または OnPurchaseFailed 経由で非同期的に発生します。
            BuyProductID(kProductIDNonConsumable);
        }
        
        
        public void BuySubscription()
        {
            // サブスクリプション商品を、その汎用識別子を使用して購入します。
            // レスポンスは ProcessPurchase または OnPurchaseFailed 経由で非同期的に発生します。
            // この ID を上述のストア別カスタム識別子にマップするのではなく、ストア別の汎用のプロダクト識別子を
            // 使用していることにご注目ください。
            BuyProductID(kProductIDSubscription);
        }
        
        
        void BuyProductID(string productId)
        {
            // Purchasing が初期化されていれば ...
            if (IsInitialized())
            {
                // ... 汎用のプロダクト識別子と Purchasing システムの商品コレクションから
                // Product の参照を取得します。
                Product product = m_StoreController.products.WithID(productId);
                
                // このデバイスのストア用のプロダクトが存在し、そのプロダクトが販売可能な状態であれば ... 
                if (product != null && product.availableToPurchase)
                {
                    Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
                    // ... プロダクトを購入します。ProcessPurchase または OnPurchaseFailed 経由で
                    // 非同期的にレスポンスが発生します。
                    m_StoreController.InitiatePurchase(product);
                }
                // そうでなければ ...
                else
                {
                    // ... product の参照取得の失敗あるいはプロダクトが購入可能でない旨のログを出力します。
                    Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
                }
            }
            // あるいは ...
            else
            {
                // ... Purchasing がまだ初期化に成功していない旨のログを出力します。更に待機し続けるか 
                // 初期化を再度試みるか検討してください。
                Debug.Log("BuyProductID FAIL. Not initialized.");
            }
        }
        
        
        // この顧客によって以前行われた購入を復元する。Google などの一部のプラットフォームでは自動的に復元されます。
        // Apple は、現段階では IAP の購入の復元は明示的に行う必要があります。場合によってはパスワードを求められます。
        public void RestorePurchases()
        {
            // Purchasing がまだセットアップされていなければ ...
            if (!IsInitialized())
            {
                // ... その旨のログを出力し復元を停止します。更に待機するか初期化を再度試みるか検討してください。
                Debug.Log("RestorePurchases FAIL. Not initialized.");
                return;
            }
            
            // Apple のデバイスで実行している場合は ... 
            if (Application.platform == RuntimePlatform.IPhonePlayer || 
                Application.platform == RuntimePlatform.OSXPlayer)
            {
                // ... 購入の復元を開始します。
                Debug.Log("RestorePurchases started ...");
                
                // Apple ストア専用サブシステムをフェッチします。
                var apple = m_StoreExtensionProvider.GetExtension();
                // 下記の Action の返り値の応答を待ってください。
                // 以前購入したプロダクトを復元する場合は ProcessPurchase が必要です。
                apple.RestoreTransactions((result) => {
                    // 復元の第一段階。 ProcessPurchase にこれ以上のレスポンスが届かない場合は、
                    // 復元できる購入はありません。
                    Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
                });
            }
            // そうでなければ ...
            else
            {
                // Apple デバイスでは実行されていません。購入復元の為にするべきことは何もありません。
                Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
            }
        }
        
        
        //  
        // --- IStoreListener
        //
        
        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            // Purchasing は初期化に成功しました。Purchasing の参照を取得します。
            Debug.Log("OnInitialized: PASS");
            
            // このアプリケーション用のプロダクトが設定された Purchasing システム全体
            m_StoreController = controller;
            // デバイス専用のストア機能にアクセスする為の、ストア専用のサブシステム
            m_StoreExtensionProvider = extensions;
        }
        
        
        public void OnInitializeFailed(InitializationFailureReason error)
        {
            // Purchasing のセットアップに成功しませんでした。エラーの原因を確認してください。ユーザーにこの原因を共有することを検討してください。
            Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
        }
        
        
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) 
        {
            // ある消耗型プロダクトがユーザーによって購入されました。
            if (String.Equals(args.purchasedProduct.definition.id, kProductIDConsumable, StringComparison.Ordinal))
            {
                Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                // この消耗型プロダクトの購入に成功しました。プレイヤーのゲーム内スコアに 100 コインを追加します。
                ScoreManager.score += 100;
            }
            // または ... 非消耗型プロダクトがこのユーザーによって購入されました。
            else if (String.Equals(args.purchasedProduct.definition.id, kProductIDNonConsumable, StringComparison.Ordinal))
            {
                Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                // TODO: 非消耗型アイテムの購入に成功しました。プレイヤーにこのアイテムを提供します。
            }
            // あるいは ... サブスクリプション商品がこのユーザーによって購入されました。
            else if (String.Equals(args.purchasedProduct.definition.id, kProductIDSubscription, StringComparison.Ordinal))
            {
                Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                // TODO: サブスクリプションアイテムの購入に成功しました。このアイテムをプレイヤーに提供します。
            }
            // あるいは ... 未知の商品がこのユーザーによって購入されました。追加商品をここに書き込んでください。....
            else 
            {
                Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
            }

            // このプロダクトが完全に受領されたか、あるいは次回のアプリ起動時にリマインドが必要かを示すフラグを返します。
            // 購入された商品が未だクラウドに保存中で、その保存に遅延が
            //  生じた場合は PurchaseProcessingResult.Pending を使用します。
            return PurchaseProcessingResult.Complete;
        }
        
        
        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            // あるプロダクトの購入に失敗しました。詳細は failureReason を確認してください。問題解決のためのヒントとして 
            // 原因をユーザーに共有することを検討してください。
            Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
        }
    }
}

スクリプトを保存して Unity に戻ってください。

Project パネルから Purchaser スクリプトを、 Hierarchy の ShopCanvas ゲームオブジェクトの子であるゲームオブジェクト IAPPanel にドラッグしてください。

Purchaser を IAPPanel にドラッグする

Purchaser スクリプトを使用して、「100 coins」という消耗型アイテムのアプリ内購入のリクエストを作成します。アイテムの購入に成功すると、プレイヤーの現在のスコアに 100 コインが追加されます。プレイヤーはこれをゲーム内購入に使用できます。

ゲーム内武器ショップ

このプロジェクトでは、武器ショップに、プレイヤーが何か購入したい時に歩いて入れる物理的な場所を与えました。ここでは、ショップは緑色に光るキューブとして表現されています。プレイヤーがキューブまで歩くとゲームが一時停止します。

ショップの物理的な場所

その後、武器ショップが呈示されます。欲しい武器を購入できるだけのゲーム内通貨をプレイヤーが持っていない場合、「OUT OF COINS?」(「コインが足りないですか?」)と書かれたボタンが表示され、プレイヤーがこれをクリックすると IAP パネルが開きます。

description

IAP パネル内には各種 IAP 商品の選択肢が表示されます。ここには 99 セントで 100 コインを購入できる Consumable (消耗型アイテム)が含まれています。

description

これがどのように設定されているか見てみましょう。

Hierarchy 内に ShopCanvas というゲームオブジェクトがあります。これは UI Canvas で、 MainPanelIAPPanel の 2 つのパネルを含んでいます。

IAPPanel をハイライトすると、先程添付した Purchaser コンポーネントが確認できます。

description

Purchaser スクリプトの関数を呼び出して IAP トランザクションを行います。 Button ゲームオブジェクトを Consumable という IAPPanel の子として追加してあります。

Hierarchy 内で Consumable ゲームオブジェクトをハイライトすると Button コンポーネントが表示されます。この Button に OnClick イベントを追加すると、ユーザーがこれをアクティベートした時に、消耗型アイテムを購入する関数が呼び出されるようになります。

Purchase (購入)関数の呼び出し

最初の OnClick イベントがセットアップされました。このイベントは、 ShopController スクリプトを含む ShopCube への参照を持っています。この Button がアクティベートされると、このイベントが ShopController の CloseIAP 関数を呼び出します。この関数が IAPPanel ゲームオブジェクトを無効化し、UI パネルが閉じられます。これは既に設定されています。

OnClick イベント Consumable 1

Consumable Button コンポーネントの OnClick イベントテーブル内で、 + ボタンをクリックして新規イベントを追加してください。呼び出したい関数が Purchaser スクリプトにあります。このスクリプトは IAPPanel にあります。 Hierarchy 上で IAPPanel を選択し、それを OnClick のテーブル内の新規エントリー内にある空の Object フィールドにドラッグしてください。この Object フィールドには "None(Object)" と表示されています。次に、 Object フィールドの右側のドロップダウンメニューから Purchaser > BuyConsumable と選択してください。これにより、ユーザーがボタンをクリックした時に BuyConsumable 関数が呼び出されるようになります。

description

他のボタンのセットアップに関しては、本記事では省略します。他のボタンを実装したい場合は、上記の手順を再度実行してください。その際は、各ボタンに関して適切な関数( BuyNonConsumable()、 BuySubscription()RestorePurchases() )が呼び出されるように注意して行ってください。

IAP の統合をテストする

シーンをプレイする際は、プレイヤーを武器ショップ(緑のキューブ)まで移動させる必要があります。 UI が表示されたら、 Out of Coins をクリックし、次に Consumable ボタンをクリックしてください。コンソールに一連のメッセージが記録されます。

以下が最初のメッセージです。

description

OnInitialized: PASS UnityEngine.Debug:Log(Object)

これは、シーン開始時に IAP が正しく初期化されたことを意味しています。

次に以下のメッセージが出ます。

description

Purchasing product asychronously: 'consumable' UnityEngine.Debug:Log(Object)

これは、あるプロダクトの購入の試みが非同期的に行われたことを示しています。 Unity エディター内では購入は即時に処理されます(次のステップ参照)。アプリケーションが機動していて購入がネットワークを介して行われている場合、オペレーティングシステムがユーザーに購入の決定をリクエストしますが、購入が処理される際に遅延が生じる場合があります。購入が完了(あるいは失敗)すると、 OS ダイアログが閉じられ、プレイヤーがゲームに戻されます。

次に、以下が表示されます。

description

ProcessPurchase: PASS. Product: 'consumable' UnityEngine.Debug:Log(Object)

これは、購入処理の試みに成功したことを示しています。

最後に以下が表示されます。

description

purchase({0}): consumableUnityEngine.Purchasing.PurchasingManager:InitiatePurchase(Product)

これは、購入処理が完了したことを意味します。

この簡単なサンプルを機能させるためのラインが、Purchaser スクリプトの PurchaseProcessingResult 関数内にあります。このラインが ScoreManager のスコア変数に 100 を加算します。 ScoreManager は、オリジナル版のサバイバルシューターゲームの一部です。コードは以下のようになっています。

ScoreManager.score += 100;

製品版の場合は、 IAP とスコアリングを更に切り離す必要があります。

上述の通り、エディターでのテスト時は、 PurchaseProcessing は常に成功します。クラウドで実際の購入のテストを行うには、ターゲットのデバイス向けにビルドしてテスト用のサンドボックスを作成する必要があります。詳細は Unity マニュアルの Unity IAP の項をご参照ください。