🎮 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:
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:
'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
- In your scene from Lesson 3.3, add a UI Canvas (Screen Space – Overlay).
- Add a Panel with two TextMeshPro texts:
bindingLabel("Jump: Space") and a hiddenpromptLabel("Press any key…"). - Add a Button labelled "Rebind Jump".
- Create the
JumpRebinderscript above; put it on the panel. Drag the Jump action intojumpAction(create anInputActionReferenceby expanding the PlayerControls asset in the Project window and dragging the Jump sub-asset), and assign the two text fields. - Wire the button's OnClick →
JumpRebinder.StartRebind. - Press Play. Confirm the label reads "Jump: Space". Click Rebind Jump, press
J, and watch the label change to "Jump: J". Now theJkey 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.