Skip to main content

🎮 Lesson 3.4: Gamepad, Keyboard & Rebinding (Mini-Project)

Time to tie Module 3 together. First we give a single action bindings for both keyboard and gamepad — and see how Control Schemes keep them organised. Then the star of the show: a settings-menu button that lets the player rebind Jump at runtime using PerformInteractiveRebinding(), updating the label to show the new key. This is the feature that separates a hobby project from a shippable one.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Add multiple bindings to one action so keyboard and gamepad both work
  • Create Control Schemes and understand device requirements
  • Turn a binding into a readable label with GetBindingDisplayString()
  • Rebind an action at runtime with PerformInteractiveRebinding()
  • Build a "Rebind Jump" menu button that captures the next press and shows the new binding

Estimated Time: 50 minutes  ·  Prerequisite: Lessons 3.2–3.3 (PlayerControls asset and a PlayerInput-driven character)

In This Lesson

Multiple Bindings per Action

Back in the Input Actions editor, an action can hold as many bindings as you like. To make Jump work on a controller too, select Jump, click + ▸ Add Binding, and set the new binding's Path to the gamepad's south button (buttonSouth [Gamepad] — the A/Cross button). Now Jump has two bindings:

  • Space [Keyboard]
  • Button South [Gamepad]

You wrote zero code for this. Whichever device the player uses, the same Jump action fires and your existing OnJump handler runs. Do the same for Move by adding a Left Stick [Gamepad] binding alongside the WASD composite (if you didn't in Lesson 3.2).

✅ Pro Tip

The Input System auto-detects devices. Start on keyboard, pick up a controller mid-session, and PlayerInput switches the active device automatically — great for couch play where people grab whatever's nearby.

Control Schemes

A Control Scheme is a named set of device requirements — for example "Keyboard&Mouse" or "Gamepad". Schemes let the system decide which bindings belong to which device group, which matters for device switching and for local multiplayer (so Player 1's keyboard and Player 2's gamepad don't collide).

📖 Definition

Control Scheme: a label plus a list of required/optional devices (e.g. Gamepad required). Each binding can be tagged with the schemes it belongs to, so the system knows "this binding is for the Gamepad scheme."

You manage schemes from the dropdown in the top-left of the Input Actions editor (it reads "All Control Schemes" by default). Choose Add Control Scheme…, name it, and add device requirements. Here's the editor showing two schemes and Jump's binding path being set to the gamepad button:

Control Schemes and a binding path in the Input Actions editor A recreation of the Input Actions editor. The top-left Control Scheme dropdown is open, listing Keyboard and Mouse, and Gamepad, plus Add Control Scheme. On the right a binding Properties panel shows a Path field set to Button South [Gamepad] and Use in control scheme checkboxes with Gamepad ticked and Keyboard and Mouse unticked. PlayerControls All Control Schemes Keyboard&Mouse Gamepad + Add Control Scheme… Edit Control Scheme… Binding Path Button South [Gamepad] Listen Use in control scheme Keyboard&Mouse Gamepad
Figure 1: Control Schemes and a binding's device path (faithfully recreated). The gamepad binding is tagged for the Gamepad scheme only; the keyboard binding lives in Keyboard&Mouse.

Showing the Current Binding

A remap menu needs to show the player what a control is currently bound to — "Jump: Space". The Input System turns a binding into a friendly label for you with GetBindingDisplayString():

// jumpAction is an InputAction (e.g. from PlayerInput or a reference)
string label = jumpAction.GetBindingDisplayString();   // "Space" or "A" on a pad

If an action has several bindings, pass the binding index to target a specific one, e.g. GetBindingDisplayString(0) for the keyboard binding. The string is human-readable and localises device names (the gamepad south button reads "A" on Xbox layouts, "Cross" on PlayStation).

✅ Pro Tip

Call GetBindingDisplayString() again right after any rebind completes and push the result into your UI Text. That single line keeps the label always truthful about what pressing the control will do.

The Rebinding Flow

Interactive rebinding means: pause the action, wait for the player to press something, capture that control, and write it into the binding. The Input System hands you an operation object that runs this whole dance. Here's the shape of it:

flowchart TB A["Player clicks
'Rebind Jump'"] --> B["Disable the action
(can't rebind while active)"] B --> C["Start PerformInteractiveRebinding()
UI shows 'Press a key…'"] C --> D{"Player presses
a control"} D --> E["OnComplete:
binding overridden"] E --> F["Dispose the operation
+ re-enable the action"] F --> G["Update label via
GetBindingDisplayString()"]

Figure 2: The interactive rebinding lifecycle — disable, listen, capture, clean up, refresh the label.

⚠️ Disable the action first, and always Dispose

You must disable the action before rebinding it — the operation throws if the action is still enabled. And the rebind operation allocates unmanaged resources, so you must call .Dispose() in the completion callback (and if you cancel). Forgetting either causes errors or leaks.

Interactive Rebinding in Code

Here is a focused, reusable rebinder. Give it the action and a UI label; call StartRebind() from a button:

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;                 // or TMPro for TextMeshPro
using TMPro;

public class JumpRebinder : MonoBehaviour
{
    [SerializeField] InputActionReference jumpAction;  // drag the Jump action
    [SerializeField] TMP_Text bindingLabel;            // "Jump: Space"
    [SerializeField] TMP_Text promptLabel;             // "Press any key…"

    InputActionRebindingExtensions.RebindingOperation rebindOp;

    void Start() => RefreshLabel();

    // Wire this to your Rebind button's OnClick
    public void StartRebind()
    {
        InputAction action = jumpAction.action;
        action.Disable();                              // required before rebinding

        promptLabel.gameObject.SetActive(true);
        promptLabel.text = "Press any key…";

        rebindOp = action.PerformInteractiveRebinding()
            .WithControlsExcluding("<Mouse>/position") // ignore mouse move noise
            .OnMatchWaitForAnother(0.1f)               // debounce
            .OnComplete(op => RebindComplete())
            .OnCancel(op => RebindComplete())
            .Start();
    }

    void RebindComplete()
    {
        rebindOp.Dispose();                            // free the operation
        rebindOp = null;
        jumpAction.action.Enable();                    // re-enable gameplay input
        promptLabel.gameObject.SetActive(false);
        RefreshLabel();
    }

    void RefreshLabel()
    {
        bindingLabel.text = "Jump: " +
            jumpAction.action.GetBindingDisplayString();
    }
}

Read it against Figure 2: Disable(), then PerformInteractiveRebinding() starts listening, OnComplete fires when the player presses a control, and RebindComplete disposes, re-enables, and refreshes the label. The .WithControlsExcluding and .OnMatchWaitForAnother calls are small quality-of-life touches so a stray mouse wiggle doesn't get captured.

⚠️ Rebinds are runtime overrides, not saved edits

PerformInteractiveRebinding() writes a binding override that lives only for this session. To persist it, save action.actionMap.asset.SaveBindingOverridesAsJson() to PlayerPrefs and reload it on startup with LoadBindingOverridesFromJson(). We build exactly that kind of persistence in Module 5.

Mini-Project: A Remap Menu

Let's assemble the deliverable: a tiny settings panel with a live label and a Rebind button that remaps Jump to whatever the player presses next.

🎯 Build the remap menu

  1. In your scene from Lesson 3.3, add a UI Canvas (Screen Space – Overlay).
  2. Add a Panel with two TextMeshPro texts: bindingLabel ("Jump: Space") and a hidden promptLabel ("Press any key…").
  3. Add a Button labelled "Rebind Jump".
  4. Create the JumpRebinder script above; put it on the panel. Drag the Jump action into jumpAction (create an InputActionReference by expanding the PlayerControls asset in the Project window and dragging the Jump sub-asset), and assign the two text fields.
  5. Wire the button's OnClickJumpRebinder.StartRebind.
  6. Press Play. Confirm the label reads "Jump: Space". Click Rebind Jump, press J, and watch the label change to "Jump: J". Now the J key jumps and Space no longer does.
💡 Hint: the label doesn't change / it captures instantly

If it "captures" the moment you click, your mouse click is being read — add .WithControlsExcluding("<Mouse>"). If nothing changes, check you disabled the action before starting (an enabled action throws), and that RefreshLabel runs inside OnComplete. Also confirm jumpAction points at the Jump action, not the whole asset.

✅ Success check

The panel shows the current Jump binding. Clicking Rebind shows "Press any key…", the next key you press becomes the new Jump control, the prompt hides, and the label updates to match. Pressing that new key actually jumps the character; the old one doesn't.

Extend It

🏋️ Exercise 1: A Reset button

Add a second button, "Reset to Default", wired to a method that calls jumpAction.action.RemoveAllBindingOverrides(); then RefreshLabel();. Confirm it snaps Jump back to Space. This is the other half every real settings menu ships with.

🏋️ Exercise 2: Rebind Move too (stretch)

Generalise JumpRebinder into a RebindButton that takes an InputActionReference and a binding index, so you can drop one on every control row. Rebinding the WASD composite means targeting a specific child binding index (e.g. index 1 for "Up") — use PerformInteractiveRebinding(bindingIndex). This turns your demo into a full controls screen.

🎯 Quick Quiz

Question 1: To make Jump work on a gamepad as well as the keyboard, you…

Question 2: Which method starts capturing the player's next input for a rebind?

Question 3: Two things you must do around an interactive rebind are…

Summary

🎉 Key Takeaways

  • One action can hold multiple bindings — add a gamepad path next to the keyboard one and both work, no code.
  • Control Schemes group device requirements (Keyboard&Mouse, Gamepad) so the system knows which bindings belong to which devices.
  • GetBindingDisplayString() turns a binding into a player-friendly label for your UI.
  • PerformInteractiveRebinding() captures the next input; you must disable the action first and Dispose the operation on complete.
  • Rebinds are session overrides — persist them with SaveBindingOverridesAsJson() / LoadBindingOverridesFromJson().
  • You built a working runtime remap menu — the hallmark of a shippable control system.

🚀 What's Next?

Module 3 is complete: you retired the legacy Input class, defined actions and maps, read them in code, and gave players full control over their bindings. Next we shift to what wraps around all of it — the interface. In Lesson 4.1: Canvas Render Modes & Scaling we start Module 4 by making UI that looks right on every screen size.

🎮 You built input like a shipped game

Device-agnostic controls, gamepad and keyboard together, and a real rebinding menu. Your players can now make the controls their own.