U
tab 03Camera & Input6 lessons — tap to open
05 / 06 · 10 min
Camera & Input

Touch controls and joystick

your project so far

Assets/

Scenes/

Arena.unity

TouchCanvas

Scripts/

InputRouter.csVirtualJoystick.csSafeAreaFitter.cs

Settings/

PlayerControls.inputactions

you already havethis lesson adds

Because the last lesson routed everything through InputRouter, you do not touch a single gameplay script here. The Knight already accepts input from anything that can fill the Move action.

Touch controls are not a separate game. They are a UI layer that produces the same Vector2 your keyboard already produces.

animated diagram

Thumb drags the knob, the knob becomes a direction, the direction feeds the same Move action.

The fast path: On-Screen Controls

  1. 1

    Create a UI Canvas: GameObject▸UI▸Canvas, and name it TouchCanvas. Set Render Mode = Screen Space Overlay.

  2. 2

    Canvas Scaler → Scale With Screen Size, reference 1080×1920, Match 0.5. Without this your UI is tiny on some phones.

  3. 3

    Add an Image for the stick base, and a child Image for the knob.

  4. 4

    Put an On-Screen Stick component on the knob and set Control Path to the Move binding.

  5. 5

    Add On-Screen Button to your action buttons the same way. The Input System now sees a virtual gamepad.

Writing your own joystick

A hand-written stick gives you a floating origin, a dead zone and a custom feel. It is about 40 lines.

VirtualJoystick.cs — put on the stick base Image
using UnityEngine;
using UnityEngine.EventSystems;

public class VirtualJoystick : MonoBehaviour,
    IPointerDownHandler, IDragHandler, IPointerUpHandler
{
    [SerializeField] private RectTransform knob;
    [SerializeField] private float radius = 90f;
    [SerializeField] private float deadZone = 0.15f;

    private RectTransform baseRect;
    public Vector2 Value { get; private set; }

    void Awake() => baseRect = GetComponent<RectTransform>();

    public void OnPointerDown(PointerEventData e) => OnDrag(e);

    public void OnDrag(PointerEventData e)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            baseRect, e.position, e.pressEventCamera, out Vector2 local);

        Vector2 clamped = Vector2.ClampMagnitude(local, radius);
        knob.anchoredPosition = clamped;

        Vector2 raw = clamped / radius;                 // -1 .. 1
        Value = raw.magnitude < deadZone ? Vector2.zero : raw;
    }

    public void OnPointerUp(PointerEventData e)
    {
        knob.anchoredPosition = Vector2.zero;
        Value = Vector2.zero;                           // always reset
    }
}
  • ScreenPointToLocalPointInRectangle converts a finger position into coordinates inside the stick, whatever the screen resolution is.

  • ClampMagnitude keeps the knob inside the circle instead of a square — a square stick feels wrong immediately.

  • A dead zone of 0.15 stops the character drifting when a thumb rests on the screen.

Taps, drags and camera on the right half

C# script
void Update()
{
    // right half of the screen rotates the camera
    foreach (Touch t in Input.touches)
    {
        if (t.position.x < Screen.width * 0.5f) continue;   // left half is the stick
        if (t.phase != TouchPhase.Moved) continue;

        yaw   += t.deltaPosition.x * touchSensitivity;
        pitch -= t.deltaPosition.y * touchSensitivity;
    }
}

Mobile UI rules that are not optional

  • Tap targets at least 44 pt on iOS or 48 dp on Android — roughly 7 to 9 mm. Smaller and players miss constantly.

  • Keep the bottom corners clear of small buttons — that is where thumbs rest and where system gestures live.

  • Anchor UI to corners, not to the centre, then respect Screen.safeArea for notches.

  • Test in Simulator view (Game view dropdown → Simulator) with a real device profile before you build.

SafeAreaFitter.cs — put on a full-screen panel
using UnityEngine;

[RequireComponent(typeof(RectTransform))]
public class SafeAreaFitter : MonoBehaviour
{
    void Start()
    {
        RectTransform rt = GetComponent<RectTransform>();
        Rect safe = Screen.safeArea;

        Vector2 min = safe.position;
        Vector2 max = safe.position + safe.size;
        min.x /= Screen.width;  min.y /= Screen.height;
        max.x /= Screen.width;  max.y /= Screen.height;

        rt.anchorMin = min;
        rt.anchorMax = max;
        rt.offsetMin = rt.offsetMax = Vector2.zero;
    }
}