Standardized YAML frontmatter on all 10 docs/architecture/ files with title, description, type, status, ticket, decision_refs, and author fields. Enables context-aware document loading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
18 KiB
title, description, type, status, ticket, decision_refs, author, created, updated
| title | description | type | status | ticket | decision_refs | author | created | updated | |
|---|---|---|---|---|---|---|---|---|---|
| Flying Taxi Over Cityscape — Architecture Analysis | Feasibility analysis of flying vehicle gameplay using the three-scope z-layer architecture — confirms support with renderer gaps identified | architecture | active |
|
Tyre | YYYY-MM-DD | YYYY-MM-DD |
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
# 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
# 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:
-
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.
-
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.
-
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.
-
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).
-
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.