Unity Input System Tutorial: A Beginner’s Guide for Unity 6

Unity new Input System beginner guide

If you learned Unity a few years ago, you probably learned Input.GetKeyDown and Input.GetAxis. That’s the legacy Input Manager. It still works, but Unity’s Input System package is the recommended way to handle input, and in Unity 6 most project templates ship with it installed and preconfigured. It supports rebindable controls, gamepads, touch, local multiplayer and cross-platform input in a way the old system never could. This guide takes you from zero to a working, rebindable character controller, with complete code and fixes for the errors everyone hits.

Why Switch? Old vs. New at a Glance

The legacy Input Manager has real limitations: no built-in rebinding, awkward multi-device handling, inconsistent gamepad mappings across platforms, and string-based axes configured in one global settings page. The Input System replaces all of that with actions: you write code against “Move” and “Jump”, and bindings decide which keys, sticks or touches produce them.

Task Legacy Input Manager Input System
Key pressed this frame Input.GetKeyDown(KeyCode.Space) jumpAction.WasPressedThisFrame()
Button held Input.GetButton("Fire1") attackAction.IsPressed()
Movement axes Input.GetAxis("Horizontal") + "Vertical" moveAction.ReadValue<Vector2>()
Mouse position Input.mousePosition Mouse.current.position.ReadValue()
Direct key check (prototyping) Input.GetKey(KeyCode.E) Keyboard.current.eKey.isPressed
Player rebinding Build it yourself PerformInteractiveRebinding()

Step 1: Install and Enable the Input System

Unity 6: open Window > Package Manager > In Project. If “Input System” is listed, it’s already installed, and you’ll usually find an InputSystem_Actions asset in your Assets folder. Skip to Step 2.

Older versions or empty templates: in Package Manager, choose Unity Registry, search for “Input System” and click Install. Unity asks whether to enable the new input backends and restart. Say yes.

Then check Edit > Project Settings > Player > Other Settings > Active Input Handling:

  • Input System Package (New): the clean choice for new projects.
  • Both: use this while migrating an existing project, or if an Asset Store package still calls the old Input class.

Changing this setting restarts the Editor.

Step 2: Understand Actions, Action Maps and Bindings

  • Actions are what the player is trying to do: Move, Look, Jump, Attack.
  • Bindings are the physical inputs that trigger an action: W/A/S/D, the left stick, a touch joystick. One action can have many bindings, so your code never branches per device.
  • Action maps group actions by context, typically Player and UI. You enable the map that fits the moment, such as switching to UI when a menu opens.
  • Control schemes (Keyboard&Mouse, Gamepad, Touch…) group bindings by device, which matters for local multiplayer and for showing the right button prompts.

In Unity 6 the easiest starting point is project-wide actions: go to Edit > Project Settings > Input System Package. If no asset is assigned, click the button to create one. You get InputSystem_Actions with a Player map (Move, Look, Jump, Attack and more) and a UI map, already bound for keyboard and mouse, gamepad, touch and XR. Project-wide actions are enabled automatically, which removes the most common beginner bug.

To make your own asset instead, right-click in the Project window > Create > Input Actions and double-click it to edit. For a movement action, set Action Type = Value and Control Type = Vector 2, then use + > Add Up/Down/Left/Right Composite to bind WASD, plus a second binding for <Gamepad>/leftStick. Jump is Action Type = Button.

Illustration of a game controller connected to Unity's Input System action mapping

Step 3: Read Input in C#

There are several ways to consume actions. Start with the first one; the others solve specific problems.

Option A: Project-wide actions (simplest in Unity 6)

A complete, working 3D character controller. Add it to a GameObject with a CharacterController:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    [SerializeField] float speed = 5f;
    [SerializeField] float jumpHeight = 1.2f;
    [SerializeField] float gravity = -20f;

    CharacterController controller;
    InputAction moveAction, jumpAction;
    float verticalVelocity;

    void Awake()
    {
        controller = GetComponent<CharacterController>();
        // throwIfNotFound turns a typo into a clear error instead of a NullReferenceException later
        moveAction = InputSystem.actions.FindAction("Player/Move", throwIfNotFound: true);
        jumpAction = InputSystem.actions.FindAction("Player/Jump", throwIfNotFound: true);
    }

    void Update()
    {
        Vector2 input = moveAction.ReadValue<Vector2>();

        // Input is 2D (x, y). For 3D ground movement, map input.y to the world Z axis.
        Vector3 move = new Vector3(input.x, 0f, input.y) * speed;

        if (controller.isGrounded)
        {
            verticalVelocity = -2f;                                  // keeps the controller snapped to the ground
            if (jumpAction.WasPressedThisFrame())
                verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }
        verticalVelocity += gravity * Time.deltaTime;
        move.y = verticalVelocity;

        controller.Move(move * Time.deltaTime);
    }
}

