โจ๏ธ Lesson 3.3: Reading Input in Code
You have a Player map with Move and Jump. Now the question every learner asks: how do I actually get those into a script? The Input System gives you three routes. We'll walk all three, explain the trade-offs, and settle on the one we recommend โ the PlayerInput component with Unity Events โ then move a real character with it.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Compare the three ways to read input: PlayerInput component, generated C# class, and direct InputAction references
- Add and configure a PlayerInput component and pick a Behavior
- Write callback methods that take an
InputAction.CallbackContext - Read a movement vector with
context.ReadValue<Vector2>() - Move a character with the new API, storing input in a callback and applying it in
Update
Estimated Time: 45 minutes ยท Prerequisite: Lesson 3.2 (the PlayerControls asset with Move and Jump)
In This Lesson
Three Ways to Read Input
All three routes read the same .inputactions asset โ they differ in how much you wire in the Inspector vs. in code:
- PlayerInput component โ drop a component on your player, point it at the asset, and it dispatches each action to your methods. Least code, great for a single player. This is our recommendation.
- Generated C# class โ Unity generates a strongly-typed wrapper class from the asset. You
newit up and subscribe to its events. Great for full control and no components. - Direct InputAction references โ expose
InputActionfields (or anInputActionReference), enable them, and read/subscribe manually. Most flexible, most boilerplate.
๐ Definition
PlayerInput: a built-in component that owns an .inputactions asset, enables an action map, and routes each action's events to your code โ via Unity Events, C# messages, or broadcast. It's the fastest way to get a responsive character on screen.
The PlayerInput Component
Select your player GameObject and click Add Component โธ Player Input (from the Input System package). It has a few key fields:
- Actions โ drag your
PlayerControlsasset here. - Default Map โ which map is active on start (choose
Player). - Behavior โ how it calls your code. This is the choice that matters.
The Behavior dropdown offers four styles:
- Invoke Unity Events โ exposes an event per action in the Inspector; you drag your method onto it. Explicit and easy to see. We use this.
- Send Messages โ calls methods named
OnMove,OnJumpby convention on the same GameObject. - Broadcast Messages โ like Send Messages but also to children.
- Invoke C# Events โ exposes C# events you subscribe to in code.
Here's the component set to Invoke Unity Events, with the Move and Jump events wired to a PlayerController script:
OnMove and OnJump on the PlayerController.Wiring the Inspector
With Behavior set to Invoke Unity Events, expand the Events foldout, then the Player group. You'll see one "(Callback)" event per action. To wire one:
- Click + under, say, Move (Callback).
- Drag the GameObject holding your
PlayerControllerscript into the object slot. - In the function dropdown choose PlayerController โธ OnMove (a dynamic method that takes a
CallbackContext). - Repeat for Jump (Callback) โ
OnJump.
โ ๏ธ Pick the dynamic method, not the static one
In the function dropdown, methods appear twice: under a Dynamic heading (they receive the live CallbackContext) and a Static heading (you'd type a fixed value). For input you almost always want the Dynamic OnMove(CallbackContext) โ otherwise your handler gets a frozen argument, not the real input.
Callbacks & CallbackContext
Each handler receives an InputAction.CallbackContext โ a little struct describing what just happened. The two things you'll use most:
context.ReadValue<T>()โ the current value, e.g.ReadValue<Vector2>()for Move.context.phaseโ where the action is in its lifecycle: started, performed, or canceled. For a Button,performedis the press andcanceledis the release. Convenience flagscontext.performedandcontext.canceledread cleanly in anif.
(WASD / stick / Space)"] --> M["Input Action
(Move / Jump)"] M --> PI["PlayerInput
component"] PI -- "Invoke Unity Event" --> H["Your handler
OnMove(context)"] H --> R["context.ReadValue<Vector2>()
context.performed / canceled"]
Figure 2: Input flows from device to action to PlayerInput to your callback, which reads the value and phase.
๐ก Store, don't act (for movement). A Move callback can fire several times a frame or not at all. Don't move the transform inside the callback. Instead store the latest Vector2 in a field and apply it inUpdateorFixedUpdate, where you control timing withTime.deltaTime.
Moving a Character
Here's a complete controller that reads Move and Jump via PlayerInput callbacks. Notice the pattern: the callbacks just capture input; Update and OnJump do the work.
using UnityEngine;
using UnityEngine.InputSystem; // gives us InputAction.CallbackContext
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
[SerializeField] float moveSpeed = 5f;
[SerializeField] float jumpSpeed = 6f;
[SerializeField] float gravity = -20f;
CharacterController controller;
Vector2 moveInput; // captured in OnMove, applied in Update
float verticalVelocity;
void Awake()
{
controller = GetComponent<CharacterController>();
}
// Wired to the Move (Callback) event on PlayerInput
public void OnMove(InputAction.CallbackContext context)
{
moveInput = context.ReadValue<Vector2>(); // (-1..1, -1..1)
}
// Wired to the Jump (Callback) event on PlayerInput
public void OnJump(InputAction.CallbackContext context)
{
// performed == the button went down this frame
if (context.performed && controller.isGrounded)
verticalVelocity = jumpSpeed;
}
void Update()
{
// map the 2D input onto the ground plane (x, z)
Vector3 horizontal = new Vector3(moveInput.x, 0f, moveInput.y);
if (controller.isGrounded && verticalVelocity < 0f)
verticalVelocity = -1f; // keep grounded
verticalVelocity += gravity * Time.deltaTime;
Vector3 velocity = horizontal * moveSpeed;
velocity.y = verticalVelocity;
controller.Move(velocity * Time.deltaTime);
}
}
Attach this to a GameObject with a CharacterController, add a PlayerInput component pointing at PlayerControls, and wire OnMove/OnJump as in Figure 1. Press Play: WASD (or a gamepad stick) moves the capsule, Space (or the gamepad button, once you add it next lesson) jumps.
โ Pro Tip
Because Move is device-agnostic, you didn't write a single line of gamepad-specific code โ the stick already feeds the same moveInput. That's the payoff for defining actions instead of polling keys.
The Other Two Approaches
PlayerInput is our default, but you should recognise the alternatives when you meet them in other projects.
Generated C# class
Select the .inputactions asset, tick Generate C# Class in the Inspector, and Apply. Unity writes a class (e.g. PlayerControls) you use directly โ no component needed:
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerControllerGenerated : MonoBehaviour
{
PlayerControls controls; // the generated class
Vector2 moveInput;
void Awake()
{
controls = new PlayerControls();
controls.Player.Move.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
controls.Player.Move.canceled += ctx => moveInput = Vector2.zero;
controls.Player.Jump.performed += ctx => Jump();
}
void OnEnable() => controls.Player.Enable();
void OnDisable() => controls.Player.Disable();
void Jump() { /* ... */ }
}
Great when you want strongly-typed access and no component, but you're responsible for enabling/disabling maps yourself.
Direct InputAction references
Expose an InputActionReference (or a serialized InputAction) and drive it manually:
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerControllerDirect : MonoBehaviour
{
[SerializeField] InputActionReference moveAction; // drag Move here
void OnEnable() => moveAction.action.Enable();
void OnDisable() => moveAction.action.Disable();
void Update()
{
Vector2 move = moveAction.action.ReadValue<Vector2>();
// ...apply move...
}
}
Most flexible and closest to the metal, but the most boilerplate โ you enable, read, and clean up everything by hand.
โ ๏ธ Enable your actions
Actions are disabled until something enables them. PlayerInput does this for you; with the generated class or direct references, forgetting Enable() (usually in OnEnable) is the #1 reason "nothing happens." Pair it with Disable() in OnDisable to avoid leaks.
Hands-on Challenge
๐๏ธ Exercise 1: Move a capsule with PlayerInput
Objective: Get a character moving with the recommended approach.
- Create a capsule, add a CharacterController and the
PlayerControllerscript above. - Add a Player Input component; set Actions =
PlayerControls, Default Map =Player, Behavior = Invoke Unity Events. - Wire Move (Callback) โ
OnMoveand Jump (Callback) โOnJump(dynamic methods). - Press Play and move with WASD; press Space to jump.
๐ก Hint: it compiles but nothing moves
Three usual suspects: (1) you wired the Static method instead of the Dynamic one, so context is empty; (2) Default Map isn't set to Player; (3) you didn't drag the PlayerControls asset into the Actions field. Also confirm the capsule's CharacterController isn't stuck inside the floor.
โ Success check
WASD glides the capsule around the ground plane and Space produces a single jump per press. Plugging in a gamepad and pushing the left stick moves it too โ with no extra code.
๐๏ธ Exercise 2: Log the phase
In OnJump, add Debug.Log(context.phase); as the first line. Watch the Console as you press and release Space: you'll see started, performed, and canceled roll by. This makes the callback lifecycle concrete โ and explains why you guard jump logic with if (context.performed).
๐ฏ Quick Quiz
Question 1: Which PlayerInput Behavior exposes a draggable event per action in the Inspector?
Question 2: How do you read the movement vector inside OnMove?
Question 3: Why store moveInput in a field and apply it in Update instead of moving inside the callback?
Summary
๐ Key Takeaways
- Three ways to read input: PlayerInput component (recommended), generated C# class, and direct InputAction references.
- Set PlayerInput's Behavior to Invoke Unity Events and wire each action's "(Callback)" event to a handler.
- Handlers take an
InputAction.CallbackContext; read withcontext.ReadValue<Vector2>()and branch oncontext.performed/context.canceled. - For movement, store the vector in the callback and apply it in
UpdatewithTime.deltaTime. - With the generated class or direct references, remember to
Enable()/Disable()your actions yourself.
๐ What's Next?
Your character moves on keyboard and gamepad already. In Lesson 3.4: Gamepad, Keyboard & Rebinding (the module mini-project) we add multiple bindings and Control Schemes, then build a settings menu that lets players remap Jump at runtime with PerformInteractiveRebinding().
โจ๏ธ Input reaches your code now
Actions in, callbacks out, a character moving with the modern API. From here it's all about making those controls flexible and player-friendly.