04 — Reading Input in C#
There are three common ways to consume actions. This kit uses the first; you should be able to recognize all three, because tutorials in the wild use all of them.
Way 1 — Direct asset reference (what this kit does)
A script holds a [SerializeField] InputActionAsset actions, finds its map and actions by name, and enables the map:
private InputAction moveAction, jumpAction;
private void OnEnable()
{
InputActionMap map = actions.FindActionMap("Player2D", throwIfNotFound: true);
moveAction = map.FindAction("Move");
jumpAction = map.FindAction("Jump");
jumpAction.performed += OnJump; // subscribe BEFORE enabling
map.Enable(); // nothing fires until the map is on
}
private void OnDisable()
{
jumpAction.performed -= OnJump; // mirror every += with a -=
actions.FindActionMap("Player2D").Disable();
}
Why teach this one first: everything is explicit — you can see exactly where the action comes from, when it's enabled, and what's subscribed. No hidden magic.
Way 2 — The PlayerInput component (great for prototypes)
Add a PlayerInput component, assign the actions asset, pick a Default Map, and choose a Behavior:
- Send Messages — calls methods named
OnMove,OnJump... on the same GameObject. Zero wiring, very magic. - Invoke Unity Events — hook methods in the Inspector like Button.onClick.
- Invoke C# Events — subscribe in code to
playerInput.onActionTriggered.
PlayerInput also powers device auto-switching and split-screen multiplayer (PlayerInputManager) for free. Downsides for teaching: typo'd method names fail silently, and the flow is invisible. Try it once as an exercise: build a cube that moves via Send Messages in ten minutes.
Way 3 — Generated C# class (type-safe, IDE-friendly)
Select the .inputactions asset → Inspector → Generate C# Class. You get compile-time-checked access:
var controls = new StarterKitControls();
controls.Player2D.Enable();
controls.Player2D.Jump.performed += OnJump;
Rename an action and the compiler finds every usage — wonderful for bigger projects. The kit skips it only to keep the import zero-config.
The event trio, precisely
For a default Button action: started = key touched down · performed = press counted (this is the one you usually want) · canceled = released. The CallbackContext tells you more:
private void OnJump(InputAction.CallbackContext ctx)
{
ctx.ReadValue<float>(); // value at the time of the event
ctx.control.device.name; // which device caused it
ctx.duration; // seconds since 'started'
}
Add an interaction (Hold, Tap, MultiTap — set on the action/binding in the Actions editor) and the same three events express charge shots, double taps, and long presses with no timer code in your script.
Polling helpers (no events at all)
moveAction.ReadValue<Vector2>(); // continuous value, every frame
jumpAction.WasPressedThisFrame(); // like the old GetKeyDown
jumpAction.WasReleasedThisFrame(); // ...GetKeyUp
jumpAction.IsPressed(); // ...GetKey
The FPS controller polls (WasPressedThisFrame in Update); the 2D controller subscribes to events. Both are correct — compare them side by side in lessons 05 - 2D Platformer Movement and 06 - First-Person and Weapon Switching, then pick per situation: events for "do X once when Y happens", polling for "read the current state of things".