本文へスキップ
UI Implementation

Prevent Repeated Purchase Clicks in Unity: State and UI Feedback

Build a local uGUI purchase example with a busy flag, button interactability, balance checks and completion feedback. Check repeated clicks and interrupted waits.

公開日: 2026-10-12#Unity#uGUI#Button#CSharp#Beginner

Introduction

This example makes one local purchase using fictional game coins. Disable UI input and guard the method itself so a repeated call cannot start another purchase while one is pending. It does not process payments or network transactions.

Prepare the scene

Use Unity 6, uGUI and TextMeshPro. Add a Button and a TextMeshPro UI label beneath a Canvas, and ensure there is an EventSystem. Attach PurchaseButtonGuard.cs to an empty GameObject and assign buyButton and statusText. Defaults are 100 coins, price 30 and a one-second wait.

The script registers the click listener itself. Do not also register TryBuy in the Inspector's On Click() list. Selectable.interactable controls UI input; it does not block direct method calls.

Complete script

Save as 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}";
    }
}

Connect state to feedback

StateButtonLabel
Affordable and not purchasedEnabledPrice and coins
WaitingDisabledProcessing...
PurchasedDisabledPurchased and remaining coins
Insufficient coinsDisabledNot enough coins

The wait uses Time.unscaledDeltaTime, so setting timeScale to zero does not stop it. No external process changes the balance during this example's wait.

Check the behavior

  1. Click repeatedly. The label should show processing, then the balance should change from 100 to 70 once.
  2. Start with 20 coins. The button should be disabled with insufficient-balance feedback.
  3. Disable and re-enable the GameObject during the wait. The interrupted purchase should not deduct coins, and input should become available again.
  4. Disable and re-enable it after completion. It should remain purchased.

Scope

Registration and cleanup are paired. This example does not persist state across application restarts or include multiple products, failures or competing balance changes. Network purchases require server-side validation and duplicate-request handling. A disabled button alone does not guarantee a unique transaction.

Summary

What to take away

Build a local uGUI purchase example with a busy flag, button interactability, balance checks and completion feedback. Check repeated clicks and interrupted waits.

The page is designed to help you move from reading to a small practical decision. Use the checklist above before jumping into the next related article.