9.5 KiB
E1.M2 — IsometricCameraController
Summary
| Field | Value |
|---|---|
| Module ID | E1.M2 |
| Epic | Epic 1 — Core Player Runtime |
| Stage target | Prototype |
| Status | In progress — follow + CameraState + discrete zoom bands + occlusion policy shipped (NEON-25–NEON-27); hardening open (NEON-28); see dependency register |
| Jira | Feature NEON-10; prototype backlog stories under parent NEON-1 — Jira backlog |
Purpose
Authority: Client vs server — camera is client-local for prototype; server does not use camera pose for gameplay checks.
Delivers an isometric follow camera that keeps the player readable during motion: follow behavior, discrete zoom bands, occlusion handling, and yaw held at a fixed default for players in early prototypes so combat telegraphs and UI stay easy to reason about. After release, the shipped camera model is settled (fixed presentation vs product setting that allows yaw)—we do not plan to revisit that post-launch. Before release, we may still decide to expose yaw orbit for comfort; the implementation follows the mid-project rotation policy so that decision stays mid-project scope, not a rewrite.
Responsibilities
- Locked isometric follow for player-facing controls in prototype: yaw = 0 (no orbit bound in UI) while the rig and
CameraStatestill model yaw explicitly so optional orbit can be enabled later via config and input binding. - Zoom bands and configuration-driven limits.
- Occlusion policy so critical gameplay elements remain visible where possible.
- Pitch / roll remain fixed for the isometric presentation; only yaw is the optional future degree of freedom.
Mid-project rotation policy (practical compromise)
Intent: Ship prototypes and early milestones as if the camera were fully fixed from the player’s perspective, without closing the door to yaw orbit before release.
| Topic | Direction |
|---|---|
| When this applies | Mid-project only. We may enable or keep disabled yaw orbit while still in development. Post-release, treat the chosen shipped behavior as final—no planned pivot between “fixed” and “orbit” for live players. |
| Implementation | Single seam: e.g. allow_yaw, max_yaw_deg, or equivalent; default keeps yaw at 0 and no rotate input until product + combat readability agree. CameraState carries yaw (default 0) so consumers do not assume “camera yaw does not exist.” |
| Gameplay semantics | Prefer world-anchored movement, targeting, and server checks—not camera-relative aim—so turning yaw on later is mostly presentation + UI, not a combat authority redesign. |
| Telegraphs & VFX | Design now for a world where yaw might exist: ground shapes remain world-accurate; plan screen-space or height readability affordances where foreshortening under yaw would otherwise lie (outlines, rings, bars—exact art TBD). |
| Migration risk | If we add yaw mid-project, expect a telegraph + minimap / compass pass. Fixed → yaw is usually lower risk than yaw → fixed (content and players may already assume spin for sightlines). |
Key contracts
| Contract | Role |
|---|---|
CameraState |
Current follow target, zoom level, yaw (default 0; optional future orbit), and policy flags for consumers (e.g. risk UX). |
ZoomBandConfig |
Data-driven min/max or discrete zoom steps. |
OcclusionPolicy |
Rules for fading, dithering, or offset when geometry blocks the view. |
Module dependencies
- E1.M1 — InputAndMovementRuntime: camera follows the player anchor derived from movement/position.
Dependents (by design)
- E6.M2 — Consent and risk UX may use camera-adjacent presentation; epic lists E1.M2 as a dependency for in-zone risk signaling and readability.
Related implementation slices
See Epic 1 Slice 2 — Locked isometric camera: follow-center, zoom bands, occlusion policy, yaw fixed for players in prototype (implementation still models yaw per mid-project rotation policy); optional telemetry such as throttled camera_zoom_changed and perf stress markers for occluders.
Implementation snapshot (NEON-25 + NEON-26 + NEON-27 + NEON-30, 2026-04-08 / 2026-04-09)
-
Godot:
client/scripts/isometric_follow_camera.gdonWorld/IsometricFollowCamera; childCamera3D(current).scripts/camera_state.gd—RefCountedtick snapshot (yaw= orbit component, 0 whileallow_yawis false;distance= effective follow distance;zoom_band_index;focus_world,follow_target_path). -
Seam:
allow_yaw/max_yaw_degon the rig; orbit would adjust_orbit_yaw_rad(no input bound yet). Presentation compass usespresentation_yaw_degseparately from orbityawin state. -
Zoom:
ZoomBandConfigresource (client/scripts/zoom_band_config.gd); defaultclient/resources/isometric_zoom_bands.treson main rig. Bands are distance-only (ascending near→far); pitch / FOV unchanged vs NEON-25. Input:camera_zoom_in/camera_zoom_outinclient/project.godot(mouse wheel +=/-+ keypad + / −).follow_distanceremains fallback when config is null or has no bands; statezoom_band_indexis 0 in that case.zoom_band_changedsignal on rig; TODO(E9.M1) for throttledcamera_zoom_changedtelemetry. -
Occlusion:
OcclusionPolicyresource (client/scripts/occlusion_policy.gd); defaultclient/resources/isometric_occlusion_policy.treson main rig. Technique: each_processframe, iterativeintersect_rayfrom_smoothed_eyeto player focus (up tomax_occluder_cast_depthcalls). Bodies tagged"occluder"that intersect the ray have theirMeshInstance3Dsurfaces overridden with a transparentStandardMaterial3Dcopy (fade_alpha = 0.25). Materials restored instantly when the body clears the ray. Null-material surfaces (no mesh or override material set) receive a plain transparentStandardMaterial3D; other non-StandardMaterial3Dsurfaces are skipped withpush_warning. Prototype district:Obstaclenode inmain.tscntagged"occluder". Perf marker:occluder_count_log_thresholdexport (default 0 = disabled) emitspush_warningfor stress-test reference. See risks and telemetry for readability gate.Prototype demo readability gate: before shipping the prototype demo, the PR for NEON-27 must include a before/after screenshot or clip demonstrating that the player is no longer fully hidden by the
Obstacle. This serves as the first data point for the occlusion-hiding-telegraphs risk documented below. -
Click-through input (NEON-30):
scripts/ground_pick.gdunconditionally skips bodies in the"occluder"group during click-to-move ground-pick raycasts. When a cast hits an occluder the ray origin advancesOCCLUDER_PICK_THROUGHmeters past the hit point and continues, independent of whetherOcclusionPolicyis currently fading that body. This keeps the"occluder"group as the sole tagging convention shared between the camera occlusion system and ground-pick input — no dedicated collision layer is added for this case.
Jira backlog
Parent epic: NEON-1 — Epic 1 — Core Player Runtime.
| Key | Type | Summary |
|---|---|---|
| NEON-10 | Feature | E1.M2 — IsometricCameraController (module umbrella) |
| NEON-25 | Story | Isometric follow camera (fixed yaw prototype; CameraState seam) |
| NEON-29 | Story | Expand prototype client district for camera/nav stress QA |
| NEON-26 | Story | ZoomBandConfig + discrete zoom input (clamped) |
| NEON-27 | Story | OcclusionPolicy — keep player readable through geometry |
| NEON-28 | Story | Camera integration hardening + dependent contract notes |
Suggested order: NEON-25 → NEON-26 → NEON-27 → NEON-28. NEON-29 may run in parallel with or after NEON-25 (scene/nav geography; does not require zoom/occlusion).
Risks and telemetry
- Occlusion hiding telegraphs: tune policy early and include readability checklist in prototype gates.
- Enabling yaw orbit mid-project without a telegraph/UI/minimap pass can undermine readability; design ground effects for world accuracy plus screen-space or height cues early (see mid-project rotation policy).
Source anchors
- Master plan:
neon_sprawl_vision.plan.md— Core Epic Map, Epic 1. - Module dependency register