For camera-relative movement, where “up” on the stick means “away from the camera”, replace the move line with:

Transform cam = Camera.main.transform;
Vector3 forward = Vector3.ProjectOnPlane(cam.forward, Vector3.up).normalized;
Vector3 right   = Vector3.ProjectOnPlane(cam.right,   Vector3.up).normalized;
Vector3 move = (forward * input.y + right * input.x) * speed;

The polling methods you’ll use most on an InputAction: ReadValue<T>() for sticks and axes, WasPressedThisFrame() and WasReleasedThisFrame() for one-shot button events, and IsPressed() for “held”.

Option B: Events (react instead of polling)

Each action raises started, performed and canceled events. Subscribing is neat for one-off actions like Interact or Pause. Always unsubscribe: a destroyed object that’s still subscribed throws MissingReferenceException the next time the button is pressed.

InputAction interactAction;

void OnEnable()
{
    interactAction = InputSystem.actions.FindAction("Player/Interact", throwIfNotFound: true);
    interactAction.performed += OnInteract;
}

void OnDisable() => interactAction.performed -= OnInteract;

void OnInteract(InputAction.CallbackContext context) => Debug.Log("Interact!");

Option C: InputActionReference (pick actions in the Inspector)

Expose [SerializeField] InputActionReference move; and drag an action onto it in the Inspector, then read move.action.ReadValue<Vector2>(). This is handy for reusable components where designers choose the action. If the reference points to an asset that isn’t your project-wide one, enable it yourself with move.action.Enable(). Don’t call Disable() in OnDisable on a project-wide action, because that switches it off for every script.

Option D: Generated C# class (type-safe)

Select your .inputactions asset, tick Generate C# Class in the Inspector, and click Apply. Unity writes a class (named after the asset) with a property for every map and action, so a renamed action becomes a compile error instead of a silent null:

PlayerControls controls;

void Awake()     => controls = new PlayerControls();
void OnEnable()  => controls.Player.Enable();   // generated instances are NOT enabled automatically
void OnDisable() => controls.Player.Disable();
void OnDestroy() => controls.Dispose();

void Update()
{
    Vector2 move = controls.Player.Move.ReadValue<Vector2>();
    if (controls.Player.Jump.WasPressedThisFrame()) { /* jump */ }
}

Each new PlayerControls() is its own copy of the actions, separate from InputSystem.actions. Keep that in mind when you apply rebinds.

Quick device checks for prototypes

For throwaway debug keys you can skip actions entirely: Keyboard.current.f1Key.wasPressedThisFrame, Mouse.current.leftButton.isPressed, Gamepad.current?.buttonSouth.wasPressedThisFrame. The ?. matters, since Gamepad.current is null when no gamepad is connected. Don’t build real gameplay on these, because they bypass rebinding.

The PlayerInput Component

PlayerInput is a component that owns an actions asset and calls your code for you. It’s the foundation for local multiplayer. Add it to your player, assign Actions, set Default Map to Player, then choose a Behavior:

  • Send Messages: calls methods named On + the action name (OnMove, OnJump) on the same GameObject, passing an InputValue.
  • Invoke Unity Events: you wire each action to a method in the Inspector. Methods take InputAction.CallbackContext.
  • Invoke C# Events: subscribe to playerInput.onActionTriggered in code.

With Send Messages, this 2D top-down controller needs no other setup:

using UnityEngine;
using UnityEngine.InputSystem;

public class TopDownMover : MonoBehaviour
{
    [SerializeField] float speed = 5f;
    Vector2 moveInput;

    // Called by PlayerInput (Behavior = Send Messages) whenever "Move" changes
    void OnMove(InputValue value) => moveInput = value.Get<Vector2>();

    void OnJump(InputValue value)
    {
        if (value.isPressed) Debug.Log("Jump pressed");
    }

    void Update() => transform.Translate(moveInput * speed * Time.deltaTime); // X/Y plane for 2D
}

Interactions and Processors

Two features in the actions editor save a lot of code:

  • Interactions change when an action performs: Hold (after a duration), Tap, Slow Tap, Multi Tap (double-tap) and Press (press, release, or both).
  • Processors change the value: Stick Deadzone, Normalize Vector 2, Invert (for “invert Y” options), Scale (sensitivity) and Clamp.

With a Hold interaction on a “ChargeAttack” action, the three events map neatly onto a charge mechanic:

charge.started   += _ => StartChargeEffect();   // button went down
charge.performed += _ => ReleaseChargedAttack(); // held long enough
charge.canceled  += _ => CancelCharge();          // released too early

