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.

Bindings (devices)ActionsYour codeAction Map: PlayerSpace keyGamepad South buttonW A S D (2D composite)Gamepad left stickJumpButtonMoveValue, Vector2jump.WasPressedThisFrame()move.ReadValue<Vector2>()readsreads
Many bindings feed one action, and your code only talks to the action. Adding a new device means adding a binding; the code on the right never changes.

Step 1: Install and enable

  1. Window > Package Manager > Unity Registry > Input System > Install. (Unity 6 templates usually have it already.)
  2. If Unity asks to enable the new backends and restart, click Yes.
  3. 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:

ColumnContainsIn this tutorial
Action MapsGroups of actions for a modePlayer and UI
ActionsWhat the player can doMove, Sprint, Dash, Pause
Bindings / PropertiesWhich controls trigger it, and settingsWASD + left stick, Shift + left stick press...

In the Player map, make sure these actions exist (add them with + if not):

ActionAction TypeControl TypeBindings
MoveValueVector 2"Up/Down/Left/Right Composite" with W/S/A/D; Left Stick [Gamepad]
SprintButtonLeft Shift [Keyboard]; Left Stick Press [Gamepad]
DashButtonSpace [Keyboard]; Button East [Gamepad]
PauseButtonEscape [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).

Composite bindingsTo bind WASD to a Vector2, click + next to the Move action and choose Add Up/Down/Left/Right Composite. Then set each of the four parts to a key. The Input System combines them into one Vector2 for you, already normalized so diagonals aren't faster (the composite's Mode setting controls this).

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.

ApproachHowGood for
InputSystem.actions.FindAction("Move")Look up by nameQuick prototypes
InputActionReference fieldPick in InspectorMost single-player games (used here)
PlayerInput componentComponent calls OnMove, OnJump... on your scriptsLocal multiplayer (one per player)
Generated C# classTick "Generate C# Class" on the asset; use controls.Player.MoveLarger 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

  1. Project-wide actions are enabled automatically when the game starts.
  2. You press Space. Before Update runs, the Input System matches the key to the Dash binding and moves the action to performed.
  3. That fires the performed event, which calls OnDash.
  4. Then Update runs and polls Move with ReadValue. 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.

UI navigationFor buttons to respond to the gamepad and mouse, the scene's EventSystem needs an Input System UI Input Module (not the old Standalone Input Module). Unity offers to replace it for you when you select the EventSystem.

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);
    }
}
Which bindingIndex?Bindings are numbered in the order they appear under the action in the editor. For composites like WASD, the composite itself is one index and each part (Up, Down, Left, Right) is the next ones. The Input System package includes a "Rebinding UI" sample (Package Manager > Input System > Samples) with a ready-made rebind UI you can copy.

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

See also