Skip to main content

โŒจ๏ธ 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:

  1. 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.
  2. Generated C# class โ€” Unity generates a strongly-typed wrapper class from the asset. You new it up and subscribe to its events. Great for full control and no components.
  3. Direct InputAction references โ€” expose InputAction fields (or an InputActionReference), 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 PlayerControls asset 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, OnJump by 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:

The Player Input component in the Inspector A recreation of Unity's Inspector showing a Player Input component. Fields include Actions set to PlayerControls, Default Map set to Player, and Behavior set to Invoke Unity Events. Below, an Events foldout is expanded showing a Player group with Move (Callback) wired to PlayerController.OnMove and Jump (Callback) wired to PlayerController.OnJump. Inspector Player Input Actions PlayerControls (Input Action Asset) Default Map Player Behavior Invoke Unity Events Events Player Move (Callback) Runtime Only PlayerController.OnMove โ—ˆ PlayerController (player) Jump (Callback) Runtime Only PlayerController.OnJump โ—ˆ PlayerController (player) + +
Figure 1: The Player Input component with Behavior = Invoke Unity Events (faithfully recreated). Each action exposes a "(Callback)" event you drag your handler method onto โ€” here 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:

  1. Click + under, say, Move (Callback).
  2. Drag the GameObject holding your PlayerController script into the object slot.
  3. In the function dropdown choose PlayerController โ–ธ OnMove (a dynamic method that takes a CallbackContext).
  4. 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, performed is the press and canceled is the release. Convenience flags context.performed and context.canceled read cleanly in an if.
flowchart LR D["Device input
(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 in Update or FixedUpdate, where you control timing with Time.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.

  1. Create a capsule, add a CharacterController and the PlayerController script above.
  2. Add a Player Input component; set Actions = PlayerControls, Default Map = Player, Behavior = Invoke Unity Events.
  3. Wire Move (Callback) โ†’ OnMove and Jump (Callback) โ†’ OnJump (dynamic methods).
  4. 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 with context.ReadValue<Vector2>() and branch on context.performed / context.canceled.
  • For movement, store the vector in the callback and apply it in Update with Time.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.