UI and the EventSystem

Unity UI buttons, sliders and navigation are driven by the EventSystem, and it needs the Input System’s module. Select the EventSystem in your scene. If it has a Standalone Input Module, click Replace with InputSystemUIInputModule. Forgetting this is a very common source of the “you are trying to read Input using the UnityEngine.Input class” error in projects that have switched fully to the new system. The module uses your UI action map for navigate, submit, cancel, point and click, so gamepad menu navigation works without extra code.

Local Multiplayer with PlayerInputManager

This is where the Input System really beats the legacy manager. Add a PlayerInputManager component to an empty GameObject, set Join Behavior to Join Players When Button Is Pressed, and assign a Player Prefab that has a PlayerInput on it. Each time a new device presses a button, Unity spawns a player, pairs that device to it exclusively, and gives it its own copy of the actions. Two gamepads, or a gamepad plus the keyboard, just work. Turn on Split-Screen in the same component for automatic camera splitting, and listen to onPlayerJoined to place each new player at a spawn point.

Let Players Rebind Controls (and Keep Them)

Runtime rebinding is built in. The version below is production-ready: it cancels with Escape, ignores mouse movement, shows the new binding, and saves overrides so they survive a restart, which the minimal examples usually leave out. Put it on a UI button and call StartRebind() from the button’s OnClick:

using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;

public class RebindButton : MonoBehaviour
{
    public const string SaveKey = "input-rebinds";

    [SerializeField] InputActionReference actionRef;
    [SerializeField] int bindingIndex;   // which binding of the action to change (see below)
    [SerializeField] TMP_Text label;

    InputActionRebindingExtensions.RebindingOperation operation;

    void Start() => UpdateLabel();

    public void StartRebind()
    {
        InputAction action = actionRef.action;
        action.Disable();                                  // actions must be disabled while rebinding
        label.text = "Press a key…";

        operation = action.PerformInteractiveRebinding(bindingIndex)
            .WithControlsExcluding("<Mouse>/position")   // don't bind to mouse movement
            .WithControlsExcluding("<Mouse>/delta")
            .WithCancelingThrough("<Keyboard>/escape")
            .OnMatchWaitForAnother(0.1f)
            .OnComplete(_ => Finish(action, save: true))
            .OnCancel(_ => Finish(action, save: false))
            .Start();
    }

    void Finish(InputAction action, bool save)
    {
        operation.Dispose();                               // rebinding operations must be disposed
        action.Enable();
        if (save)
            PlayerPrefs.SetString(SaveKey, action.actionMap.asset.SaveBindingOverridesAsJson());
        UpdateLabel();
    }

    void UpdateLabel() => label.text = actionRef.action.GetBindingDisplayString(bindingIndex);
}

Load the saved overrides once at startup, for example in your bootstrap or main menu script, before any gameplay reads input:

string json = PlayerPrefs.GetString(RebindButton.SaveKey, string.Empty);
if (!string.IsNullOrEmpty(json))
    InputSystem.actions.LoadBindingOverridesFromJson(json);

Finding the right bindingIndex: composite bindings like WASD are stored as one “2DVector” entry followed by its parts (up, down, left, right). To rebind “move forward” you target the part, not the composite. Log the list once to see the indexes:

InputAction move = InputSystem.actions.FindAction("Player/Move");
for (int i = 0; i < move.bindings.Count; i++)
{
    InputBinding b = move.bindings[i];
    Debug.Log($"{i}: {b.name} {b.effectivePath} (part of composite: {b.isPartOfComposite})");
}

For a complete rebinding screen, import the Rebinding UI sample from Package Manager > Input System > Samples. It’s a good reference for handling duplicate bindings and gamepad icons.

🎮 Shipping couch co-op or heavy gamepad support?

The Input System handles most games well. If you need the widest controller coverage and a ready-made remapping UI, Rewired is the long-standing Asset Store alternative. It’s in our roundup of tools worth buying.

See Rewired and 9 other tools worth buying →

Troubleshooting

“InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package in Player Settings.”

Something is still calling the legacy Input class while Active Input Handling is set to the new system only. The usual suspects, in order:

  1. The EventSystem still uses Standalone Input Module. Replace it with InputSystemUIInputModule (see UI and the EventSystem).
  2. Your own scripts still call Input.GetKey, Input.GetAxis or Input.mousePosition. Double-click the error to jump to the line and convert it using the table at the top.
  3. An Asset Store package or old sample uses the legacy API. Set Active Input Handling to Both until it’s updated.

