# 5. Worked Example Remappable Player
Part of [[Input System]]. Previous: [[4. UI Input and XR Input]]. Next: [[6. Pitfalls]].
Builds a desktop player with **Move / Look / Jump / Interact**, control schemes, and saveable rebinds. Compatible in spirit with [[Surface-Walking with an Omnidirectional Player Movement]] and the lighter [[Unity New Input System Upgrade Tutorial]].
> [!warning] Verification status
> Asset layout follows 1.20 docs. All C# below was **not** compiled in Unity. Generated class names assume an asset `PlayerInputActions` with map `Player` and actions Move, Look, Jump, Interact — regenerate if your names differ. Mouse look sensitivity via Scale processor is conceptual (set in the Editor on the mouse binding).
## 5.1 Create the asset
1. `Create > Input Actions` → `PlayerInputActions`.
2. Control schemes: **Keyboard&Mouse**, **Gamepad**.
3. Map **Player** with:
| Action | Type | Control Type | Bindings |
|---|---|---|---|
| Move | Value | Vector2 | 2D Vector composite WASD + Arrows (Keyboard&Mouse); `<Gamepad>/leftStick` (Gamepad) + Stick Deadzone |
| Look | Value | Vector2 | `<Mouse>/delta` with **Scale Vector 2** (Keyboard&Mouse); `<Gamepad>/rightStick` (Gamepad) |
| Jump | Button | Button | `<Keyboard>/space`; `<Gamepad>/buttonSouth` |
| Interact | Button | Button | `<Keyboard>/e`; `<Gamepad>/buttonWest` |
4. Optional second map **UI** (or rely on project-wide UI).
5. Enable **Generate C# Class**, Apply, save.
## 5.2 Player motor (consumes move/look, reacts to jump/interact)
```csharp
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public sealed class RemappablePlayerMotor : MonoBehaviour
{
[SerializeField] float moveSpeed = 6f;
[SerializeField] float lookSensitivity = 0.12f;
[SerializeField] float jumpHeight = 1.2f;
[SerializeField] float gravity = -20f;
[SerializeField] Transform cameraPivot;
CharacterController _cc;
Vector2 _move;
Vector2 _look;
float _verticalVel;
float _pitch;
void Awake()
{
_cc = GetComponent<CharacterController>();
if (cameraPivot == null)
cameraPivot = transform;
}
public void SetMove(Vector2 value) => _move = value;
public void SetLook(Vector2 value) => _look = value;
public void Jump()
{
if (_cc.isGrounded)
_verticalVel = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
public void Interact()
{
// Replace with raycast / IInteractable. For teaching, log is enough.
Debug.Log("Interact", this);
}
void Update()
{
// Look
transform.Rotate(0f, _look.x * lookSensitivity, 0f);
_pitch = Mathf.Clamp(_pitch - _look.y * lookSensitivity, -89f, 89f);
cameraPivot.localRotation = Quaternion.Euler(_pitch, 0f, 0f);
// Move
Vector3 wish = transform.right * _move.x + transform.forward * _move.y;
if (wish.sqrMagnitude > 1f)
wish.Normalize();
if (_cc.isGrounded && _verticalVel < 0f)
_verticalVel = -2f;
_verticalVel += gravity * Time.deltaTime;
Vector3 velocity = wish * moveSpeed + Vector3.up * _verticalVel;
_cc.Move(velocity * Time.deltaTime);
}
}
```
## 5.3 Input director (generated class + prefs)
```csharp
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// Owns PlayerInputActions lifetime, loads binding overrides, forwards to the motor.
/// </summary>
public sealed class RemappablePlayerInputDirector : MonoBehaviour, PlayerInputActions.IPlayerActions
{
[SerializeField] RemappablePlayerMotor motor;
[SerializeField] bool lockCursor = true;
PlayerInputActions _actions;
void Awake()
{
_actions = new PlayerInputActions();
BindingOverridesStore.Load(_actions.asset);
_actions.Player.AddCallbacks(this);
}
void OnEnable()
{
_actions.Player.Enable();
if (lockCursor)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
void OnDisable()
{
_actions.Player.Disable();
}
void OnDestroy()
{
_actions?.Dispose();
}
public void OnMove(InputAction.CallbackContext context) => motor.SetMove(context.ReadValue<Vector2>());
public void OnLook(InputAction.CallbackContext context) => motor.SetLook(context.ReadValue<Vector2>());
public void OnJump(InputAction.CallbackContext context)
{
if (context.performed)
motor.Jump();
}
public void OnInteract(InputAction.CallbackContext context)
{
if (context.performed)
motor.Interact();
}
// Optional: expose for a settings screen
public InputActionAsset Asset => _actions.asset;
public InputAction FindPlayerAction(string name) => _actions.asset.FindAction("Player/" + name, true);
}
```
Reuse `BindingOverridesStore` from [[3. Runtime Rebinding]] (same script inline there). If Obsidian publishing splits code across notes, copy the static class into this assembly or a shared `Input` folder.
## 5.4 Minimal rebind button (uGUI)
```csharp
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public sealed class RebindButtonUI : MonoBehaviour
{
[SerializeField] RemappablePlayerInputDirector director;
[SerializeField] string actionName = "Jump";
[SerializeField] string scheme = "Keyboard&Mouse";
[SerializeField] TMP_Text label;
[SerializeField] Button button;
InputActionRebindingExtensions.RebindingOperation _op;
void OnEnable()
{
button.onClick.AddListener(StartRebind);
RefreshLabel();
}
void OnDisable()
{
button.onClick.RemoveListener(StartRebind);
_op?.Dispose();
_op = null;
}
void RefreshLabel()
{
var action = director.FindPlayerAction(actionName);
int index = BindingDisplay.FindBindingIndex(action, scheme);
label.text = index >= 0
?
quot;{actionName}: {action.GetBindingDisplayString(index)}"
: quot;{actionName}: (none)";
}
void StartRebind()
{
var action = director.FindPlayerAction(actionName);
int index = BindingDisplay.FindBindingIndex(action, scheme);
if (index < 0)
return;
action.Disable();
label.text = quot;{actionName}: …";
_op?.Dispose();
_op = action.PerformInteractiveRebinding(index)
.WithControlsExcluding("<Mouse>/position")
.WithControlsExcluding("<Mouse>/delta")
.WithCancelingThrough("<Keyboard>/escape")
.OnComplete(op =>
{
BindingOverridesStore.Save(director.Asset);
action.Enable();
RefreshLabel();
op.Dispose();
_op = null;
})
.OnCancel(op =>
{
action.Enable();
RefreshLabel();
op.Dispose();
_op = null;
})
.Start();
}
}
```
`BindingDisplay` is defined in part 3.
## 5.5 Scene setup steps
1. Plane + Player capsule with `CharacterController`, `RemappablePlayerMotor`, `RemappablePlayerInputDirector`.
2. Child camera on `cameraPivot`.
3. EventSystem with `InputSystemUIInputModule` for the rebind canvas.
4. Canvas with one `RebindButtonUI` per action/scheme pair you care about (Jump Keyboard, Jump Gamepad, …).
5. Enter Play Mode: WASD moves, mouse looks, Space jumps, E logs Interact. Click Jump rebind, press F, confirm label + prefs survive domain reload **only if** you reload prefs in `Awake` (device state still resets — part 6).
## 5.6 Extension ideas
- Add a **Vehicle** map and swap like [[Car Controller]] entry/exit.
- For surface walking, replace the motor with the upgraded walker from [[Unity New Input System Upgrade Tutorial]].
- For Netcode, read input on the owner client only and send RPCs — see [[Multiplayer with Netcode for GameObjects]]; never let every client enable the same keyboard actions into a shared sim without ownership checks.
- Treat Jump/Interact as **Command** objects if you need replay/undo ([[Game Programming Patterns (Review)]]).