11 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. The
"C# backend" in
README.md's early framing was never built. A backend service is now planned for matchmaking (see below), but nothing has chosen C# for it — that framing predates every real decision here. - No HTTP/WebSocket/gRPC layer for multiplayer — ENet/Steam SDR over UDP
only, via Godot's own
MultiplayerAPI. Matchmaking will add the project's first non-UDP network path, for backend traffic only; the simulation stays on ENet/SDR. - No ONNX or other ML runtime in the shipped game — see "AI opponents" above.
Planned, not yet built
- A Go matchmaking control plane — independently runnable API, matcher,
allocator and maintenance roles backed by PostgreSQL and Redis, deployed on
provider-portable Kubernetes with Agones-managed game fleets. The cloud
provider remains deliberately replaceable; the application stack is locked.
This is a 1.0 launch blocker and the single largest departure from "one
Godot project, no backend". See
MATCHMAKING.md.