Three architecture documents from Sprint 6 design work: - z-layer-gap-analysis.md: full three-scope rendering pipeline with reserved z-ranges, airborne treatment, cross-floor VFX, liquid depth - fog-shader-spec.md: 5-layer fog shader architecture (D-059) - flying-taxi-analysis.md: validates z-layer architecture supports flight scenarios with LOD tiers and performance budget Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
355 lines
18 KiB
Markdown
355 lines
18 KiB
Markdown
# Flying Taxi Over Cityscape — Architecture Analysis
|
||
|
||
Author: Tyre (architecture) | Sprint: 6 | Ref: D-049, z-layer-gap-analysis.md
|
||
|
||
## The Question
|
||
|
||
> Theoretically this will allow the player to take a flying taxi over a cityscape right?
|
||
|
||
Does the three-scope z-layer architecture support a gameplay scenario where the player is in a flying vehicle ascending over a city — seeing the cityscape below with buildings, streets, NPCs getting smaller and fading?
|
||
|
||
## Short Answer
|
||
|
||
**Yes — the architecture supports it.** The z-range allocation, CanvasGroup compositing, scale/alpha model, and negative-z lower floor system all generalize cleanly to a flight scenario. No architectural changes are needed. But the RENDERER needs a flight mode that doesn't exist yet — the architecture provides the skeleton, the renderer provides the muscle.
|
||
|
||
Tier 1 (architecture) — already solved. Tier 2 (renderer) — new systems needed, medium effort.
|
||
|
||
---
|
||
|
||
## What the Architecture Already Supports
|
||
|
||
### 1. The Floor Generalization
|
||
|
||
*cracks knuckles* — this is where it gets elegant.
|
||
|
||
The negative-z floor system was designed for "looking down through a hole" (balcony, catwalk, atrium). But there's nothing hole-specific about it. The system works like this:
|
||
|
||
- Player has a "current floor" (derived from z_step)
|
||
- Floors below render at negative z: z:-100 per floor
|
||
- Each lower floor gets scale < 1.0 (depth illusion) and modulate dimming
|
||
- Parallax from scale-around-player during camera movement sells the depth
|
||
|
||
In a flying taxi, the player's "current floor" IS the taxi's altitude. As the taxi ascends from floor 0 to floor 5:
|
||
|
||
```
|
||
Altitude 0 (ground): current floor = 0, nothing below
|
||
Altitude 1: current floor = 1, floor 0 at z:-100 (scale 0.97)
|
||
Altitude 3: current floor = 3, floors 0-2 at z:-100 to z:-300
|
||
Altitude 5: current floor = 5, floors 0-4 at z:-100 to z:-500
|
||
Altitude 10: current floor = 10, floors 0-9 at z:-100 to z:-1000
|
||
```
|
||
|
||
The architecture doesn't care WHY the player is above other floors — it just renders lower floors at the appropriate depth. Flying taxi, balcony, jetpack, telekinetic levitation — same system.
|
||
|
||
### 2. Scale + Alpha for Depth
|
||
|
||
The existing depth treatment composes:
|
||
|
||
| Component | Per-floor effect | Source |
|
||
|-----------|-----------------|--------|
|
||
| Scale reduction | × 0.97 per floor distance | gap-analysis: Distance Scaling |
|
||
| Modulate dimming | progressive desaturation + alpha | gap-analysis: Fog Interaction |
|
||
| Parallax | content moves slower than camera | gap-analysis: Parallax Effect |
|
||
| Occlusion | higher z draws over lower z | CanvasGroup z-ordering |
|
||
|
||
At altitude 5, the ground floor renders at:
|
||
- Scale: 0.97^5 = 0.859 (86% size)
|
||
- Modulate: heavily dimmed and desaturated
|
||
- Parallax: content moves at 86% of camera speed
|
||
|
||
At altitude 10 (render ceiling):
|
||
- Scale: 0.97^10 = 0.737 (74% size)
|
||
- Modulate: barely visible, ghostly
|
||
- Parallax: content moves at 74% of camera speed — clear depth separation
|
||
|
||
This is exactly the visual of "city getting smaller below you."
|
||
|
||
### 3. CanvasGroup Compositing
|
||
|
||
CanvasGroup collects ALL children, sorts by z_index (negative first), composites into a single texture. Then FogOverlay draws over it. Whether there are 2 lower floors or 10, the compositing pipeline is identical. No architectural limit.
|
||
|
||
Godot z_index range: -4096 to 4096. At z:-100 per floor, we support 40 floors of depth before hitting the limit. The render ceiling (10 floors) is well within range.
|
||
|
||
### 4. The Taxi Itself
|
||
|
||
The player's taxi is a vehicle entity. As it ascends, its z_step increases. From the rendering perspective:
|
||
|
||
- The taxi is on the "current floor" (always at the camera center)
|
||
- The taxi does NOT get the ascending-object scale+alpha treatment (it's not flying AWAY from the player — the player IS in it)
|
||
- Other flying objects at the same altitude render normally relative to the taxi
|
||
- Objects at lower altitude render via the lower floor system
|
||
|
||
The architecture already handles this distinction: "current floor" content renders at z:0-400, lower floors render at negative z.
|
||
|
||
### 5. Render Ceiling as Flight Ceiling
|
||
|
||
The render ceiling (10 floors / 25m) serves double duty:
|
||
|
||
- For ground-based gameplay: maximum altitude for visible sprites (drones, ships)
|
||
- For flight gameplay: maximum flight altitude with full rendering
|
||
|
||
Above the render ceiling, the game transitions to a different mode (narrative cutscene, map view, fast travel). This is a gameplay design constraint, not an architecture limitation.
|
||
|
||
---
|
||
|
||
## What the Architecture Does NOT Provide (Gaps)
|
||
|
||
These are renderer-level systems that need building. The architecture accommodates all of them — the z-ranges, compositing model, and scene tree structure are correct. But the scripts don't exist yet.
|
||
|
||
### Gap 1: Dynamic Visible Floor Window
|
||
|
||
**Current:** `VISIBLE_FLOOR_DEPTH = 2` (constant, ±2 floors)
|
||
**Needed:** Dynamic window that expands with altitude
|
||
|
||
```gdscript
|
||
# The visible floor window grows with altitude, capped at render ceiling
|
||
func get_visible_depth(altitude_floors: int) -> int:
|
||
return mini(altitude_floors, Constants.RENDER_CEILING_FLOORS)
|
||
```
|
||
|
||
At altitude 2, you see 2 floors below (same as current). At altitude 8, you see 8 floors below. At altitude 10+, capped at 10.
|
||
|
||
**Effort: trivial.** Change one constant to one function call. The lower floor node creation is already designed to be dynamic (gap-analysis: "created dynamically when the player enters a location with vertical visibility").
|
||
|
||
### Gap 2: LOD Tiers for Distant Floors
|
||
|
||
**Current:** All visible floors render at full detail (tiles + entities + objects)
|
||
**Needed:** Progressive LOD — distant floors simplify
|
||
|
||
Rendering 10 floors at full detail would cost ~1.6ms (10 × 0.16ms per floor budget). That's manageable but wasteful — floors 7-10 below are barely visible through heavy dimming. Three LOD tiers:
|
||
|
||
| Distance (floors) | LOD tier | Content | Cost estimate |
|
||
|-------------------|----------|---------|---------------|
|
||
| 0-2 | Full | Tiles + objects + entities + VFX | ~0.16ms/floor |
|
||
| 3-5 | Reduced | Tiles + entity dots (no sprites) | ~0.08ms/floor |
|
||
| 6-10 | Minimal | Colored rectangles per building/zone | ~0.02ms/floor |
|
||
|
||
Total at altitude 10: 2×0.16 + 3×0.08 + 5×0.02 = 0.66ms. Well within budget.
|
||
|
||
**Effort: medium.** Needs a LOD manager that switches floor group content based on distance. The z-range allocation doesn't change — only what gets placed INTO each floor's node group.
|
||
|
||
### Gap 3: Camera Zoom Transition
|
||
|
||
**Current:** Camera zoom fixed at Vector2(2, 2)
|
||
**Needed:** Camera zooms out with altitude to show more of the city
|
||
|
||
```gdscript
|
||
# Zoom decreases (shows more) as altitude increases
|
||
var altitude_factor := float(altitude_floors) / Constants.RENDER_CEILING_FLOORS
|
||
var zoom_level := lerpf(2.0, 0.8, altitude_factor) # 2.0 at ground, 0.8 at ceiling
|
||
camera.zoom = Vector2(zoom_level, zoom_level)
|
||
```
|
||
|
||
At ground level: zoom 2.0 (current, focused). At altitude 10: zoom 0.8 (wide, showing city below). The zoom transition should be smooth (tween over the ascent duration).
|
||
|
||
**Effort: trivial.** One zoom tween on Camera2D. No architecture impact.
|
||
|
||
### Gap 4: Empty Current Floor
|
||
|
||
**Current:** The "current floor" always renders ground tiles at z:0
|
||
**Needed:** When airborne, the current floor is empty (you're in the sky)
|
||
|
||
The renderer needs to detect "player is airborne" and:
|
||
- Skip rendering FloorTiles at z:0 (no ground beneath your feet)
|
||
- Skip YSortGroup ground content (no furniture/walls at sky altitude)
|
||
- Keep the Airborne (z:200) and HighAirborne (z:350) nodes active for other flying objects
|
||
- The taxi entity renders as a special case (the vehicle you're in)
|
||
|
||
**Effort: low.** Conditional in world_renderer.update_from_state() — if player is airborne, don't populate current-floor ground content.
|
||
|
||
### Gap 5: Cityscape Composition
|
||
|
||
A city viewed from above shows a different visual than individual floors viewed through holes:
|
||
|
||
- **Rooftops** of buildings at various heights (not interior floors)
|
||
- **Streets** visible between buildings (ground floor, open air)
|
||
- **Parks/plazas** as ground-level open areas
|
||
- **Building heights vary** — a 5-story building next to a 2-story building means different content at the same XY at different altitudes
|
||
|
||
The server needs to send appropriate "viewed from above" content per tile per floor. The ARCHITECTURE handles this (per-floor tile data, per-floor entity data), but the server's snapshot generation needs a flight mode that sends rooftop tiles instead of interior tiles for floors below the player's altitude.
|
||
|
||
**Effort: server-side, medium.** The client architecture is ready — it just renders whatever tiles the server provides at each floor level. The server needs to know the player is airborne and send rooftop/exterior content instead of interior content for visible floors.
|
||
|
||
### Gap 6: Flight Fog Treatment
|
||
|
||
**Current:** Fog shader applies current-floor visibility. Option 2 (modulate dimming) for lower floors.
|
||
**Needed:** At altitude, fog treatment must adapt
|
||
|
||
Two approaches that work within the architecture:
|
||
|
||
**A. Modulate-only (v0.1, simple):** Keep using per-floor modulate for depth. Disable the current-floor fog shader (nothing to fog at sky altitude) or switch it to a "cloud/haze" mode. Lower floors are already dimmed by modulate — fog adds atmospheric depth.
|
||
|
||
**B. Per-floor fog (v0.2):** Each lower floor gets its own visibility texture from the server. The fog shader samples the correct texture per floor. This was already designed as Option 1 in the gap analysis — it generalizes to flight.
|
||
|
||
**Effort: low for A, medium for B.** The architecture supports both. The choice is visual quality vs. implementation cost.
|
||
|
||
---
|
||
|
||
## What the Full Flight Mode Renderer Looks Like
|
||
|
||
When the player enters a flying vehicle, the renderer switches to flight mode:
|
||
|
||
```
|
||
Flight Mode Renderer State:
|
||
current_floor = taxi.altitude_floor
|
||
visible_depth = min(current_floor, RENDER_CEILING_FLOORS)
|
||
camera.zoom = lerp(2.0, 0.8, altitude_factor)
|
||
|
||
FloorTiles z:0 = EMPTY (airborne, no ground)
|
||
FloorObjects z:10 = taxi shadow on ground? (only if altitude < ceiling)
|
||
YSortGroup z:100 = other flying objects at same altitude
|
||
|
||
LowerFloor[1..N] z:-100..-N*100 = city floors below
|
||
LOD tier based on distance:
|
||
1-2 floors: full tiles + entity sprites
|
||
3-5 floors: tiles + entity dots
|
||
6-10 floors: macro colored blocks
|
||
Scale: 0.97^distance
|
||
Modulate: progressive dim/desat
|
||
Parallax: scale-around-player, slower movement
|
||
```
|
||
|
||
### Scene Tree in Flight Mode
|
||
|
||
```
|
||
FogGroup (CanvasGroup)
|
||
LowerFloor10 (Node2D) z:-1000 [LOD: minimal, scale 0.74]
|
||
LowerFloor9 (Node2D) z:-900 [LOD: minimal, scale 0.76]
|
||
...
|
||
LowerFloor6 (Node2D) z:-600 [LOD: minimal, scale 0.83]
|
||
LowerFloor5 (Node2D) z:-500 [LOD: reduced, scale 0.86]
|
||
LowerFloor4 (Node2D) z:-400 [LOD: reduced, scale 0.88]
|
||
LowerFloor3 (Node2D) z:-300 [LOD: reduced, scale 0.91]
|
||
LowerFloor2 (Node2D) z:-200 [LOD: full, scale 0.94]
|
||
LowerFloor1 (Node2D) z:-100 [LOD: full, scale 0.97]
|
||
FloorTiles z:0 [empty — sky]
|
||
FloorObjects z:10 [taxi ground shadow]
|
||
YSortGroup z:100 [other flying objects at altitude]
|
||
Airborne z:200 [higher flying objects]
|
||
Overhead z:300 [empty — no ceiling in sky]
|
||
HighAirborne z:350 [objects above taxi altitude]
|
||
FogOverlay z:900 [altitude haze / disabled]
|
||
```
|
||
|
||
This is a direct extension of the existing gap-analysis architecture. No structural changes — just more LowerFloor nodes and conditional content.
|
||
|
||
---
|
||
|
||
## The Cityscape Visual
|
||
|
||
What does this actually look like at altitude 5 (roughly 12.5m, ~4th story height)?
|
||
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ │ Camera view (zoomed to ~1.2)
|
||
│ ┌────┐ ┌──────────┐ │
|
||
│ │roof│ │ rooftop │ │ Building rooftops (floor 5 tiles)
|
||
│ │ 5F │ │ 3F │ │ Different heights visible
|
||
│ └────┘ └──────────┘ │
|
||
│ │
|
||
│ ═══════════════════════════ │ Street (floor 0, dimmed, 86% scale)
|
||
│ · · · · · · │ NPC dots on the street
|
||
│ ┌──────┐ │
|
||
│ │ roof │ │ Another building (floor 2, dimmer)
|
||
│ │ 2F │ │
|
||
│ └──────┘ │
|
||
│ │
|
||
│ ▓▓▓▓▓▓▓ │ Park (floor 0, green tint, 86% scale)
|
||
│ │
|
||
│ [taxi shadow] │ Your taxi's ground shadow
|
||
│ │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
Buildings taller than the taxi altitude show their rooftops at higher z (closer to current floor). Short buildings show rooftops that are further away (lower z, more dimmed, smaller scale). Streets and ground are the most distant layer — heavily dimmed, small scale, only entity dots visible.
|
||
|
||
The parallax sells it: when the taxi moves, ground-level content moves noticeably slower than rooftop-level content. You perceive DEPTH even in a 2D top-down view.
|
||
|
||
---
|
||
|
||
## Performance Budget
|
||
|
||
| Component | At altitude 5 | At altitude 10 (ceiling) |
|
||
|-----------|--------------|-------------------------|
|
||
| LOD Full floors (0-2) | 2 × 0.16ms = 0.32ms | 2 × 0.16ms = 0.32ms |
|
||
| LOD Reduced floors (3-5) | 3 × 0.08ms = 0.24ms | 3 × 0.08ms = 0.24ms |
|
||
| LOD Minimal floors (6-10) | — | 5 × 0.02ms = 0.10ms |
|
||
| Camera zoom | ~0ms | ~0ms |
|
||
| Scale transforms | ~0ms (GPU) | ~0ms (GPU) |
|
||
| Compositing | ~0.1ms | ~0.15ms |
|
||
| **Total flight overhead** | **~0.66ms** | **~0.81ms** |
|
||
|
||
Compared to ground-level rendering (~2ms total frame budget used): flight adds 0.66-0.81ms. Total stays under 3ms. Well within 16ms frame budget at 60fps.
|
||
|
||
The LOD system is the key performance enabler. Without it, 10 full floors would cost ~1.6ms — still feasible but wasteful.
|
||
|
||
---
|
||
|
||
## What the Server Needs to Provide
|
||
|
||
The architecture is client-ready. The server needs:
|
||
|
||
1. **Airborne player state:** The ObserverSnapshot needs to indicate the player is airborne (not on a surface). The client uses this to switch to flight rendering mode.
|
||
|
||
2. **Multi-floor visible tiles:** When airborne, the server sends tile data for ALL visible floors below (up to render ceiling), tagged with floor_index. Currently the server sends tiles for the current floor only.
|
||
|
||
3. **Rooftop vs. interior tiles:** For floors below the player, send exterior/rooftop tile data, not interior layouts. A building's 3rd floor seen from above shows the roof, not the rooms inside. This is a server-side content selection issue.
|
||
|
||
4. **Per-floor entity data:** Entities on visible floors below, grouped by floor_index. The existing entity data already includes z_step — the client can derive floor_index from it. But the server's visibility calculation needs to include ground-level entities visible from above (not just same-floor LOS).
|
||
|
||
5. **Building height data:** For LOD Minimal tier, the server could send simplified building footprints (position + height + color) instead of per-tile data. This is an optimization for high-altitude rendering, not a requirement.
|
||
|
||
None of these require protocol version changes — they're extensions to existing ObserverSnapshot fields. The `visible_tiles` and `visible_entities` arrays already support per-tile and per-entity floor data via z coordinates.
|
||
|
||
---
|
||
|
||
## Implementation Roadmap
|
||
|
||
**Phase 0 — Already done (Sprint 6):**
|
||
- Z-range allocation supports negative z to -4096
|
||
- RENDER_CEILING_FLOORS = 10 defined
|
||
- VISIBLE_FLOOR_DEPTH = 2 defined (becomes dynamic)
|
||
- CanvasGroup compositing verified for multi-floor
|
||
- Scale + modulate depth treatment designed
|
||
|
||
**Phase 1 — Basic flight (script work only, no server changes):**
|
||
- Flight mode flag in world_renderer
|
||
- Dynamic visible floor window
|
||
- Camera zoom tween based on altitude
|
||
- Empty current floor when airborne
|
||
- Use existing ±2 floor rendering (limited but functional)
|
||
- Can demo with hardcoded lower floor content
|
||
|
||
**Phase 2 — Full cityscape (requires server support):**
|
||
- Server sends multi-floor tile data for airborne players
|
||
- LOD tier system in floor renderer
|
||
- Dynamic LowerFloor node creation/destruction
|
||
- Rooftop tile selection on server side
|
||
|
||
**Phase 3 — Polish:**
|
||
- Altitude fog/haze shader mode
|
||
- Entity dot rendering for distant floors
|
||
- Macro rendering for LOD Minimal tier
|
||
- Taxi interior UI overlay (passenger view)
|
||
- Ground shadow for taxi at z:10
|
||
|
||
---
|
||
|
||
## Verdict
|
||
|
||
The three-scope z-layer architecture **fully supports** the flying taxi scenario. Every component — negative z for lower floors, scale/alpha depth treatment, CanvasGroup compositing, render ceiling, dynamic floor groups — generalizes from "looking through a hole" to "flying above a city."
|
||
|
||
What exists: the skeleton (z-ranges, compositing, contracts).
|
||
What needs building: the muscle (flight renderer, LOD, server multi-floor data).
|
||
|
||
The architecture made the right bets:
|
||
- **Negative z was designed generously** (-4096 available, -1000 needed for 10 floors)
|
||
- **Scale-around-player parallax** works identically for holes and flight
|
||
- **CanvasGroup composites any number of floors** without structural changes
|
||
- **Render ceiling defines the flight ceiling** — one constant, dual purpose
|
||
- **Per-floor modulate** provides atmospheric depth without shader changes
|
||
|
||
Feasibility: **Challenging but doable.** The architecture is the easy part (done). The renderer flight mode is a medium-effort feature (Phase 1: ~1 sprint). Full cityscape with server support is larger (Phase 2: ~2 sprints). But there are no architecture blockers — no rethinking z-ranges, no CanvasGroup limitations, no compositing rewrites.
|
||
|
||
*The flying taxi just fell out of the architecture for free. That's what good range allocation gets you.*
|