# 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)]]).