Touch controls and joystick
Assets/
Scenes/
Arena.unity
Scripts/
Settings/
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.
Thumb drags the knob, the knob becomes a direction, the direction feeds the same Move action.
The fast path: On-Screen Controls
- 1
Create a UI Canvas: , and name it TouchCanvas. Set Render Mode = Screen Space Overlay.
- 2
Canvas Scaler → Scale With Screen Size, reference 1080×1920, Match 0.5. Without this your UI is tiny on some phones.
- 3
Add an Image for the stick base, and a child Image for the knob.
- 4
Put an On-Screen Stick component on the knob and set Control Path to the Move binding.
- 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.
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
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.
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;
}
}