Touch Button
Mobile-friendly UI button with press, release, and hold events plus visual feedback. Includes configurable cooldown and hold duration tracking for responsive touch controls.
How to Use
Create a UI Button inside a Canvas with an EventSystem in the scene
Attach the TouchButton script to the Button GameObject
Configure cooldown time and hold threshold in the inspector
Wire up onPressed, onReleased, or onHoldStart events to your game actions
Adjust pressedScale for visual press feedback
Features
- Press and release events
- Hold detection with duration
- Visual press feedback (scale)
- Configurable cooldown
- Works with Unity UI system
When to Use This
Use for mobile game action buttons — shoot, jump, interact, or any button that needs hold-to-charge mechanics. Great for hyper-casual games with single-button controls and mobile RPGs with ability buttons that distinguish between tap and hold.
Common Mistakes
An EventSystem must exist in the scene or IPointerDown/Up events won't fire at all. The pressedScale multiplies the original scale, so if your button is already scaled to 0.5, a pressedScale of 0.9 results in 0.45 — not 0.9. The cooldownTime prevents rapid re-presses, but setting it too high makes the button feel unresponsive in fast-paced games.
Source Code
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class TouchButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
[Header("Button Settings")]
[SerializeField] private float cooldownTime = 0.1f;
[SerializeField] private float holdThreshold = 0.4f;
[Header("Visual Feedback")]
[SerializeField] private float pressedScale = 0.9f;
[SerializeField] private float scaleSpeed = 10f;
[Header("Events")]
public UnityEvent onPressed;
public UnityEvent onReleased;
public UnityEvent onHoldStart;
public UnityEvent<float> onHolding;
public bool IsPressed { get; private set; }
public bool IsHolding { get; private set; }
public float HoldDuration { get; private set; }
private RectTransform rectTransform;
private Vector3 originalScale;
private Vector3 targetScale;
private float cooldownTimer;
private bool holdFired;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
originalScale = rectTransform.localScale;
targetScale = originalScale;
}
private void Update()
{
// Scale animation
rectTransform.localScale = Vector3.Lerp(
rectTransform.localScale, targetScale, Time.deltaTime * scaleSpeed);
// Cooldown
if (cooldownTimer > 0f)
cooldownTimer -= Time.deltaTime;
// Hold tracking
if (IsPressed)
{
HoldDuration += Time.deltaTime;
if (!holdFired && HoldDuration >= holdThreshold)
{
holdFired = true;
IsHolding = true;
onHoldStart?.Invoke();
}
if (IsHolding)
{
onHolding?.Invoke(HoldDuration);
}
}
}
public void OnPointerDown(PointerEventData eventData)
{
if (cooldownTimer > 0f) return;
IsPressed = true;
HoldDuration = 0f;
holdFired = false;
IsHolding = false;
targetScale = originalScale * pressedScale;
onPressed?.Invoke();
}
public void OnPointerUp(PointerEventData eventData)
{
if (!IsPressed) return;
IsPressed = false;
IsHolding = false;
targetScale = originalScale;
cooldownTimer = cooldownTime;
onReleased?.Invoke();
}
public void SetInteractable(bool interactable)
{
enabled = interactable;
Image img = GetComponent<Image>();
if (img != null)
{
Color c = img.color;
c.a = interactable ? 1f : 0.5f;
img.color = c;
}
if (!interactable && IsPressed)
{
IsPressed = false;
IsHolding = false;
targetScale = originalScale;
}
}
public void ResetButton()
{
IsPressed = false;
IsHolding = false;
HoldDuration = 0f;
holdFired = false;
cooldownTimer = 0f;
targetScale = originalScale;
rectTransform.localScale = originalScale;
}
private void OnDisable()
{
ResetButton();
}
}