CLAUDE.md and README.md described the pre-multiplayer state (local-only MVP, planned C# backend) even though server-authoritative multiplayer, the dedicated server, Docker/CI verification, and Steam transport have since shipped (Phases 1-6). Update both to reflect reality and add a docs index in CLAUDE.md pointing at multiplayer-next.md as the current checklist. - Add docs/TECH_STACK.md, linked from README, explaining the stack and why it's a single GDScript project with no separate backend. - Add one TODO item for the video settings menu (missing presets/vsync/ resolution scaling), blocked on the same profiling gate as the multiplayer 0.17 tasks. - Pick up editor-generated .gd.uid sidecars and minor project.godot formatting noise from opening the project in Godot 4.7.
10 KiB
Tech stack
What this project is built with, and why each piece was chosen, sourced from the project's own docs and code comments. Where the reasoning for a choice isn't recorded anywhere, this doc says so rather than guessing.
Engine: Godot 4.7
The whole game — client and dedicated server alike — is one Godot 4.7 project, GDScript only. There is no C#, ONNX, or .NET code involved at runtime anywhere in the shipped product.
Why Godot, specifically: legal, not technical. Per README.md's
"Legality" section, the concept of "vehicle soccer" cannot be copyrighted,
but Rocket League's specific expression of it can be. Building on Unreal or
Unity — the engines Psyonix and most Rocket-League-likes use — would invite
comparison to that specific expression. Using a different engine (Godot) and
different vehicles (space ships instead of cars) is a deliberate part of
keeping the project's own expression original and legally distinct.
Physics: Jolt Physics
Set via Game/project.godot's 3d/physics_engine="Jolt Physics" — Godot
4's alternative physics backend, not the engine's own default
(GodotPhysics3D). All ship and ball movement is force/torque-based
(_integrate_forces), never kinematic.
Why Jolt over Godot's default physics: not written down anywhere in the project's own docs, but per the project owner, the goal was a physics engine whose behavior isn't tied to Godot's own release cycle — so upgrading to a future major Godot version doesn't silently change how the game feels, the way an engine-version upgrade has repeatedly worried Rocket League's own playerbase (players have specifically flagged that Unreal Engine upgrades risk changing timestep/continuous-collision behavior enough to break muscle memory built over thousands of hours).
This holds up under scrutiny. GodotPhysics3D (Godot's built-in default) is
an internal engine subsystem, versioned and rewritten alongside Godot itself,
and has a real history of behavior changing across Godot releases — for
example a kinematic-body regression introduced between 4.3-dev4 and
4.3-dev5, and collision-detection differences reported across the 4.0 line.
Jolt, by contrast, is developed as an independent upstream library
(jrouwe/JoltPhysics) with its own
semantic versioning and a user base beyond just Godot, so its collision
behavior changes on its own release cadence rather than as a side effect of
unrelated Godot core work. It's a real reduction in coupling, not a
complete guarantee: Godot still pins (and can bump) a specific Jolt version
per release, and Godot's own Jolt integration layer can itself introduce
differences — e.g. a sleeping RigidBody3D wakes differently under Jolt
than under GodotPhysics3D when another body approaches it.
Separately documented, and a real consequence either physics backend would
share: Jolt is not bit-deterministic across platforms or even across
differing contact orderings on the same platform, and Godot exposes no world
snapshot/restore API. That fact is why the multiplayer architecture is
server-authoritative with client-side prediction of only the local ship,
rather than rollback/resimulation netcode — rollback would require
deterministic replay, which no physics engine choice here provides
(multiplayer-todo.md §1, decision 1).
Multiplayer transport: Godot's built-in MultiplayerAPI over ENet
The default and fully-supported transport is ENetMultiplayerPeer —
Godot's built-in high-level multiplayer networking, direct-IP over UDP,
port 7777 by default. A thin NetTransport abstraction
(Game/scripts/net_transport.gd) exists specifically so a second transport
(Steam) can be swapped in without touching the rest of the networking code.
Design choices layered on top of the built-in peer, and why:
ENetMultiplayerPeer.server_relayis forced tofalse. It defaults totrue, which lets any clientrpc()any other client through the server — incompatible with a server-authoritative model. Called out inmultiplayer-todo.md§2.1 as "the single highest-value one-line security change in the document."- Manual multiplayer polling, not Godot's automatic idle-frame poll.
NetworkManagercallsset_multiplayer_poll_enabled(false)because the automatic poll runs on the idle frame, which would tax every RPC issued from_physics_processup to a full frame in each direction — unacceptable for a physics-tick-rate game. - Server-authoritative simulation with client-side prediction of the local ship and ball only; no world rollback. Direct consequence of Jolt's non-determinism (see above).
- A custom binary wire format (
net_codec.gd) rather than raw RPC argument marshalling, for compact, quantised input/snapshot packets sent at high frequency — no stated alternative was considered in the docs, but the packet-size/channel-intent design inmultiplayer-todo.md§2 is extensive and deliberate.
Optional multiplayer transport: Steam (GodotSteam)
Game/scripts/steam_transport.gd implements the same NetTransport
interface using SteamMultiplayerPeer over Steam's SDR (Steam Datagram
Relay), from a custom GodotSteam-patched Godot build (not stock Godot —
STEAM.md). It is entirely optional: the default build and every CI check
use ENet only, and a build without the steam feature is fully functional
without it.
Why it's optional and why raw ENet remains primary: multiplayer-todo.md
states plainly that "Docker/VPS is the primary v1 deployment path. Raw ENet
self-hosting needs port forwarding, and SDR is Phase 7 — so [the ENet
phases] ship something that works on LAN or a VPS and nowhere else." Steam/SDR
is being added later specifically to remove the port-forwarding requirement
and to supply verified player identity — direct-IP ENet's slot-reclaim logic
is keyed by display name today, which is insecure against a public server
(see multiplayer-next.md).
Dedicated server hosting: Docker (primary) or native systemd
The dedicated server is not a separately-written service — it's the same
Godot project, exported headless (res://scenes/server_boot.tscn) via
Godot's own --export-release "Linux Dedicated Server" preset. Two
deployment paths are documented (SERVER.md):
- Docker, the primary path: a multi-stage
Dockerfilebuilds the export inside a pinnedbarichello/godot-ci:4.7.1image and produces a slimubuntu:24.04runtime image.make verify-phase6builds it, runs it, joins two independent client processes to it, and asserts on match/goal/ arena-rotation behaviour — this is also the entire Phase 6 GitHub Actions workflow. - Native systemd, for a VPS: copy the exported binary to
/opt/cosmic-clash, run it as a dedicatedcosmicclashservice user viadeploy/cosmic-clash-server.service.
Per the project owner, Docker was chosen as the primary path for three
reasons: it gives a pinned, reproducible build environment that behaves
identically across local development, CI, and hosted production servers
(rather than three separately-drifting setups); it's portable across
hosting providers instead of assuming a specific Linux distro/init system
the way the systemd unit does; and it's the tooling the team is already
most familiar with. That matches what's independently visible in the repo —
SERVER.md documents Docker as the one path CI actually exercises
(make verify-phase6), while the systemd unit is native-deployment
documentation only, with no automated verification of its own.
AI opponents: reinforcement learning, trained out-of-process, run in pure GDScript
Two entirely separate pieces, deliberately joined only at a JSON file:
- Training (Python, not shipped): Godot RL Agents
(
godot-rl==0.8.2, vendored bridge plugin atGame/addons/godot_rl_agents, MIT-licensed) drives self-play PPO via Stable-Baselines3 (==2.4.0) over PyTorch (==2.13.0) and Gymnasium (==1.0.0), against a headless instance of the actual game (training/train.pylaunches real parallelgodot --headlessprocesses from source — the training environment is the game, not a reimplementation of its physics).training/requirements.txtpins these versions strictly, because the training code (ship_action_codec.gd,train.py) depends on specific library-internal behaviour (godot_rl's discrete-action-space branch, SB3's logit layout, Gymnasium's dict-key sorting) that an unpinned upgrade could silently change mid-curriculum. - In-game inference (
Game/scripts/policy_network.gd): the trained checkpoint is exported to a small JSON file (training/export_policy.py) and evaluated at runtime by a hand-written, dependency-free GDScript MLP.Game/addons/godot_rl_agents/VENDORED.mdnotes the plugin's ONNX/C# files are present upstream but unused here — they require the .NET Godot build, which this project does not use.
Why this split, rather than shipping ONNX/.NET inference: stated
directly in TRAINING.md — "the trained policy is exported to a small JSON
file and runs inside the game in pure GDScript — shipped bots need no
Python, no .NET, no network." Keeping the shipped game GDScript-only (no
.NET Godot build) is consistent with the rest of the stack.
Tooling (not shipped with the game)
mcp/godot-mcp(git submodule, Node/TypeScript) — drives a live Godot editor/runtime instance for AI-assisted development; not part of the game.mcp/blender-mcp(git submodule, Python/uv) — drives a live Blender instance for generating original 3D assets (ships, arenas), for the same originality reasons covered under "Why Godot" above.tools/blender/,tools/textures/— standalone Python scripts (Blender's embedded Python, plus texture generators) used to produce the project's original meshes and textures.
What's deliberately absent
- No C# or .NET runtime anywhere in the shipped game or server, despite
early project framing (see
README.md's history) once describing a "C# backend" — that was never built and is not the current plan. - No HTTP/WebSocket/gRPC layer for multiplayer — ENet/Steam SDR over UDP
only, via Godot's own
MultiplayerAPI. - No ONNX or other ML runtime in the shipped game — see "AI opponents" above.