概要
Unty で内課金処理をおこなうには、Version 4 までは、IStoreListener や IDetailedStoreListener を継承したクラスで処理をしていました。
これらを使ったままにしておくと、「旧形式です (This API is deprecated. Please upgrade to the new APIs introduced in IAP v5)」とコンソールに warning が出ます。
解決するには IAP v5 に置き換えれば良い訳ですが、これにはかなりの労力を必要とします。
日本語の公式ドキュメントも、まだ整備されていません。
(Unity 公式の V5ドキュメント の言語を「日本語」にすると、現時点では Not found になります)。
v4 で使っていたメソッドを v5 でどう置き換えるかは、Replace IDetailedStoreListener and IStoreListener に書いてあるのですが、変更点が多すぎるので、基本的に置き換えるのは無理と思った方が良いです。
幸いにも v5 では、v4 に比べて作業がぐっと単純になっています。
リスナーを継承する必要は無く、UnityIAPServices.StoreController()
のインスタンスに対して、課金が成功した時と失敗した時などの関数を定義してやるだけです。
とは言え面倒なのは変わりないので、v5 に置き換えるためのひな形を作ってみました。
なお筆者は、Unity 6.2 (6000.2.4f1) で検証しています。
(Unity 6 (6000.0.26f1) とかだと 4KB メモリのページサイズを使用していて、2025/11/1 までに 16 KB にしろと言われます)
あと余談ですが、現時点では v5 の IAP Button は、使えないようです(?)
昔みたいに、ボタンとして表示されません。
普通のボタンに Add Component して使うのかと思っていたのですが、違うみたいですね(??)
なので、IAP Catalog も使いません。
ゼロベースから、新規に作りましょう
内部課金については皆さんかなり作り込んでいらっしゃると思いますが、過去のソースを変更して使おうとすると、時間を溶かします! (溶かしました…orz)
ゼロベースから新規ソースを作り、動作確認しながら徐々に過去のソースから処理をコピーしてくる方法を使うのを推奨します。
一番良いサンプルは、Package Manager で「In-App-Purchasing」を選び、ページ内の「samples」タグから import して得られます。
が、コメントが英語だし、イマイチ何やってるか分からん感じなので、修正してみたものを添付します。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;
public class BuyingConsumables : MonoBehaviour
{
StoreController m_StoreController; // The Unity Purchasing system.
private string myProductId = "* ストアに登録している products ID *";
// ヒエラルキー上の GameObject
public Text TxtMess; // 状況を見せるための Text
public GameObject BtnBuy; // 購入ボタン
public Text TxtBtnBuy; // 購入ボタンの Text
private enum State { // ステータス(UpdateUI() で表示されるボタンとメッセージ)
None, // ストアに通信中
CanClick, // 購入ボタンがクリックできる
Clicked, // 購入ボタンがクリックされた
Canceled, // ユーザーがストア上で購入しなかった
Failed, // 購入失敗
// -- これ以降は、ボタンが「戻るになる」
Purchased, // 購入成功
NetError // 通信エラー
}
private State state; // 表示した状況。この状況により、購入ボタンのText変更。
// シーン開始時に一度だけ呼び出される。
void Awake()
{
UpdateUI(State.None);
InitializeIAP();
}
// 初期化
async void InitializeIAP()
{
m_StoreController = UnityIAPServices.StoreController();
// m_StoreController に、必要なメソッドを登録していく
m_StoreController.OnPurchasePending += OnPurchasePending; // 購入待ち・購入決定
m_StoreController.OnPurchaseFailed += OnPurchaseFailed; // 購入キャンセル・失敗
// ↓以下の2つは必須では無いが、失敗原因を探るのに役立つ(かも)
m_StoreController.OnStoreDisconnected += OnStoreDisconnected; // ストアとの通信失敗
m_StoreController.OnPurchaseConfirmed += OnPurchaseConfirmed; // 購入後の確認
Debug.Log("ストアと通信");
await m_StoreController.Connect();
// 製品情報の登録
var initialProductsToFetch = new List<ProductDefinition>
{
// 製品IDと、消費形態を指定
// ProductType: 何回も購入できるなら Consumable、1回限りなら NonConsumable
new(myProductId, ProductType.NonConsumable)
// 製品が複数あるなら、Listとして追加
};
// ↓以下の2つは必須では無いが、失敗原因を探るのに役立つ(かも)
m_StoreController.OnProductsFetched += OnProductsFetched; // 製品情報取得成功
m_StoreController.OnProductsFetchFailed += OnProductsFetchedFailed; // 製品情報取得失敗
// ストアに製品情報登録開始
m_StoreController.FetchProducts(initialProductsToFetch);
state = State.CanClick;
}
// ストアとの通信失敗
void OnStoreDisconnected(StoreConnectionFailureDescription description)
{
Debug.Log($"ストアと通信できない: {description.message}");
UpdateUI(State.NetError);
}
// 製品情報確認成功:ストアと通信確立後、複数回呼ばれます
void OnProductsFetched(List<Product> products)
{
Debug.Log($"製品情報確認成功: 製品登録個数 = {products.Count}");
UpdateUI(State.CanClick); // 購入ボタンがクリックできるようにする
}
// 製品情報取得失敗
void OnProductsFetchedFailed(ProductFetchFailed failure)
{
Debug.Log($"製品情報確認失敗: {failure.FailedFetchProducts.Count} products: {failure.FailureReason}");
UpdateUI(State.NetError);
TxtMess.text = "ストアに登録している情報が\n何か間違っている";
}
// ヒエラルキー上 購入ボタンの On Click で設定するメソッド
// 製品が複数あるなら複数個のボタンを用意して製品IDを変更するとかする。
public void Buy_ID1()
{
if (state < State.Purchased) { // 課金クリックなら
UpdateUI(State.Clicked);
// 製品IDを指定して、購入処理開始してもらう
m_StoreController.PurchaseProduct(myProductId);
} else { // 「戻る」なら
UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("戻り先のシーン");
}
}
// 必須メソッド
// 購入決定
// @param order 購入に関するすべての関連情報
void OnPurchasePending(PendingOrder order)
{
var product = GetFirstProductInOrder(order); // ストアからの製品状態
if (product is null)
{
Debug.Log("ストアに製品IDが見つからない");
UpdateUI(State.NetError);
TxtMess.text = "ストアに製品IDが見つからない";
return;
}
if (product.definition.id == myProductId) // 製品IDが購入されていたら
{
Debug.Log("購入成功!");
UpdateUI(State.Purchased);
/* todo: 以下で、購入後の処理をおこなうのを推奨。 */
} else {
Debug.Log($"指定したのと違う製品が購入された(?): {product.definition.id}");
UpdateUI(State.Failed);
TxtMess.text = "すでに購入済みと思われます。";
}
m_StoreController.ConfirmPurchase(order); // 購入後の確認
}
// 購入後の確認 (厳密に確認しないなら、実装しなくても良い)
// @param order 購入に関するすべての関連情報
void OnPurchaseConfirmed(Order order)
{
Debug.Log("OnPurchaseConfirmed");
switch (order)
{
case ConfirmedOrder confirmedOrder: // 確認自体は成功
{
var product = GetFirstProductInOrder(confirmedOrder);
if (product == null)
{
Debug.Log("すでに GetFirstProductInOrder 確認しているのに、ここに来た");
UpdateUI(State.Failed);
return;
}
Debug.Log($"購入後の確認OK: {product?.definition.id}");
}
break;
case FailedOrder failedOrder: // 確認失敗
{
var product = GetFirstProductInOrder(failedOrder);
if (product == null)
{
Debug.Log("購入確認ができなかった");
}
Debug.Log($"購入確認失敗: ID='{product?.definition.id}', " +
$"失敗理由: {failedOrder.FailureReason.ToString()}, " +
$"詳細情報: {failedOrder.Details}");
UpdateUI(State.Failed);
TxtMess.text = "課金はされてますが、\n購入確認失敗に失敗しました。";
}
break;
default:
Debug.Log("原因不明の理由で、購入確認ができなかった");
UpdateUI(State.Failed);
TxtMess.text = "課金はされてますが、\n原因不明のエラーが起こりました。";
break;
}
}
// 購入のキャンセル・失敗
void OnPurchaseFailed(FailedOrder order)
{
var product = GetFirstProductInOrder(order);
if (product == null)
{
Debug.Log("製品情報が見つからない myProductId のミス?");
}
if (order.FailureReason == PurchaseFailureReason.UserCancelled)
{ // ユーザーによるキャンセル(ストアで購入を選ばなかった)
Debug.Log($"購入のキャンセル: ID='{product?.definition.id}', " +
$"詳細情報: {order.Details}");
UpdateUI(State.Canceled);
} else { // キャンセル以外の失敗
// 失敗理由→ https://docs.unity3d.com/ja/560/ScriptReference/Purchasing.PurchaseFailureReason.html
Debug.Log($"購入の失敗: ID='{product?.definition.id}', " +
$"失敗理由: {order.FailureReason.ToString()}, " +
$"詳細情報: {order.Details}");
UpdateUI(State.Failed);
TxtMess.text = "購入に失敗しました。\nしばらく待ってから\n再度お試しください。\n" +
$"失敗理由: {order.FailureReason.ToString()}\n" +
$"詳細情報: {order.Details}";
}
}
// ストアから得られた製品情報の、一番最初を得る
Product GetFirstProductInOrder(Order order)
{
return order.CartOrdered.Items().First()?.Product;
}
// 画面にメッセージを書いたり、ボタンのtextを変更
void UpdateUI(State s)
{
state = s; // 状況保存(仮)
// Debug.Log("UpdateUI: " + s);
switch (s) {
case State.None:
TxtMess.text = "通信中...";
break;
case State.CanClick:
TxtBtnBuy.text = "課金・課金の復元";
BtnBuy.SetActive(true);
TxtMess.text = "課金して頂くと、○○が追加されます。";
break;
case State.Clicked:
TxtMess.text = "課金を検討して頂き、ありがとうございます。\nしばらくお待ちください。";
BtnBuy.SetActive(false); // ストアでの操作が確認出来るまで、購入ボタンを非アクティブに
break;
case State.Canceled:
TxtMess.text = "キャンセルしました";
BtnBuy.SetActive(true); // 購入ボタンを有効に戻す
state = State.CanClick; // 再び購入ボタンが押せるように
break;
case State.Failed:
TxtMess.text = "課金に失敗しました。\nしばらく待ってから\n再度お試しください。";
BtnBuy.SetActive(true);
state = State.CanClick; // 再び購入ボタンが押せるように
break;
case State.Purchased:
TxtMess.text = "課金が確認されました。\n○○が追加されました。";
BtnBuy.SetActive(true); // 購入ボタンを有効に戻す
TxtBtnBuy.text = "戻る"; // 購入ボタンのText入れ替え
break;
case State.NetError:
// エラーが起こった時は、UpdateUI() 後に TxtMess.text 書き換えを推奨
TxtMess.text = "通信に失敗しました。\n電波状態の良い場所でお試しください。";
BtnBuy.SetActive(true);
TxtBtnBuy.text = "戻る"; // 購入ボタンのText入れ替え
break;
}
}
}
コードの説明
v4 だと BuyProductID()
で購入開始をおこない、ProcessPurchase()
で購入後の処理をしていたのですが、
v5 だと PurchaseProduct()
で購入開始し、OnPurchasePending()
で購入後の処理をします。
購入後の確認として OnPurchaseConfirmed()
を定義することも出来、コチラで購入後の処理をしても良さそうな気もしますが、 OnPurchasePending()
が呼ばれた時点でユーザーの購入処理はおそらく完了しているので、変に OnPurchaseConfirmed()
の中でエラーになるとクレームの原因になる可能性があるかもしれません。
エラー処理に十分な自信があるようなら、OnPurchaseConfirmed()
で購入後の処理をしても良いかもしれません。
上記のコードはオリジナルに比べて、画面にステータスを表示するようにしています。
Debug.Log()
では製品版でのエラーなどが分からないので、TxtMess にメッセージを書いています。
ついでに、購入処理中に再び購入ボタンが押されるのを防ぐために、BtnBuy のアクティブも操作してます。
スクリプトを書いてどこかに保存し、課金をおこなうシーンの hierarchy に、そのスクリプトをドラッグ&ドロップで追加。
Inspector のスクリプトで「Txt Mess」「Btn Buy」「Txt Btn Buy」に GameObject を割り当ててください。
購入ボタンの On Click() リストの「+」を押し、オブジェクトの所に、hierarchy に追加したスクリプトをドラッグ&ドロップ。
Function の中から実行させたいメソッド Buy_ID1
を指定してください。
このソースでは(広告の削除などをイメージした)1回限りの購入(NonConsumable)としているので、購入が確認出来たら購入ボタンの意味を「購入前のシーンに戻る」になるようにしています。
Consumable にしたいなら変更してください。
その場合は、購入前のシーンに戻るボタンなども必要になると思います。
Unity エディタ上での実行
myProductId
が本物のIDで無くても、Unity エディタ上で実行させてテストする事ができます。
Play して購入ボタンを押すと、ダミーの課金・キャンセルダイアログが出てくるので、その後の経緯がどうなるのかログで確認して見てみると良いでしょう。
ただし、Unity エディタ上での実行では、購入後の確認 OnPurchaseConfirmed()
は走りません。
これをテストしたければ、別途ダミーのボタンで走らせるとかした方が良いかもしれません。
そういう意味でも、購入後の処理は OnPurchasePending()
内に書いてしまった方が楽です。