Input Setup
The foundation for all Code Bank scripts. Set this up once, and all other scaffolds will work with both gamepad and keyboard+mouse automatically.
Step 1: Create the Input Actions Asset
- In Unity: Assets → Create → Input Actions
- Name it
GameInputActions - Double-click to open the Input Actions editor
Step 2: Create Action Maps
We'll create two action maps: Gameplay (movement, actions) and UI (menus).
- Click + next to "Action Maps"
- Name it
Gameplay - Click + again, name it
UI
Step 3: Add Actions to Gameplay Map
Select the Gameplay action map, then add these actions:
| Action Name | Action Type | Control Type | Purpose |
|---|---|---|---|
Move |
Value | Vector2 | WASD / Left Stick movement |
Look |
Value | Vector2 | Mouse / Right Stick camera |
Jump |
Button | - | Space / South Button (A/X) |
Dash |
Button | - | Shift / Right Trigger |
Attack |
Button | - | Left Click / West Button (X/Square) |
Interact |
Button | - | E / East Button (B/Circle) |
Step 4: Add Bindings
For each action, add bindings for keyboard+mouse AND gamepad:
Move Action Bindings:
- Select
Moveaction - Click + → Add 2D Vector Composite → name it "WASD"
- Bind: Up=W, Down=S, Left=A, Right=D
- Click + → Add Binding → path:
Gamepad/leftStick
Look Action Bindings:
- Select
Lookaction - Click + → Add Binding → path:
Mouse/delta - Click + → Add Binding → path:
Gamepad/rightStick
Jump Action Bindings:
- Select
Jumpaction - Click + → Add Binding → path:
Keyboard/space - Click + → Add Binding → path:
Gamepad/buttonSouth
Dash Action Bindings:
- Select
Dashaction - Click + → Add Binding → path:
Keyboard/leftShift - Click + → Add Binding → path:
Gamepad/rightTrigger
Attack Action Bindings:
- Select
Attackaction - Click + → Add Binding → path:
Mouse/leftButton - Click + → Add Binding → path:
Gamepad/buttonWest
Interact Action Bindings:
- Select
Interactaction - Click + → Add Binding → path:
Keyboard/e - Click + → Add Binding → path:
Gamepad/buttonEast
Step 5: Save and Generate C# Class
- Click Save Asset in the Input Actions window
- Select your
GameInputActionsasset in the Project window - In the Inspector, check Generate C# Class
- Click Apply
Unity generates GameInputActions.cs - you won't edit this directly.
Step 6: Create the InputReader Script
This is the bridge between Unity's Input System and your gameplay scripts. All Code Bank scripts read from this.
Create a new C# script called InputReader.cs:
InputReader.cs
// ============================================================
// InputReader.cs
// Central input hub - all gameplay scripts read from this.
// Handles gamepad, keyboard, and mouse seamlessly.
// Unity 6.x + New Input System
// ============================================================
using UnityEngine;
using UnityEngine.InputSystem;
using System;
/// <summary>
/// Reads input from the New Input System and exposes it
/// in a clean, device-agnostic way. Attach to a GameObject
/// and reference from other scripts.
/// </summary>
public class InputReader : MonoBehaviour
{
// ========================================================
// SINGLETON (optional - convenient for prototyping)
// ========================================================
public static InputReader Instance { get; private set; }
[Header("Settings")]
[Tooltip("Use singleton pattern for easy global access")]
[SerializeField] private bool useSingleton = true;
[Tooltip("Mouse look sensitivity multiplier")]
[SerializeField] [Range(0.1f, 5f)] private float mouseSensitivity = 1f;
[Tooltip("Gamepad look sensitivity multiplier")]
[SerializeField] [Range(0.1f, 10f)] private float gamepadLookSensitivity = 3f;
// ========================================================
// INPUT VALUES - Read these from other scripts
// ========================================================
/// <summary>
/// Movement input as Vector2 (-1 to 1 on each axis)
/// WASD or Left Stick
/// </summary>
public Vector2 MoveInput { get; private set; }
/// <summary>
/// Look/camera input as Vector2
/// Mouse delta or Right Stick (sensitivity-adjusted)
/// </summary>
public Vector2 LookInput { get; private set; }
/// <summary>
/// True while jump button is held
/// </summary>
public bool JumpHeld { get; private set; }
/// <summary>
/// True while dash button is held
/// </summary>
public bool DashHeld { get; private set; }
/// <summary>
/// True while attack button is held
/// </summary>
public bool AttackHeld { get; private set; }
/// <summary>
/// True while interact button is held
/// </summary>
public bool InteractHeld { get; private set; }
// ========================================================
// EVENTS - Subscribe for button press/release
// ========================================================
/// <summary>Fired when jump is pressed</summary>
public event Action OnJumpPressed;
/// <summary>Fired when jump is released</summary>
public event Action OnJumpReleased;
/// <summary>Fired when dash is pressed</summary>
public event Action OnDashPressed;
/// <summary>Fired when dash is released</summary>
public event Action OnDashReleased;
/// <summary>Fired when attack is pressed</summary>
public event Action OnAttackPressed;
/// <summary>Fired when attack is released</summary>
public event Action OnAttackReleased;
/// <summary>Fired when interact is pressed</summary>
public event Action OnInteractPressed;
// ========================================================
// INTERNAL
// ========================================================
private GameInputActions inputActions;
private bool isGamepad = false;
// ========================================================
// UNITY LIFECYCLE
// ========================================================
void Awake()
{
// Singleton setup
if (useSingleton)
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
// Create input actions
inputActions = new GameInputActions();
}
void OnEnable()
{
inputActions.Gameplay.Enable();
// Subscribe to actions
inputActions.Gameplay.Move.performed += OnMove;
inputActions.Gameplay.Move.canceled += OnMove;
inputActions.Gameplay.Look.performed += OnLook;
inputActions.Gameplay.Look.canceled += OnLook;
inputActions.Gameplay.Jump.performed += OnJump;
inputActions.Gameplay.Jump.canceled += OnJumpCancel;
inputActions.Gameplay.Dash.performed += OnDash;
inputActions.Gameplay.Dash.canceled += OnDashCancel;
inputActions.Gameplay.Attack.performed += OnAttack;
inputActions.Gameplay.Attack.canceled += OnAttackCancel;
inputActions.Gameplay.Interact.performed += OnInteract;
// Track device changes
InputSystem.onActionChange += OnActionChange;
}
void OnDisable()
{
inputActions.Gameplay.Disable();
inputActions.Gameplay.Move.performed -= OnMove;
inputActions.Gameplay.Move.canceled -= OnMove;
inputActions.Gameplay.Look.performed -= OnLook;
inputActions.Gameplay.Look.canceled -= OnLook;
inputActions.Gameplay.Jump.performed -= OnJump;
inputActions.Gameplay.Jump.canceled -= OnJumpCancel;
inputActions.Gameplay.Dash.performed -= OnDash;
inputActions.Gameplay.Dash.canceled -= OnDashCancel;
inputActions.Gameplay.Attack.performed -= OnAttack;
inputActions.Gameplay.Attack.canceled -= OnAttackCancel;
inputActions.Gameplay.Interact.performed -= OnInteract;
InputSystem.onActionChange -= OnActionChange;
}
// ========================================================
// INPUT HANDLERS
// ========================================================
private void OnMove(InputAction.CallbackContext ctx)
{
MoveInput = ctx.ReadValue<Vector2>();
}
private void OnLook(InputAction.CallbackContext ctx)
{
Vector2 raw = ctx.ReadValue<Vector2>();
// Apply appropriate sensitivity based on device
float sensitivity = isGamepad ? gamepadLookSensitivity : mouseSensitivity;
LookInput = raw * sensitivity;
}
private void OnJump(InputAction.CallbackContext ctx)
{
JumpHeld = true;
OnJumpPressed?.Invoke();
}
private void OnJumpCancel(InputAction.CallbackContext ctx)
{
JumpHeld = false;
OnJumpReleased?.Invoke();
}
private void OnDash(InputAction.CallbackContext ctx)
{
DashHeld = true;
OnDashPressed?.Invoke();
}
private void OnDashCancel(InputAction.CallbackContext ctx)
{
DashHeld = false;
OnDashReleased?.Invoke();
}
private void OnAttack(InputAction.CallbackContext ctx)
{
AttackHeld = true;
OnAttackPressed?.Invoke();
}
private void OnAttackCancel(InputAction.CallbackContext ctx)
{
AttackHeld = false;
OnAttackReleased?.Invoke();
}
private void OnInteract(InputAction.CallbackContext ctx)
{
InteractHeld = true;
OnInteractPressed?.Invoke();
// Auto-release interact after one frame
Invoke(nameof(ReleaseInteract), 0.1f);
}
private void ReleaseInteract()
{
InteractHeld = false;
}
// ========================================================
// DEVICE DETECTION
// ========================================================
private void OnActionChange(object obj, InputActionChange change)
{
if (change == InputActionChange.ActionPerformed)
{
var action = obj as InputAction;
if (action?.activeControl?.device != null)
{
isGamepad = action.activeControl.device is Gamepad;
}
}
}
/// <summary>
/// Returns true if current input device is a gamepad
/// </summary>
public bool IsUsingGamepad => isGamepad;
// ========================================================
// PUBLIC METHODS
// ========================================================
/// <summary>
/// Enable gameplay input (call when unpausing)
/// </summary>
public void EnableGameplayInput()
{
inputActions.Gameplay.Enable();
}
/// <summary>
/// Disable gameplay input (call when pausing/in menus)
/// </summary>
public void DisableGameplayInput()
{
inputActions.Gameplay.Disable();
MoveInput = Vector2.zero;
LookInput = Vector2.zero;
}
}
// ============================================================
// USAGE:
//
// 1. Create empty GameObject, add InputReader component
// 2. Other scripts read input like this:
//
// // Option A: Using singleton (quick prototyping)
// Vector2 move = InputReader.Instance.MoveInput;
// if (InputReader.Instance.JumpHeld) { ... }
//
// // Option B: Using reference (better for production)
// [SerializeField] private InputReader input;
// Vector2 move = input.MoveInput;
//
// // Option C: Subscribe to events
// void OnEnable() {
// InputReader.Instance.OnJumpPressed += HandleJump;
// }
// void OnDisable() {
// InputReader.Instance.OnJumpPressed -= HandleJump;
// }
//
// 3. Works automatically with both keyboard+mouse and gamepad!
// ============================================================
Step 7: Add InputReader to Your Scene
- Create empty GameObject: GameObject → Create Empty
- Name it
InputReader - Add the
InputReadercomponent - It will persist across scenes if
useSingletonis checked
InputReader.Instance and will automatically work with gamepad or keyboard+mouse.
Quick Reference: Reading Input
Quick Reference
// Movement (WASD or Left Stick)
Vector2 moveInput = InputReader.Instance.MoveInput;
float horizontal = moveInput.x; // -1 to 1
float vertical = moveInput.y; // -1 to 1
// Camera (Mouse or Right Stick)
Vector2 lookInput = InputReader.Instance.LookInput;
// Buttons - check if held
if (InputReader.Instance.JumpHeld) { }
if (InputReader.Instance.DashHeld) { }
if (InputReader.Instance.AttackHeld) { }
// Buttons - subscribe to press events
InputReader.Instance.OnJumpPressed += MyJumpHandler;
InputReader.Instance.OnJumpReleased += MyJumpReleaseHandler;
Perspective-Specific Notes
2D Platformer
- Use
MoveInput.xfor horizontal movement - Ignore
MoveInput.y(or use for ladder climbing) LookInputtypically unused
3D Third-Person
- Use
MoveInputrelative to camera forward - Use
LookInputto orbit camera around player
3D First-Person
- Use
MoveInputrelative to player forward - Use
LookInputto rotate player view directly - Consider locking and hiding cursor
Isometric / Top-Down
- Use
MoveInputdirectly as world-space direction LookInputcan aim independently of movement
Why This Architecture
The InputReader pattern serves several pedagogical goals:
- Device-agnostic: Students don't write separate gamepad/keyboard code
- Single source of truth: All input flows through one place
- Event-driven option: Supports both polling and event patterns
- Easily mockable: Can simulate input for testing
Common Student Issues
"My input isn't working"
- Check InputReader exists in scene
- Check GameInputActions asset has bindings saved
- Check "Generate C# Class" is enabled and applied
- Check Gameplay action map is enabled
"Gamepad works but keyboard doesn't (or vice versa)"
Check bindings in the Input Actions asset. Each action needs BOTH keyboard and gamepad bindings.
Extending for New Actions
To add a new action (e.g., "Crouch"):
- Add action to GameInputActions asset
- Add bindings for both keyboard and gamepad
- Save and Apply (regenerates C# class)
- Add property and handler in InputReader.cs
Input Abstraction Philosophy
The gap between "player intent" and "hardware signal" is larger than it seems:
- Hardware: "Left stick at (0.7, 0.3)"
- Intent: "Move forward-right at moderate speed"
Good input architecture bridges this gap cleanly. The InputReader transforms hardware signals into semantic intent that gameplay code can understand without caring about the source device.
Why Events AND Polling
Different systems need input differently:
- Polling (reading MoveInput each frame): Movement, aiming - continuous actions
- Events (OnJumpPressed): Jump, attack - discrete actions with timing that matters
Offering both lets each system use the appropriate pattern.
The Sensitivity Problem
Mouse delta and stick position are fundamentally different:
- Mouse: "Moved 50 pixels since last frame" (velocity)
- Stick: "Currently at 0.8 right" (position)
The sensitivity multipliers help normalize these, but perfect parity is impossible. This is why many videogames have separate mouse and gamepad sensitivity settings - and why we expose both in the InputReader.
Related
- Basic Jump - uses this input setup
- Dash - uses this input setup
- Code Bank Overview