Tap Input Handler
Handles single tap, double tap, and long press gestures with configurable timing. Provides tap position in both screen and world space coordinates.
How to Use
Create an empty GameObject named TapManager
Attach the TapInputHandler script to it
Adjust the double-tap window and long press duration in the inspector
Connect onSingleTap, onDoubleTap, or onLongPress events to your game methods
Use LastTapWorldPosition to spawn objects or trigger actions at tap location
Features
- Single and double tap detection
- Configurable double-tap window
- Tap position in screen and world space
- Long press detection
- Multi-tap counting
When to Use This
Use in hyper-casual and mobile games that need tap-to-interact mechanics — idle clickers, object placement games, or puzzle games with tap and long-press actions. Perfect when you need world-space tap positions for spawning objects or selecting units.
Common Mistakes
The doubleTapWindow delay means single taps fire after a short wait — if your game feels laggy on taps, reduce this value or bypass the delayed single tap. The GetWorldPosition uses a fixed depth offset from the near clip plane, which may not match your game's ground plane — use a raycast for accurate 3D placement. Long press and tap are mutually exclusive; a long press won't also fire onSingleTap.
Source Code
using UnityEngine;
using UnityEngine.Events;
public class TapInputHandler : MonoBehaviour
{
[Header("Tap Settings")]
[SerializeField] private float doubleTapWindow = 0.3f;
[SerializeField] private float longPressDuration = 0.5f;
[SerializeField] private float tapMoveTolerance = 10f;
[Header("Events")]
public UnityEvent<Vector2> onSingleTap;
public UnityEvent<Vector2> onDoubleTap;
public UnityEvent<Vector2> onLongPress;
public UnityEvent<Vector3> onSingleTapWorld;
public UnityEvent<int> onMultiTap;
public int TapCount { get; private set; }
public bool IsLongPressing { get; private set; }
public Vector2 LastTapScreenPosition { get; private set; }
public Vector3 LastTapWorldPosition { get; private set; }
private int consecutiveTaps;
private float lastTapTime;
private float touchStartTime;
private Vector2 touchStartPos;
private bool isTouching;
private bool longPressFired;
private void Update()
{
Vector2 inputPos = Vector2.zero;
bool inputDown = false;
bool inputHeld = false;
bool inputUp = false;
if (Input.touchCount > 0)
{
Touch touch = Input.GetTouch(0);
inputPos = touch.position;
inputDown = touch.phase == TouchPhase.Began;
inputHeld = touch.phase == TouchPhase.Stationary || touch.phase == TouchPhase.Moved;
inputUp = touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled;
}
else
{
inputPos = Input.mousePosition;
inputDown = Input.GetMouseButtonDown(0);
inputHeld = Input.GetMouseButton(0);
inputUp = Input.GetMouseButtonUp(0);
}
if (inputDown)
{
touchStartPos = inputPos;
touchStartTime = Time.time;
isTouching = true;
longPressFired = false;
IsLongPressing = false;
}
if (inputHeld && isTouching && !longPressFired)
{
float held = Time.time - touchStartTime;
float moved = Vector2.Distance(inputPos, touchStartPos);
if (held >= longPressDuration && moved < tapMoveTolerance)
{
longPressFired = true;
IsLongPressing = true;
onLongPress?.Invoke(inputPos);
}
}
if (inputUp && isTouching)
{
isTouching = false;
IsLongPressing = false;
if (longPressFired) return;
float moved = Vector2.Distance(inputPos, touchStartPos);
if (moved > tapMoveTolerance) return;
LastTapScreenPosition = inputPos;
LastTapWorldPosition = GetWorldPosition(inputPos);
float timeSinceLastTap = Time.time - lastTapTime;
lastTapTime = Time.time;
if (timeSinceLastTap <= doubleTapWindow)
{
consecutiveTaps++;
TapCount = consecutiveTaps;
onDoubleTap?.Invoke(inputPos);
onMultiTap?.Invoke(consecutiveTaps);
}
else
{
consecutiveTaps = 1;
TapCount = 1;
StartCoroutine(DelayedSingleTap(inputPos));
}
}
}
private System.Collections.IEnumerator DelayedSingleTap(Vector2 pos)
{
yield return new WaitForSeconds(doubleTapWindow);
if (consecutiveTaps == 1)
{
onSingleTap?.Invoke(pos);
onSingleTapWorld?.Invoke(GetWorldPosition(pos));
}
}
private Vector3 GetWorldPosition(Vector2 screenPos)
{
Camera cam = Camera.main;
if (cam == null) return Vector3.zero;
Vector3 screenPoint = new Vector3(screenPos.x, screenPos.y, cam.nearClipPlane + 10f);
return cam.ScreenToWorldPoint(screenPoint);
}
public void ResetTapCount()
{
consecutiveTaps = 0;
TapCount = 0;
}
}