-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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).
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.
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 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.
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 toowned=trueand silently skips any node without anownerset. Every runtime-built node needs.ownerset explicitly, in add order — the next node's@onreadyvars resolve synchronously during its ownadd_child()call, so owner-setting can't be batched at the end. -
XRHelpers.get_xr_camera()looks upget_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.
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).
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.