# 5. Worked Example: On-Screen Stick Player
Part of [[Mobile Input and Touch]]. Prev: [[4. Device Simulator and Safe Area]]. Next: [[6. Mobile Input Pitfalls]].
Reuse the remappable player from [[5. Worked Example Remappable Player]]. Do not rewrite movement. The phone overlay **simulates a Gamepad** (design A in part 1) so the existing Move / Jump (or Attack) actions keep their Gamepad bindings.
Scripts below were **not compiled** in Unity and were **not** play-tested on a physical device.
## 5.1 Scene setup
1. The existing player, Actions asset, and `PlayerInput` (or generated wrapper) already in the scene. Auto-switch may stay **on**; the stick will use isolated input.
2. A Screen Space Overlay **Canvas**, an **EventSystem** with the Input System UI module.
3. A full-screen child with `SafeAreaFitter` (part 4). Name it `SafeArea`.
4. Under `SafeArea`, an Image in the lower-left: `MoveStick`. Add `OnScreenStick`:
- Control Path: `<Gamepad>/leftStick`
- Movement Range: `80` (tune after you feel it)
- **Use Isolated Input Actions:** enabled
5. Optional: a uGUI Button in the lower-right with `OnScreenButton`, path `<Gamepad>/buttonSouth` (Jump, or whatever `buttonSouth` already is in your asset).
6. A thin script that shows the overlay when a touchscreen is present (or when you force it in the Inspector for Simulator testing) and hides it for a keyboard-only playtest.
```csharp
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// Shows the on-screen stick canvas when a touchscreen is present, or when forced.
/// NOT compiled in Unity; not play-tested on a device.
/// </summary>
public sealed class OnScreenControlsVisibility : MonoBehaviour
{
[SerializeField] GameObject overlayRoot;
[SerializeField] bool forceVisibleInEditor;
void OnEnable()
{
InputSystem.onDeviceChange += OnDeviceChange;
Refresh();
}
void OnDisable() => InputSystem.onDeviceChange -= OnDeviceChange;
void OnDeviceChange(InputDevice device, InputDeviceChange change) => Refresh();
void Refresh()
{
if (overlayRoot == null) return;
bool visible = forceVisibleInEditor || Touchscreen.current != null;
overlayRoot.SetActive(visible);
}
}
```
`InputSystem.onDeviceChange` and `Touchscreen.current` are existing Input System 1.20 APIs already in the spirit of [[2. Control Schemes Devices and Consumption Patterns]]. `forceVisibleInEditor` exists because a desktop Editor often has **no** `Touchscreen` until the Device Simulator (or a similar path) injects one. If `Touchscreen.current` stays null in the Simulator in your 1.20.x version, leave the force flag on for Editor tests and gate the overlay on `Application.isMobilePlatform` (confirm 6000.5 Scripting Reference) in a player build. Do not invent a Simulator-only API for this.
## 5.2 Do not add a second Move reader
The stick writes into the Gamepad left stick. The player already reads the **Move** action. You should not also poll `OnScreenStick` in `Update`, and you should not also poll `Touch.activeTouches` to move the character. If movement is twice as fast on a phone, you added a second reader.
If you use a **generated C# wrapper** *and* `PlayerInput`, the Input System series already forbids constructing a second asset: read `playerInput.actions`. The stick does not change that rule.
## 5.3 A tiny HUD so you can see the value
```csharp
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
/// <summary>
/// Labels the current Move action value. Wire the same action the player uses.
/// NOT compiled in Unity.
/// </summary>
public sealed class MoveValueLabel : MonoBehaviour
{
[SerializeField] InputActionReference move;
[SerializeField] Text label;
void OnEnable() => move.action.Enable();
void OnDisable()
{
// Do not disable an action you do not own if PlayerInput is also enabling it.
// This Enable/Disable pair is for a scene that uses the reference alone.
}
void Update()
{
if (move == null || label == null) return;
var v = move.action.ReadValue<Vector2>();
label.text =
quot;Move {v.x:F2}, {v.y:F2}";
}
}
```
If `PlayerInput` already owns the asset, **do not** disable the action in `OnDisable` of this label (the comment is the lesson). Prefer reading from `playerInput.actions["Move"]` in that setup.
## 5.4 Playtest checklist
1. Keyboard playtest: overlay hidden (force flag off, no touchscreen) - WASD still works; remapping from [[3. Runtime Rebinding]] still works.
2. Device Simulator, Play Mode, notch device, force flag on if needed: drag the stick, `Move` is non-zero, the **player walks**, the stick does **not** snap back mid-drag.
3. Toggle `useIsolatedInputActions` off once and confirm the official snap-back, then turn it back on.
4. Optional jump button fires the same Jump action as `<Gamepad>/buttonSouth`.
5. Rotate to landscape; the stick stays inside `Screen.safeArea`.
6. A **player build** on a phone: same checklist, plus "can I reach the stick with a thumb." Multitouch is out of scope for the Simulator half of this test.
## 5.5 Exercise
Ship the overlay on the remappable player. Record a Simulator screenshot (portrait + landscape) and, if you have a device, a 10-second clip of walking with the thumb stick. Write one sentence about why isolated input is on.
## Sources
- [[5. Worked Example Remappable Player]]
- https://docs.unity3d.com/Packages/[email protected]/manual/create-on-screen-stick-control.html
- https://docs.unity3d.com/6000.5/Documentation/Manual/device-simulator-introduction.html