---
title: "First-person camera movement and mouselook controls"
description: "The player sees through their own eyes, turns with the mouse, and walks with the keyboard. The engine has no `FirstPersonController` atomic (it was stripped, see component-types.md Recently Stripped Pattern Components). You assemble the feel from the existing components and native input dispatch."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/knowledge/playbooks/first-person-controls/
---

# First-person camera movement and mouselook controls

## When to use

The player sees through their own eyes, turns with the mouse, and walks with the
keyboard. The engine has no `FirstPersonController` atomic (it was stripped, see
component-types.md Recently Stripped Pattern Components). You assemble the
feel from the existing components and native input dispatch.

## The recipe

1. Create the player entity with `project_create_entity`
   (`project.entity.create`).
2. Attach `TransformComponent` with `project_add_component`
   (`project.entity.component.set`). Every component value MUST carry its
   discriminator: `{ "type": "TransformComponent", "position": {...} }`.
3. Attach `CameraComponent` on the SAME entity:
   `{ "type": "CameraComponent", "fov": 90, "projection": "perspective",
   "active": true, "priority": 20, "clearMode": "skybox" }`. The highest-priority
   active camera wins in Play. Leave `followEntityId`/`lookAtEntityId` unset for
   first person (the camera rides the player transform).
4. Attach `CharacterMovementComponent`:
   `{ "type": "CharacterMovementComponent", "enableKcc": true, "maxSpeed": 6,
   "gravity": 20, "jumpSpeed": 7 }`. `enableKcc: true` opts the
   entity into the predicted kinematic controller.
5. Attach a `ColliderComponent` so the mover is blocked by geometry:
   `{ "type": "ColliderComponent", "shape": "capsule", "collision": "solid",
   "size": {"x":0.5,"y":1.8,"z":0.5}, "layers":["default"], "friction":0.5,
   "restitution":0 }`.
6. Attach an `InputProfileComponent` with a look action of `valueType:
   "vector2d"`, `policy.effectSystem: "look"`, and a `policy.look`
   `{ sensitivityDeg, invertY, pitchMinDeg, pitchMaxDeg }` block (bounds
   -89..89). Set `prediction.mode: "client_only"` and bind
   `sources: [{ "device": "mouse", "control": "move", "event": "move" }]`
   with `triggers: [{ "kind": "continuous_delta", "parameters": {} }]`.
   This follows `firstPersonControllerBaseActions` in `packages/ecs/src/index.ts`.
   Keep the default `player.move`
   action for WASD (see input-mapping-actions-bindings for the full shape).
7. Verify mouse look through the native input dispatcher. Its client-only look
   branch calls `stream.applyLookInput` in
   `web_client/spa/src/runtime/inputDispatcher/dispatcher.ts`. Do not add a script
   that applies the same look input again. Author scripts only for additional
   game behavior; ordinary movement and look do not require them.

## Pitfalls

- Component values are discriminated unions: omit `type` and the write is
  rejected `component_state_invalid`. This is the TransformComponent lesson and
  applies to every component.
- `CharacterMovementComponent` with `enableKcc: false` (the default) will not
  move under prediction; set it true for a player.
- Camera `fov` is clamped 1..170 and `projection` must be `perspective` for FPS.
- Pointer lock needs a user gesture (`platform.v1` guard `platform.user_gesture`);
  request it from a click handler, not on spawn.
- Do not look for a `PlayerControllerComponent` or `CameraTargetComponent`; both
  are stripped patterns and no longer exist.

## Verify

- `engine_get_component_schema` (`engine.component.schema.read`) on
  `CameraComponent` and `InputProfileComponent` to confirm field names before
  writing.
- `project_get_graph_snapshot` (`project.graph.snapshot.read`) to confirm the
  player entity carries Camera, CharacterMovement, Collider and Input.
- `simulation_run` (`qa.run.start`) to prove movement responds, then
  `qa_capture_renderer_viewport` to confirm the first-person view renders.
