Using the Input System
In this tutorial you'll build a top-down character that walks, sprints and dashes with keyboard or gamepad, opens a pause menu that switches controls to UI mode, shows the correct button prompt, and lets the player rebind keys and keep their choices.
You'll need: Unity 6 (or 2022.3 with Input System 1.8+), a scene with a floor and a capsule for the player. If you haven't yet, read Handling Input first for the basic ideas.
Think of the Input System as a translator between the player's hands and your game. Your game speaks one language: "Move", "Jump", "Pause". Players speak many: keyboard, Xbox controller, PlayStation controller, touchscreen. The Input Actions asset is the translator's dictionary. You write the dictionary once, and your game code only ever hears the translated words.
Step 1: Install and enable
- Window > Package Manager > Unity Registry > Input System > Install. (Unity 6 templates usually have it already.)
- If Unity asks to enable the new backends and restart, click Yes.
- Check Project Settings > Player > Active Input Handling. "Input System Package (New)" or "Both" both work for this tutorial.
Step 2: Set up the actions
Open Project Settings > Input System Package. Unity 6 projects have a project-wide actions asset assigned here (usually InputSystem_Actions). If it's empty, click Create and assign a default project-wide Action Asset.
Double-click the asset to open the editor. You'll see three columns:
| Column | Contains | In this tutorial |
|---|---|---|
| Action Maps | Groups of actions for a mode | Player and UI |
| Actions | What the player can do | Move, Sprint, Dash, Pause |
| Bindings / Properties | Which controls trigger it, and settings | WASD + left stick, Shift + left stick press... |
In the Player map, make sure these actions exist (add them with + if not):
| Action | Action Type | Control Type | Bindings |
|---|---|---|---|
| Move | Value | Vector 2 | "Up/Down/Left/Right Composite" with W/S/A/D; Left Stick [Gamepad] |
| Sprint | Button | Left Shift [Keyboard]; Left Stick Press [Gamepad] | |
| Dash | Button | Space [Keyboard]; Button East [Gamepad] | |
| Pause | Button | Escape [Keyboard]; Start [Gamepad] |
In the UI map, add a Resume button action bound to Escape and Start as well. If you changed anything, click Save Asset (unless Auto-Save is ticked).
Step 3: Choose how scripts get the actions
There are several ways to reach actions from code. This tutorial uses InputActionReference fields: you pick the action from a dropdown in the Inspector, the code has no action name strings, and it works with the project-wide asset.
| Approach | How | Good for |
|---|---|---|
InputSystem.actions.FindAction("Move") | Look up by name | Quick prototypes |
InputActionReference field | Pick in Inspector | Most single-player games (used here) |
PlayerInput component | Component calls OnMove, OnJump... on your scripts | Local multiplayer (one per player) |
| Generated C# class | Tick "Generate C# Class" on the asset; use controls.Player.Move | Larger projects that want compile-time checked names |
Step 4: The movement script
Add a CharacterController to your capsule, then this script:
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class TopDownPlayer : MonoBehaviour
{
[Header("Input")]
[SerializeField] private InputActionReference moveAction;
[SerializeField] private InputActionReference sprintAction;
[SerializeField] private InputActionReference dashAction;
[Header("Movement")]
[SerializeField] private float walkSpeed = 4f;
[SerializeField] private float sprintSpeed = 7f;
[SerializeField] private float dashDistance = 4f;
[SerializeField] private float dashTime = 0.15f;
[SerializeField] private float dashCooldown = 0.6f;
private CharacterController controller;
private Vector3 lastMoveDir = Vector3.forward;
private bool isDashing;
private float nextDashTime;
void Awake()
{
controller = GetComponent<CharacterController>();
}
void OnEnable()
{
// "performed" fires once when a Button action is pressed.
dashAction.action.performed += OnDash;
}
void OnDisable()
{
dashAction.action.performed -= OnDash;
}
void Update()
{
if (isDashing) return;
// Polling: read the current value every frame.
Vector2 input = moveAction.action.ReadValue<Vector2>();
Vector3 dir = new Vector3(input.x, 0f, input.y);
if (dir.sqrMagnitude > 0.01f)
{
lastMoveDir = dir.normalized;
transform.rotation = Quaternion.LookRotation(lastMoveDir);
}
float speed = sprintAction.action.IsPressed() ? sprintSpeed : walkSpeed;
// CharacterController.Move doesn't apply gravity; add it yourself for slopes/stairs.
controller.Move(dir * speed * Time.deltaTime);
}
// Callback: runs when Dash is pressed.
private void OnDash(InputAction.CallbackContext ctx)
{
if (isDashing || Time.time < nextDashTime) return;
StartCoroutine(DashRoutine());
}
private IEnumerator DashRoutine()
{
isDashing = true;
nextDashTime = Time.time + dashCooldown;
float speed = dashDistance / dashTime;
for (float t = 0f; t < dashTime; t += Time.deltaTime)
{
controller.Move(lastMoveDir * speed * Time.deltaTime);
yield return null;
}
isDashing = false;
}
}
In the Inspector, click the circle next to each Input field and pick Player/Move, Player/Sprint and Player/Dash. Press Play, then try it with the keyboard and with a gamepad plugged in. It works with both, and you didn't write any device-specific code.
What happens at runtime
- Project-wide actions are enabled automatically when the game starts.
- You press Space. Before
Updateruns, the Input System matches the key to the Dash binding and moves the action to performed. - That fires the
performedevent, which callsOnDash. - Then
Updateruns and polls Move withReadValue. If you hold W and D, the composite gives roughly (0.71, 0.71).
Polling vs. callbacks: polling (ReadValue, IsPressed) fits things that are continuous, like movement. Callbacks (performed) fit one-off moments, like a dash. Both are fine; mixing them is normal.
Step 5: Pause menu and switching action maps
While paused, the player map should be off (so Escape doesn't dash or move), and the UI map should be on.
Action maps are like the modes on a TV remote. In "TV" mode the arrow keys change channels; in "Menu" mode the same arrow keys move the cursor. Switching maps changes what the same buttons mean.
using UnityEngine;
using UnityEngine.InputSystem;
public class PauseController : MonoBehaviour
{
[SerializeField] private GameObject pausePanel;
private InputActionMap playerMap;
private InputActionMap uiMap;
private InputAction pauseAction;
private InputAction resumeAction;
void Awake()
{
playerMap = InputSystem.actions.FindActionMap("Player");
uiMap = InputSystem.actions.FindActionMap("UI");
pauseAction = playerMap.FindAction("Pause");
resumeAction = uiMap.FindAction("Resume");
pausePanel.SetActive(false);
}
void Update()
{
if (pauseAction.WasPressedThisFrame()) SetPaused(true);
else if (resumeAction.WasPressedThisFrame()) SetPaused(false);
}
public void SetPaused(bool paused)
{
pausePanel.SetActive(paused);
Time.timeScale = paused ? 0f : 1f;
if (paused) { playerMap.Disable(); uiMap.Enable(); }
else { uiMap.Disable(); playerMap.Enable(); }
}
}
Hook the pause menu's "Resume" Button OnClick to PauseController.SetPaused with the box unticked.
Step 6: Show the right button prompt
"Press Space to dash" is wrong for a gamepad player. Ask the action for its binding's display name:
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
public class DashPrompt : MonoBehaviour
{
[SerializeField] private InputActionReference dashAction;
[SerializeField] private TMP_Text label;
private bool? usingGamepad; // null = not decided yet
void Update()
{
// Remember whichever device the player touched most recently.
bool gamepadUsed = Gamepad.current != null && Gamepad.current.wasUpdatedThisFrame;
bool keyboardUsed = Keyboard.current != null && Keyboard.current.wasUpdatedThisFrame;
bool nowGamepad = usingGamepad ?? false;
if (gamepadUsed) nowGamepad = true;
else if (keyboardUsed) nowGamepad = false;
if (nowGamepad == usingGamepad) return; // no change, don't rebuild the text
usingGamepad = nowGamepad;
string group = nowGamepad ? "Gamepad" : "Keyboard&Mouse";
string key = dashAction.action.GetBindingDisplayString(InputBinding.MaskByGroup(group));
label.text = $"Press {key} to dash";
}
}
This relies on the bindings being assigned to control schemes named "Keyboard&Mouse" and "Gamepad", which the default Unity 6 asset already has. The text only changes when the player switches device, not every frame.
Step 7: Let players rebind keys
Rebinding waits for the player to press any control, then assigns it to the action. The change is stored as an override on top of your original binding, so you can always reset it.
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
public class RebindButton : MonoBehaviour
{
[SerializeField] private InputActionReference action;
[SerializeField] private int bindingIndex = 0; // which binding of the action to change
[SerializeField] private TMP_Text label;
private InputActionRebindingExtensions.RebindingOperation operation;
void Start() { UpdateLabel(); }
// Hook this to the UI Button's OnClick.
public void StartRebind()
{
label.text = "Press a key...";
action.action.Disable(); // an action must be disabled while rebinding
operation = action.action.PerformInteractiveRebinding(bindingIndex)
.WithCancelingThrough("<Keyboard>/escape")
.OnComplete(op => Finish())
.OnCancel(op => Finish())
.Start();
}
private void Finish()
{
operation.Dispose();
action.action.Enable();
UpdateLabel();
RebindSaver.Save();
}
private void UpdateLabel()
{
label.text = action.action.GetBindingDisplayString(bindingIndex);
}
}
Save and load all overrides with one line each, using PlayerPrefs:
using UnityEngine;
using UnityEngine.InputSystem;
public static class RebindSaver
{
private const string Key = "InputRebinds";
public static void Save()
{
string json = InputSystem.actions.SaveBindingOverridesAsJson();
PlayerPrefs.SetString(Key, json);
PlayerPrefs.Save();
}
// Call once at startup, e.g. from a bootstrap script's Awake.
public static void Load()
{
string json = PlayerPrefs.GetString(Key, "");
if (!string.IsNullOrEmpty(json))
InputSystem.actions.LoadBindingOverridesFromJson(json);
}
public static void ResetAll()
{
InputSystem.actions.RemoveAllBindingOverrides();
PlayerPrefs.DeleteKey(Key);
}
}
Alternative: the PlayerInput component
If you'd rather not handle actions in code, add a Player Input component, assign the actions asset and set Behavior to Send Messages. It then calls methods named On + the action name on your scripts:
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerInputReceiver : MonoBehaviour
{
private Vector2 move;
void OnMove(InputValue value) { move = value.Get<Vector2>(); }
void OnDash() { Debug.Log("Dash"); }
void Update()
{
transform.position += new Vector3(move.x, 0f, move.y) * 4f * Time.deltaTime;
}
}
For local multiplayer, add a Player Input Manager and a player prefab with PlayerInput. Each player who presses a button on a new device gets their own copy with their own controls.
Common Mistakes
- Nothing happens. The action isn't enabled (only project-wide actions enable themselves), or the Inspector reference is empty.
- Movement keeps going after releasing keys. You used
performedfor a Value action and never handledcanceled. Poll withReadValueinstead. - Double input after scene reload. You subscribed in
Awake/Startand never unsubscribed. UseOnEnable/OnDisable. - Buttons in the menu don't work with a gamepad. Replace the EventSystem's input module, and set a "First Selected" button on the EventSystem.
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official Unity documentation.