Unity Input System Rebinding: Let Players Change Controls

Illustration: a holographic keyboard key being swapped with a gamepad button

Players expect to change their controls. Left-handed players, AZERTY keyboards, accessibility needs and people who simply hate jumping on Space all need it, and on PC a missing rebind menu is one of the most common complaints in reviews. The good news is that Unity Input System rebinding is built in. This guide builds a complete rebind menu: interactive “press a key” rebinding, composite bindings like WASD, cancelling, duplicate detection, saving overrides to JSON and showing the right label or icon for each binding.

How Rebinding Works in the Input System

The Input System never edits your .inputactions asset when a player rebinds. Instead, each binding has an optional override path. When an override exists, the action listens to that path instead of the original. This design gives you three useful properties:

  • The original bindings stay intact, so “Reset to default” is just removing the override.
  • Only the overrides need saving, which is a tiny JSON string.
  • Your gameplay code doesn’t change at all. It still reads jumpAction.WasPressedThisFrame().

If you are new to actions and action maps, read our beginner’s guide to the new Input System first, because everything here builds on an Input Action Asset.

Setting Up Actions for Rebinding

A few decisions in the asset make rebinding much easier later:

  • Use control schemes such as “Keyboard&Mouse” and “Gamepad”, and assign each binding to one. That lets you rebind the keyboard binding of Jump without touching the gamepad one.
  • One binding per scheme per action for simple buttons. Two keyboard bindings on Jump makes the UI ambiguous.
  • Use the same asset instance everywhere. If your player uses a generated C# class and your menu uses a separate asset reference, overrides applied to one are invisible to the other. Pick one shared instance.

A simple way to share it is a single PlayerInput component (its actions property is the runtime instance) or a static holder for the generated class. In this guide, the menu works from an InputActionReference, which points into the asset assigned in the Inspector. That works as long as gameplay uses the same asset, for example through PlayerInput with that asset assigned, or through InputActionReference fields everywhere.

Interactive Rebinding: Press Any Key

The core API is PerformInteractiveRebinding, an extension method on InputAction. It waits for the player to actuate a control and writes it as an override. The action must be disabled while rebinding, otherwise you get an exception.

using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;

public class RebindButton : MonoBehaviour
{
    [SerializeField] InputActionReference actionRef;
    [SerializeField] string bindingGroup = "Keyboard&Mouse"; // control scheme
    [SerializeField] TMP_Text label;
    [SerializeField] GameObject waitingOverlay;

    InputActionRebindingExtensions.RebindingOperation op;

    InputAction Action => actionRef.action;

    void OnEnable() => UpdateLabel();

    int GetBindingIndex() =>
        Action.GetBindingIndex(InputBinding.MaskByGroup(bindingGroup));

    public void StartRebind()
    {
        int index = GetBindingIndex();
        if (index < 0) { Debug.LogWarning("No binding for group " + bindingGroup); return; }
        StartRebind(index);
    }

    void StartRebind(int index)
    {
        bool wasEnabled = Action.enabled;
        Action.Disable();
        waitingOverlay.SetActive(true);

        op = Action.PerformInteractiveRebinding(index)
            .WithControlsExcluding("<Mouse>/position")
            .WithControlsExcluding("<Mouse>/delta")
            .WithCancelingThrough("<Keyboard>/escape")
            .OnMatchWaitForAnother(0.1f)
            .OnComplete(o => Finish(wasEnabled))
            .OnCancel(o => Finish(wasEnabled))
            .Start();
    }

    void Finish(bool reEnable)
    {
        op.Dispose();
        op = null;
        waitingOverlay.SetActive(false);
        if (reEnable) Action.Enable();
        UpdateLabel();
    }

    public void ResetToDefault()
    {
        Action.RemoveBindingOverride(GetBindingIndex());
        UpdateLabel();
    }

    public void UpdateLabel()
    {
        int index = GetBindingIndex();
        label.text = index < 0 ? "-" : Action.GetBindingDisplayString(index);
    }

    void OnDisable() => op?.Cancel();
}

Wire the button’s onClick to StartRebind. Some details worth understanding:

  • Always Dispose() the operation. It allocates unmanaged memory. Forgetting it leaks and logs warnings.
  • OnMatchWaitForAnother(0.1f) waits a moment after the first match so a better candidate can win. Without it, pressing a gamepad trigger can bind a noisy stick axis first.
  • Exclude mouse position and delta, or simply moving the mouse to click the button rebinds the action to mouse movement.
  • WithCancelingThrough means Escape cancels instead of being bound. If you want Escape to be bindable, choose a different cancel control or use a visible Cancel button that calls op.Cancel().

To restrict a gamepad rebind to gamepad controls only, add .WithControlsHavingToMatchPath("<Gamepad>"). For a keyboard row, use "<Keyboard>" plus "<Mouse>" (you can call it twice to allow both).

Rebinding Composite Bindings (WASD)

A Move action with a 2D Vector composite has one binding for the composite itself and then one binding per part: Up, Down, Left, Right. The composite binding has no path you can rebind. You rebind the parts. In action.bindings, the composite has isComposite == true and each part following it has isPartOfComposite == true.

The cleanest UI is one button per direction. To find the index of a specific part, walk the bindings:

int FindPart(InputAction action, string group, string partName)
{
    var bindings = action.bindings;
    for (int i = 0; i < bindings.Count; i++)
    {
        var b = bindings[i];
        if (b.isPartOfComposite && b.name == partName && b.groups != null && b.groups.Contains(group))
            return i;
    }
    return -1;
}

// Usage: StartRebind(FindPart(Action, "Keyboard&Mouse", "up"));

