Skip to content

Instantly share code, notes, and snippets.

@AmbientLion
Last active November 14, 2024 20:52
Show Gist options
  • Save AmbientLion/de61165b710f6dd613abf8ed4212fbe4 to your computer and use it in GitHub Desktop.
Save AmbientLion/de61165b710f6dd613abf8ed4212fbe4 to your computer and use it in GitHub Desktop.
Unity Touch Input Tracking

Overview

Unity's InputSystem is powerful, but can be confusing to use with various gotchas. In this example, we demonstrate how we can correctly receieve touch inputs without some of the pitfalls (like getting the initial touch at (0,0) for example). This implementation relies on a InputActions definition that is included below. The names of the actions are not important, but the choice of action bindings is.

For the scene to be correctly configured, it must include an object that has the EventSystem and Input System UI Input Module components attached to it. For convenience, we add the Input Manager component to this same object (but this is not required).

The InputManager class subscribes to events from the generated InputActions class (which is created from the .inputactions asset by the Unity engine/editor. InputActions contains objects that correspond to the actions and bindings that are defined in the asset, making it easier to program against from C#.

The InputReceiver is a simple component that can be attached to objects that should be able to receive input events from the InputManager. This is just one possible way of dispatching inputs that was chosen for illustrative purposes.

using UnityEngine;
using UnityEngine.InputSystem;
public class InputManager : MonoBehaviour
{
// the class generates from the InputActions asset (your name can vary)
private TouchInputs touchInputs;
// tracks which object is currently actively receiving touch input
private InputReceiver currentReceiver;
// the last touch position that was recorded
private Vector2 lastTouchPosition;
private void Awake() {
touchInputs = new();
}
private void Start() {
// we subscribe to the start/cancel events of the Button action
touchInputs.CoreActions.PrimaryTouch.started += OnPrimaryTouchStarted;
touchInputs.CoreActions.PrimaryTouch.canceled += OnPrimaryTouchCancelled;
// and the performed event of the position tracking PassThrough Value action
touchInputs.CoreActions.PrimaryTouchPosition.performed += OnPrimaryTouchPositionChanged;
// NOTE: The PassThrough action will not raise the started/cancelled events
}
private void OnEnable() => touchInputs.Enable();
private void OnDisable() => touchInputs.Disable();
// Here we check if we have a current reciever, and if not ray-cast the touch position in screen-space to locate
// which object should be notified it is recieving touch inputs. Since we use Physics.Raycast() the object MUST
// have a collider. We also return whether this is the first touch of the new object or not.
private InputReceiver FindTouchReceiver(Vector2 touchPosition, Camera raycastCamera, out bool isFirstTouch) {
// if we already have a receiver, return it
// NOTE: We may need to reset this in some cases if the receiver is destroyed or
// if there was a focus change in the game (or the app was minimized/suspended).
if (currentReceiver) {
// TODO: Correctly handle focus loss and re-acquisition and destroyed objects
isFirstTouch = false;
return currentReceiver;
}
isFirstTouch = true;
if (raycastCamera) {
Ray ray = raycastCamera.ScreenPointToRay(touchPosition);
if (Physics.Raycast(ray, out var hit)) {
return hit.transform.GetComponent<InputReceiver>();
}
} else {
Debug.Log("No camera provided found to raycast from.");
}
return null;
}
private void OnPrimaryTouchPositionChanged(InputAction.CallbackContext context) {
// we can read the Vector2 value from the event state and use it to maintain the last touch position
lastTouchPosition = context.ReadValue<Vector2>();
if (currentReceiver) {
currentReceiver.OnTouchPositionUpdated(lastTouchPosition);
}
}
private void OnPrimaryTouchStarted(InputAction.CallbackContext context) {
// we can read the initial position of the touch event (this works because we use a Modifier in the action binding)
lastTouchPosition = context.ReadValue<Vector2>();
var receiver = FindTouchReceiver(lastTouchPosition, Camera.main, out var isFirstTouch);
if (receiver) {
if(isFirstTouch) { // dispatch touch even as either an first touch, or touch position update
currentReceiver = receiver;
receiver.OnTouchStarted(lastTouchPosition);
} else {
currentReceiver.OnTouchPositionUpdated(lastTouchPosition);
}
}
}
// When the touch input ends, this even will be raised. NOTABLY, we don't try to read the touch input position
// from the context, as it will always be (0,0) here. Instead, we rely on the lastTouchPosition to notify the
// receiver of the last location of the touch input.
private void OnPrimaryTouchCancelled(InputAction.CallbackContext context) {
if (currentReceiver) {
currentReceiver.OnTouchStopped(lastTouchPosition);
}
currentReceiver = null;
}
}
using UnityEngine;
/// <summary>
/// Receives input from the InputManager and responds to it.
/// </summary>
public class InputReceiver : MonoBehaviour
{
/// <summary>
/// Called when the player touches the screen.
/// </summary>
/// <param name="touchPosition">The position on the screen where the player touched.</param>
public void OnTouchPositionUpdated(Vector2 touchPosition)
{
Debug.Log($"{name}: InputReceiver.OnPositionUpdated @ {touchPosition}");
}
public void OnTouchStarted(Vector2 touchPosition)
{
Debug.Log($"{name}: InputReceiver.OnTouchStarted @ {touchPosition}");
}
public void OnTouchStopped(Vector2 touchPosition)
{
Debug.Log($"{name}: InputReceiver.OnTouchStopped @ {touchPosition}");
}
}
{
"name": "TouchInputs",
"maps": [
{
"name": "CoreActions",
"id": "ab265fee-67df-47d4-b93b-b9d1ac1b60d7",
"actions": [
{
"name": "TouchPress",
"type": "Button",
"id": "1dc1bcf8-ce5e-434a-974d-c4819028dfa3",
"expectedControlType": "",
"processors": "",
"interactions": "Press(behavior=2)",
"initialStateCheck": false
},
{
"name": "PrimaryTouchPosition",
"type": "PassThrough",
"id": "ef3027cc-f2b4-42c2-a3a9-51818ba65d93",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "PrimaryTouch",
"type": "Button",
"id": "dc636f0c-dc8c-4885-bafb-3ef1591701b0",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "",
"id": "12ebf728-3dd7-4c00-970c-6ee9e6f4cfe0",
"path": "<Touchscreen>/primaryTouch/press",
"interactions": "",
"processors": "",
"groups": ";TouchInput",
"action": "TouchPress",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "9c106893-ab08-41a6-90ba-fe2410aedd0c",
"path": "<Touchscreen>/primaryTouch/position",
"interactions": "",
"processors": "",
"groups": ";TouchInput",
"action": "PrimaryTouchPosition",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "One Modifier",
"id": "1cd56e49-f37f-4aab-85d9-8d89cd0e4f16",
"path": "OneModifier",
"interactions": "",
"processors": "",
"groups": "",
"action": "PrimaryTouch",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "modifier",
"id": "fb57f5af-912b-4f45-8b14-f4f878259423",
"path": "<Touchscreen>/primaryTouch/press",
"interactions": "",
"processors": "",
"groups": ";TouchInput",
"action": "PrimaryTouch",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "binding",
"id": "bad514ea-b365-4171-83cf-21ce2378fb1b",
"path": "<Touchscreen>/primaryTouch/position",
"interactions": "",
"processors": "",
"groups": ";TouchInput",
"action": "PrimaryTouch",
"isComposite": false,
"isPartOfComposite": true
}
]
}
],
"controlSchemes": [
{
"name": "TouchInput",
"bindingGroup": "TouchInput",
"devices": [
{
"devicePath": "<Touchscreen>",
"isOptional": false,
"isOR": false
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment