導入
購入ボタンを連打すると、処理が終わる前に次の購入要求が入ることがあります。ボタンを無効にする表示と、処理側の受付条件を一緒に作ります。この例はゲーム内の仮想コインで1回購入するローカル教材です。実際の課金や通信購入は扱いません。
準備と配置
Unity 6、uGUI、TextMeshProを使います。Canvasの子にButtonとTextMeshProのUIテキストを1つずつ置き、EventSystemがあることを確認します。空のGameObjectへPurchaseButtonGuard.csを付け、InspectorでbuyButtonとstatusTextを登録します。初期値は所持コイン100、価格30、処理時間1秒です。
この例はコードでButtonのクリックを登録します。InspectorのOn Click()には同じ購入処理を追加しません。Button.interactableはUIの入力受付を切り替えるプロパティです。uGUIの公式資料
受付条件を先に確認する
処理中、購入済み、コンポーネントが無効なら戻ります。残高不足も開始前に判定し、購入が始まる時点でbusyを立てます。interactableだけでは、別のスクリプトからのメソッド呼び出しまでは防げません。
完成版
ファイル名をPurchaseButtonGuard.csにして保存します。
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class PurchaseButtonGuard : MonoBehaviour
{
[SerializeField]
private Button buyButton;
[SerializeField]
private TMP_Text statusText;
[SerializeField, Min(0)]
private int coins = 100;
[SerializeField, Min(1)]
private int price = 30;
[SerializeField, Min(0.1f)]
private float delaySeconds = 1f;
private bool busy;
private bool purchased;
private void Awake()
{
if (buyButton == null || statusText == null)
{
Debug.LogError("Assign buyButton and statusText.", this);
enabled = false;
}
}
private void OnEnable()
{
buyButton.onClick.AddListener(TryBuy);
Refresh();
}
private void OnDisable()
{
if (buyButton != null)
buyButton.onClick.RemoveListener(TryBuy);
StopAllCoroutines();
busy = false;
if (buyButton != null)
buyButton.interactable = false;
}
public void TryBuy()
{
if (!isActiveAndEnabled || busy || purchased)
return;
if (coins < price)
{
Refresh();
return;
}
busy = true;
Refresh();
StartCoroutine(CompletePurchase());
}
private IEnumerator CompletePurchase()
{
float remaining = Mathf.Max(0.1f, delaySeconds);
while (remaining > 0f)
{
remaining -= Time.unscaledDeltaTime;
yield return null;
}
coins -= price;
purchased = true;
busy = false;
Refresh();
}
private void Refresh()
{
buyButton.interactable = !busy && !purchased && coins >= price;
statusText.text = busy ? "Processing..." : purchased ? $"Purchased / Coins: {coins}" : coins < price ? $"Not enough coins / Coins: {coins}" : $"Price: {price} / Coins: {coins}";
}
}
UIと状態の対応
| 状態 | ボタン | 表示 |
|---|---|---|
| 未購入・残高あり | 操作できる | 価格と残高 |
| 処理中 | 操作できない | Processing... |
| 購入済み | 操作できない | Purchasedと残高 |
| 残高不足 | 操作できない | Not enough coins |
待ち時間はTime.unscaledDeltaTimeで進めるため、Time.timeScale = 0でも完了します。公式API資料。今回は待機中に残高を変更する外部処理がない設計です。
動作確認
- Playで連打し、処理中表示の後に残高が100から70へ1回だけ変わることを確認する。
coinsを20にして再生し、ボタンが無効で残高不足が表示されることを確認する。- 処理中に対象GameObjectを無効化し、再び有効化する。中断した購入でコインが減らず、受付状態へ戻ることを確認する。
- 購入後に無効化・有効化し、購入済みの状態を維持することを確認する。
この例の範囲
有効化と無効化のたびに購読と解除を対にし、処理が途中で止まった場合は受付を戻します。保存や再起動後の状態維持、複数商品、購入失敗、複数の購入経路は含みません。ネットワーク購入ではサーバー側の残高検証や要求IDによる重複排除などが別途必要です。画面上のボタン無効化だけで、取引の一意性を保証しません。
次に読む記事
この記事のまとめ
uGUIの購入ボタンを押した直後に受付を止める最小例。処理中フラグ、interactable、残高確認、完了表示をつないで連打時の挙動を確認します。