Actions never fire, or ReadValue always returns zero

  • The action map isn’t enabled. Project-wide actions are enabled automatically, but your own assets, generated classes and InputActionReferences to them are not. Call .Enable().
  • The Game view doesn’t have focus. In the Editor, keyboard input only reaches the game while the Game view is focused. Click inside it.
  • Wrong action type. A Move action set to Button won’t give you a Vector2. Use Value + Vector 2.
  • Something disabled it. Another script calling Disable() on a shared project-wide action switches it off everywhere.

Open Window > Analysis > Input Debugger while in Play Mode. It lists connected devices, live control values and which actions are enabled, which answers most “why isn’t this working” questions in seconds.

OnMove is never called (PlayerInput)

Check that Behavior is Send Messages, the script is on the same GameObject as PlayerInput, the method name is exactly On + the action name, and it takes InputValue (not InputAction.CallbackContext, which is for Unity Events).

Jump sometimes doesn’t register

You’re probably checking WasPressedThisFrame() in FixedUpdate. Physics steps don’t line up with rendered frames, so presses get missed. Read input in Update, store it in a bool, and consume it in FixedUpdate. Alternatively, set Update Mode to Process Events In Fixed Update under Project Settings > Input System Package.

My gamepad isn’t detected

Check the Input Debugger first. If the device appears under “Unsupported”, it has no layout. If it doesn’t appear at all, it’s a driver or connection issue. When testing from Steam, Steam Input may be remapping the controller; disable it for your game in Steam if the input looks wrong.

Diagonal movement is faster than straight movement

Set the WASD composite’s Mode to Digital Normalized, or add a Normalize Vector 2 processor. Keep analog sticks un-normalized so partial tilts still walk slowly.

error CS0246: The type or namespace name ‘InputAction’ could not be found

Add using UnityEngine.InputSystem; at the top of the script. If the error stays, check that the Input System package is installed in the Package Manager. If your scripts live in an Assembly Definition, add Unity.InputSystem to its Assembly Definition References. Our CS0246 guide covers the other causes.

FAQ

Is the old Input Manager deprecated?

Unity recommends the Input System for all new projects, and new features such as project-wide actions and UI Toolkit runtime input are built around it. The legacy manager still works for now, but new projects shouldn’t depend on it.

Can I use the old and new systems at the same time?

Yes. Set Active Input Handling to Both. It’s useful during migration or when a third-party package still uses Input.GetAxis. Aim to remove the legacy calls eventually so you can switch to the new system only.

Does the Input System work with touch and mobile?

Yes. Touch bindings work in actions like any other device, EnhancedTouch gives you multi-finger APIs, and the On-Screen Stick and On-Screen Button components turn UI elements into virtual gamepad controls that feed your existing actions.

Should I use PlayerInput or read actions in code?

For single-player games, reading project-wide actions in code (Option A) is the most transparent: you can see exactly where input comes from. Use PlayerInput when you need local multiplayer, automatic device pairing or control-scheme switching.

Why is there no option to create Input Actions?

The Create > Input Actions menu entry only appears once the Input System package is installed and compiling. Install it from the Package Manager, let Unity restart, and clear any red Console errors. In Unity 6 you can also skip the menu and use the project-wide actions asset under Project Settings > Input System Package.

Is It Worth Learning Now?

Yes. The Input System is Unity’s standard, it’s actively maintained, and in Unity 6 it’s the default for new projects. Learning it now saves you a migration later and unlocks rebinding, gamepad support and local co-op, all things players notice and review scores reflect.

📚 New to C# too?

If the C# in this guide felt unfamiliar, a good beginner book will get you further than piecing together forum answers. Head First C# is our top pick for complete beginners.

See our recommended books for game developers →

Comments

5 responses to “Unity Input System Tutorial: A Beginner’s Guide for Unity 6”

  1. […] fields, so you can drag actions straight from the asset into the Inspector. New to actions? Our Input System beginner’s guide explains maps and […]

  2. […] keeps the example short. In a real project you’d use an Input Action asset, as shown in our Input System beginner’s guide. Notice that Speed uses input rather than velocity. Velocity briefly reads non-zero when you stop […]

  3. […] Press Play and click around. The agent plans a path around the walls and walks it. This uses the new Input System; if you’re not familiar with it, see our Input System beginner’s guide. […]

  4. […] sure the Input System package is installed too; the input examples below rely on it. See our Input System beginner’s guide if you haven’t used it […]

  5. […] Make sure Project Settings > Player > Active Input Handling is set to Input System Package (New) or Both. Then open Window > Analysis > Input Debugger with the pad plugged in. If it isn’t listed there, no script will see it. New to actions? Start with our beginner’s guide to the Input System. […]

Leave a Reply

Your email address will not be published. Required fields are marked *