06 — First-Person and Weapon Switching
Run Tools → Input System Starter Kit → Create First-Person Demo in an empty scene. WASD to move, mouse to look, Shift to sprint, Space to jump. Click to fire — you paint targets with the current weapon's color. Scroll the wheel or press 1/2/3 to switch between the three primitive "weapons". All of it works on a gamepad too. Two scripts: FirstPersonController.cs and WeaponSwitcher.cs.
Movement and look — FirstPersonController.cs
- All four actions are polled — this controller is the "Way 3 polling" counterpart to the platformer's events (see 04 - Reading Input in C Sharp).
jumpAction.WasPressedThisFrame()is the newGetKeyDown. - The one device branch in the whole kit: mouse
deltaarrives as pixels-per-frame; a stick arrives as a constant -1..1. SameLookaction, but sensitivity must differ:
bool fromStick = lookAction.activeControl?.device is Gamepad;
Vector2 delta = fromStick
? look * (stickSensitivity * Time.deltaTime) // degrees per second
: look * mouseSensitivity; // per-pixel
activeControl tells you which physical control most recently drove the action — the clean way to ask "keyboard or gamepad?" without maintaining device state.
- Body/camera split: the body yaws left-right, the camera pivot pitches up-down (clamped ±85°). One transform doing both causes roll drift — a classic FPS bug.
Cursor.lockState = CursorLockMode.LockedinOnEnable, unlocked inOnDisable. Esc frees the mouse in the editor (Unity handles that automatically in Play mode).Sprintis a Button action read as a float —ReadValue<float>() > 0.5fis the cleanest "is held" check for buttons on both keyboard and stick-click.
Weapon switching — WeaponSwitcher.cs
Five actions, three techniques:
NextWeapon/PreviousWeapon— bound to<Mouse>/scroll/upand/down(sub-controls of the scroll vector used as buttons) plus the shoulder buttons. The handlers just move an index with wraparound arithmetic.WeaponSlot1/2/3— instead of three copy-pasted handlers, a loop subscribes a lambda that captures its slot index:
for (int i = 0; i < 3; i++)
{
int slot = i; // capture a COPY — capturing 'i' directly is the classic C# trap
map.FindAction($"WeaponSlot{i + 1}").performed += _ => Equip(slot);
}
The comment in OnDisable about not being able to unsubscribe those lambdas is deliberate — have students explain why (the delegate was never stored), then fix it by storing the delegates in an array. Ready-made discussion question.
Fire— aperformedhandler raycasts from the camera center (Physics.Raycast) and recolors what it hits. Hitscan in ten lines; swapping in a projectile prefab is the natural extension.
Exercises
- Add hold-to-charge: put a Hold interaction (0.5 s) on
Fire;performednow means "charged" andcanceledbefore that means "tap shot". No timers in code. - Add an
Interactaction (E / buttonWest) that raycasts 2 m and logs what you're looking at — doors and pickups start here. - Give each weapon a different fire behavior (the Zapper paints random colors; the Orb scales targets down). All the input plumbing is already there.
Deeper reference: FPS Controller with the Input System and samyam's Cinemachine first-person variant.