Part names are lowercase in the bindings list (“up”, “down”, “left”, “right”). If you want a single “Rebind movement” button that walks through all four, start the next part’s rebind from inside OnComplete until isPartOfComposite is false on the next index. Show the part name (“Press key for: Left”) on the overlay so the player knows which one they’re setting.

Preventing Duplicate Bindings

If the player binds Jump to F and F was already Interact, both actions fire together. You can check for a clash before accepting the rebind. OnPotentialMatch runs when a control is detected, before it’s applied, and you can cancel from there:

.OnPotentialMatch(o =>
{
    string newPath = o.selectedControl.path;
    foreach (var other in Action.actionMap.bindings)
    {
        if (other.action == Action.name) continue;
        if (other.effectivePath == newPath)
        {
            Debug.Log("Already used by " + other.action);
            o.Cancel();
            return;
        }
    }
})

Note that selectedControl.path includes the device name, such as /Keyboard/f, while binding paths usually use layouts, such as <Keyboard>/f. For a robust comparison, use InputControlPath.Matches(other.effectivePath, o.selectedControl) instead of string equality. Many games choose to swap rather than refuse: apply the new binding and give the old action the previous key. Either way, tell the player what happened.

Saving and Loading Overrides as JSON

The asset can serialise every override in one call. Save it when the menu closes and load it at startup, before any gameplay reads input:

using UnityEngine;
using UnityEngine.InputSystem;

public class BindingSaver : MonoBehaviour
{
    [SerializeField] InputActionAsset actions;
    const string Key = "rebinds";

    void Awake()
    {
        string json = PlayerPrefs.GetString(Key, string.Empty);
        if (!string.IsNullOrEmpty(json))
            actions.LoadBindingOverridesFromJson(json);
    }

    public void Save()
    {
        PlayerPrefs.SetString(Key, actions.SaveBindingOverridesAsJson());
        PlayerPrefs.Save();
    }

    public void ResetAll()
    {
        actions.RemoveAllBindingOverrides();
        PlayerPrefs.DeleteKey(Key);
    }
}

The JSON only contains overrides, so it survives you adding new actions in a later update. If you already have a save file, you can store this string inside it instead of PlayerPrefs; our JSON save system guide shows a setup that handles that. After calling ResetAll, refresh every rebind button’s label.

Display Strings and Binding Icons

GetBindingDisplayString(index) returns a human-friendly name: “Space”, “W”, “Left Button”, “A” on an Xbox pad, “Cross” on a DualShock. It respects keyboard layouts, so on an AZERTY keyboard the key at the W position shows as “Z”. That alone covers most PC games.

For icons, use the overload that also gives you the device layout and control path:

string text = Action.GetBindingDisplayString(index,
    out string deviceLayout, out string controlPath);

Sprite icon = null;
if (InputSystem.IsFirstLayoutBasedOnSecond(deviceLayout, "DualShockGamepad"))
    icon = psIcons.Get(controlPath);        // "buttonSouth" -> Cross sprite
else if (InputSystem.IsFirstLayoutBasedOnSecond(deviceLayout, "Gamepad"))
    icon = xboxIcons.Get(controlPath);      // "buttonSouth" -> A sprite

iconImage.sprite = icon;
iconImage.enabled = icon != null;
label.enabled = icon == null;

Here psIcons and xboxIcons are your own ScriptableObjects mapping control paths like buttonSouth, leftShoulder and rightTrigger to sprites. The Input System package also ships a “Rebinding UI” sample (Package Manager, Input System, Samples) with a ready-made icon example you can copy from. For full controller prompt handling, including switching when the player picks up a different device, see our controller support guide.

🎮 Want a finished input UI instead?

Several Asset Store packs give you polished rebind menus, controller glyph sets and prompt switching out of the box, which saves days of UI work.

See our favourite Unity Asset Store tools →

Troubleshooting

InvalidOperationException: cannot rebind action while it is enabled

Call action.Disable() before PerformInteractiveRebinding and re-enable it in both OnComplete and OnCancel. If a PlayerInput re-enables the map every frame, disable the whole map or switch to a UI map while the rebind menu is open.

Rebind works in the menu but gameplay still uses the old key

The menu and the player are using different instances of the actions. A generated C# class created with new PlayerControls() is a separate copy from the asset in your project. Share one instance, or load the same JSON into both.

The rebind triggers instantly with the mouse click

The click on the button itself is captured. Exclude <Mouse>/leftButton or add .OnMatchWaitForAnother and start the rebind on the next frame. Moving the mouse binding to position means you didn’t exclude position and delta.

Saved bindings don’t load

Load must happen before the actions are first used and on the same asset instance. Also check that binding IDs didn’t change: deleting and recreating a binding in the asset gives it a new ID, and old overrides no longer match.

Composite shows “W/A/S/D” and won’t rebind

You passed the composite’s own index. Rebind its part indices instead.

FAQ

Does rebinding change my .inputactions file?

No. Rebinding adds runtime overrides. The asset on disk stays the same, which is why you need to save and load the overrides yourself.

Where should I store rebinds?

PlayerPrefs is fine for most games since the JSON is small. If you already have a settings or save file, store the string there so cloud saves include it.

Can players rebind gamepad buttons too?

Yes. Use the gamepad binding index (mask by the Gamepad group) and restrict candidates with WithControlsHavingToMatchPath(“<Gamepad>”).

How do I reset a single binding?

Call RemoveBindingOverride with that binding index. RemoveAllBindingOverrides on the asset resets everything.

Is there an official example?

Yes. Import the “Rebinding UI” sample from the Input System package in the Package Manager. It includes a reusable rebind prefab and gamepad icon support.

Comments

Leave a Reply

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