本文へスキップ
UI実装

Unityの購入ボタンで連打を防ぐ|受付状態とUI表示をつなぐ

uGUIの購入ボタンを押した直後に受付を止める最小例。処理中フラグ、interactable、残高確認、完了表示をつないで連打時の挙動を確認します。

公開日: 2026-10-12#Unity#uGUI#Button#CSharp#Beginner
ガイドキャラクター
購入ボタンを連打すると、処理が終わる前に次の購入要求が入ることがあります。ボタンを無効にする表示と、処理側の受付条件を一緒に作ります。この例はゲーム内の仮想コインで1回購入するローカル教材です。実際の課金や通信購入は扱いません。
学習者キャラクター
準備するものと、動作確認で見る結果を先に確認してから進めます。
Unityの購入ボタンで連打を防ぐ|受付状態とUI表示をつなぐの手順と判断条件を示す説明図
仕組みを説明する模式図です。エディタのスクリーンショットではありません。小さい画面では横にスクロールできます。設定値と手順は本文で確認してください。

導入

購入ボタンを連打すると、処理が終わる前に次の購入要求が入ることがあります。ボタンを無効にする表示と、処理側の受付条件を一緒に作ります。この例はゲーム内の仮想コインで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資料。今回は待機中に残高を変更する外部処理がない設計です。

動作確認

  1. Playで連打し、処理中表示の後に残高が100から70へ1回だけ変わることを確認する。
  2. coinsを20にして再生し、ボタンが無効で残高不足が表示されることを確認する。
  3. 処理中に対象GameObjectを無効化し、再び有効化する。中断した購入でコインが減らず、受付状態へ戻ることを確認する。
  4. 購入後に無効化・有効化し、購入済みの状態を維持することを確認する。

この例の範囲

有効化と無効化のたびに購読と解除を対にし、処理が途中で止まった場合は受付を戻します。保存や再起動後の状態維持、複数商品、購入失敗、複数の購入経路は含みません。ネットワーク購入ではサーバー側の残高検証や要求IDによる重複排除などが別途必要です。画面上のボタン無効化だけで、取引の一意性を保証しません。

次に読む記事

学習者キャラクター
今回の内容で覚えておくポイントを確認したいです。
ガイドキャラクター
uGUIの購入ボタンを押した直後に受付を止める最小例。処理中フラグ、interactable、残高確認、完了表示をつないで連打時の挙動を確認します。
この記事のまとめを案内するガイドキャラクター

この記事のまとめ

uGUIの購入ボタンを押した直後に受付を止める最小例。処理中フラグ、interactable、残高確認、完了表示をつないで連打時の挙動を確認します。