Skip to content

Architecture

Alex C edited this page Jul 3, 2026 · 1 revision

Architecture

This page is for anyone adopting this repo as a sample project. It covers the conventions that make this codebase unusual, and why they exist.

Zero imported assets, everything built at runtime

There are no imported meshes, textures, or skeletal rigs anywhere in this project. Every mesh (vehicles, cockpit controls, enemies, world dressing, avatars) is built in pure GDScript from primitives, and every scene node is constructed at runtime with .new()/add_child()nothing is instantiated from a .tscn scene file. This is deliberate, not incidental, and it has real consequences called out below (owner-based child lookups, editor import caching).

MeshKit (scripts/mesh_kit.gd)

Static helpers (MeshKit.box, MeshKit.cyl, MeshKit.prism, ...) that append triangles to a shared SurfaceTool, each carrying vertex colors instead of a UV-mapped texture. MeshKit.commit() bakes the result into one ArrayMesh with one vertex-color material — the whole point is that a complex merged mesh (a full vehicle, a full building) still draws in a single draw call, which matters a lot on Quest's draw-call budget.

If you're extending this: any new primitive helper must get its winding order right, or the triangle back-face-culls invisibly with no error — this bit the project twice (MeshKit.cyl() side walls, and a copy-pasted SurfaceTool quad-grid bug in world_dressing.gd's sea/lava). There's a headless audit tool, scripts/mesh_audit.gd, that checks real winding-vs-stored-normal across every mesh in the game — run it after touching any mesh-generation code.

VRControl (scripts/interactables.gd)

Base class for every physical cockpit control — Lever, TwoAxisGrip, Knob, PushButton, ToggleSwitch, SafetyCover. Each is a small state machine with a public API the rig drives: on_grab(hand) / on_hand_update(hand_pos) / release() for grabbables, poke_check(tip, delta) for poke-only controls (buttons, switches). No physics joints — same "abstract the drag into a value" trick you'd use in a 2D game, applied in 3D.

cockpit_builder.gd instantiates ~20 of these per vehicle and returns them in a Dictionary (cockpit["controls"]), keyed by name ("battery", "starter", "tiller_l", ...). This dict is the stable, always-working way to reach a control programmatically — DesktopRig (keyboard testing), AI/network-driven state sync (net.gd), and the vehicle's own internal logic (player_tank.gd) all read/write controls through this dict directly, not through physical proximity.

See Known Issues for the current gap between this dict-based path (always works) and physical hand-grab/poke discovery (currently broken).

XRRig / DesktopRig (scripts/xr_rig.gd, scripts/desktop_rig.gd)

XRRig is the real VR rig: origin + camera + two hands, menu laser pointers (aim-pose ray, with a head-gaze fallback so the menu is never unreachable), vehicle seat attach/detach, thumbstick fallbacks for every core action (drive, aim, fire, reload, rockets, restart — see How to Play), and seat auto-calibration on pose_recentered.

DesktopRig is the non-VR fallback: mouse-look camera + WASD/IJKL keyboard driving, used for headless smoke tests and desktop screenshots. Both expose the same attach_to_vehicle(v) / to_menu_anchor(parent) interface so main.gd doesn't need to know which one is active.

On-foot mode (godot-xr-tools integration)

Built on top of the godot-xr-tools addon's movement providers (direct movement, sprint, climb, grapple) and XRToolsPlayerBody. XRRig builds these provider/pickup nodes once, permanently, enabled=false, then flips them via one _set_on_foot_active() helper when the player enters/exits a vehicle — the addon's own _ready() scans for movement-provider nodes exactly once at startup, so they must already exist in the tree before any OnFootBody is added. Three MeshKit-built XRToolsPickable props (grapple hook, climbing gloves, energy drink) gate which providers are active.

Gotchas hit integrating a .tscn-based addon into a zero-.tscn project, worth knowing if you're doing the same:

  • XRTools.find_xr_child() defaults to owned=true and silently skips any node without an owner set. Every runtime-built node needs .owner set explicitly, in add order — the next node's @onready vars resolve synchronously during its own add_child() call, so owner-setting can't be batched at the end.
  • XRHelpers.get_xr_camera() looks up get_node_or_null("Camera") first; its owned-child-search fallback can't succeed here (see above), so the camera node must be named exactly "Camera".
  • The addon's file-copy list can skip files as "example-only" that other addon files still hard-depend on just to compile (e.g. effects/fade.gd, hands/*.gd), even if this project never instantiates them.

Avatars (scripts/avatar_rig.gd)

Procedural, transform-driven AvatarRig — no Skeleton3D, same "rigid MeshKit pieces repositioned per frame" idiom as everything else in this codebase, just applied to a body instead of a vehicle. Hip yaw blends 70% head / 30% hand-implied yaw with damping (a Rec Room trick — hips lag behind head). Arms use analytic 2-bone IK. One configure(mode, tint) call switches between SEATED (legless, for cockpit use) and ON_FOOT (full body with a leg-hiding skirt panel), while preserving the same instance's identity across a seated↔on-foot transition. The same rig drives remote multiplayer players (via replicated hand/pose data over RPC) and stationary NPCs (via an authored idle/aiming pose, no tracking data).

QA / verification without a headset

Several harnesses exist specifically so this project can be checked without a Quest attached:

Tool What it proves
scripts/mesh_audit.gd Winding-vs-normal correctness across every generated mesh (--check-only-friendly)
scripts/gameplay_qa.gd + scenes/gameplay_qa.tscn Drives the real scenes/main.tscn through menu→drive→fire→wave-progression→death→restart, headless
tools/xr_interaction_smoke.gd Drives VRControl's own public API directly against a real cockpit — the same calls xr_rig.gd makes, bypassing the (currently broken) group-based discovery
--smoke flag (tools/main.gd) Boots the real game for N frames headless, fails on any script error

None of these substitute for a human in a headset — they're explicitly scoped to what can be verified without one. Real controller/hand-tracking input plumbing, comfort, and reachability still need a person physically playing the game. See Known Issues for a case where this distinction mattered a lot.