Files
transcience/docs/WORKFLOW.md
T
claude c4beeae38f Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated
server with client-side prediction. Clients send input only; the server
resolves every hit for both players and enemies (no PvP).

- SimWorld: whole simulation as plain RefCounted objects (no nodes, no
  physics server), ~0.24ms/tick at peak load -- runs headless for free and
  drives 78 tests in under a second
- BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn
  events rather than per-tick state
- Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash
  enemies and bosses -- a new boss is data in src/content/content.gd, no
  simulation changes
- The Warden of the Fold: stationary 4-phase boss built entirely on that
  format
- Lobby hub with a portal into on-demand dungeon instances; one process
  hosts the hub plus every concurrent dungeon
- Emergency escape: 3s server-owned channel, cancelled by damage
- tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over
  ENet), bench.gd; git hooks wired to the same scripts
- docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
2026-09-03 16:03:57 +02:00

8.8 KiB

Agent-assisted Godot development

Research notes and the resulting setup for this repo. The short version: Godot is an editor-centric engine and a coding agent is a text-centric tool, so the whole workflow is built around making the game fully runnable, inspectable and verifiable from the command line — with the editor as an optional viewer rather than a required step.

The core problem

An agent editing a Godot project cannot see the editor. It cannot click play, cannot look at the scene dock, and cannot tell whether a .tscn it wrote is valid. Left alone it will produce code that looks right and does not run.

Everything below exists to close that gap. Three properties matter:

  1. A fast, honest failure signal. Parse errors must surface in seconds, from the CLI, with the engine's own words.
  2. A way to actually play the game without a human. Not "the tests pass" — the real binary, the real socket, the real loop.
  3. Source of truth in text. Binary scenes and inspector-tuned resources are invisible to review and produce meaningless diffs.

Godot's CLI, as an agent uses it

Command Use
godot --headless --path . --import Rebuild the import + global class cache. Required after adding any class_name. ~2s.
godot --headless --path . res://scene.tscn Run a scene headless with autoloads alive.
godot --headless --path . --script tools/x.gd Run a SceneTree script. No autoloads.
godot --headless --check-only -s file.gd Parse one file.
godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://tests -gexit Run the test suite.
godot --headless --path . --export-release <preset> <out> Build.

Four things cost real time to discover, so they are worth stating plainly:

  • The global class cache is not built by running the game. It lives in .godot/global_script_class_cache.cfg and is written by the editor or by --import. Add a class_name, run your code, and every reference to it fails with Identifier "Foo" not declared in the current scope — a message that looks exactly like a typo and is not one. tools/check.sh runs --import first for this reason.
  • Autoload singletons do not exist under --script. --script replaces the main loop, so autoload names are unresolved identifiers at compile time. Any tool that needs an autoload has to run as a scene instead. The practical consequence for design: keep autoloads to the minimum that genuinely needs to be a node. This repo has exactly one (Net, because RPC routing is by node path); logging and CLI options are static classes, which makes them usable from tests and tools alike.
  • ResourceLoader.load() on a script with a parse error can return a non-null object. Detecting failures by null-checking the return value silently passes. Read the engine's stderr instead — that is what tools/check.sh does.
  • Never call Script.reload() on the script currently executing. It hangs the engine with no output.

What this repo does about it

tools/check.sh — the inner loop

Refreshes the class cache, loads every .gd in the project, greps the engine's stderr for Parse Error / SCRIPT ERROR / Failed to load script, and exits non-zero on any hit. About five seconds. This is the command to run after every edit; it catches the entire class of "looks right, does not compile" mistakes that an agent produces most often.

tools/test.sh — GUT, headless

GUT 9.7.1 is vendored into addons/gut/. Chosen over gdUnit4 because this project is GDScript-only, where GUT is the lighter and simpler of the two; gdUnit4 is the better pick when C# is in play or when you want its scene-runner utilities. Either runs headless in CI.

The suite runs in about two seconds because of an architectural choice, not a testing trick: the entire simulation is plain RefCounted objects — no nodes, no physics server, no rendering. A test constructs a SimWorld, drives a thousand ticks, and asserts. Nothing to await, nothing to instantiate, no frame timing. If the simulation had been built out of CharacterBody2D and Area2D, every one of these tests would need a live SceneTree and would be slower and flakier by an order of magnitude.

That is the single highest-leverage decision in this repo for agent-assisted work, and it is worth stating as a general rule: keep game logic out of nodes. Nodes are for presentation and input. Logic in plain objects is testable, diffable, reviewable, and runs on a headless server for free.

tools/smoke.sh — the thing tests cannot do

Boots the real dedicated server, connects two scripted bot clients over a real ENet socket, and asserts on the server and client logs that the whole loop happened: handshake, lobby, portal into a dungeon, emergency escape back out.

Unit tests structurally cannot cover RPC wiring, codec round-trips over the wire, instance transfers or client reconciliation. This does, in ~35 seconds, with no display. The bot input lives in ClientRuntime._bot_input() behind --bot, so the "player" driving it is the same code path a human uses.

This is the pattern to reach for whenever an agent needs to verify something interactive: give the program a scripted-input mode and a --autoquit, run it headless, and assert on structured log lines. Log markers like SERVER_READY exist specifically to be grepped.

tools/bench.gd — performance as a number

Runs the simulation with no transport and no rendering and prints milliseconds per tick. Current numbers, 4 players, 60s of play:

boss fight     0.237 ms/tick   peak  352 bullets   headroom x70
trash wave     0.179 ms/tick   peak  145 bullets   headroom x93

A 60 Hz tick has a 16.6 ms budget, so one instance uses ~1.4% of one core. That is the measurement that says a single server process can host dozens of concurrent dungeons, and it took ten seconds to get because the simulation has no engine dependencies.

Content as code, not as .tres

Enemies and bosses are built by GDScript functions in src/content/content.gd, not authored as .tres files. For agent-assisted work this is the right default:

  • A boss is a readable diff. A .tres full of SubResource ids is not.
  • No resource UID churn in version control.
  • A test can build content inline without touching the filesystem.
  • The agent can write a boss without an editor.

tools/export_content.gd writes .tres copies into resources/ for anyone who wants to tune numbers in the inspector, with the direction of truth documented: code wins, port inspector changes back.

The same reasoning applies to scenes. This project has three .tscn files, each a handful of nodes. Anything dynamic — the HUD, the menu — is built in code, because a scene file for it would only be one more thing to keep in sync.

Godot MCP servers

An MCP server lets an agent talk to a live Godot editor: inspect the scene tree, read node properties, create nodes, run the project and read its output. The most established option is Coding-Solo/godot-mcp (~5.5k stars, Node ≥18):

claude mcp add godot -- npx @coding-solo/godot-mcp

It exposes launch_editor, run_project, stop_project, get_debug_output, get_godot_version, get_project_info, create_scene, add_node, load_sprite, save_scene, export_mesh_library, and UID helpers for 4.4+. Set GODOT_PATH if godot is not on PATH.

Where it earns its place: projects with large hand-authored scene trees, where the agent otherwise has to guess node names and paths. Being able to read the real tree instead of inferring it is the biggest single quality jump in that situation.

Where it does not: this repo. run_project and get_debug_output are what tools/server.sh and tools/smoke.sh already do, more precisely and with assertions attached; and the scene-authoring tools matter little when there are three small scenes. It also adds a live-editor dependency, and editor auto-reload racing against agent file writes is a known source of stale state.

The honest recommendation: start with the headless CLI loop, add the MCP when scene inspection becomes the bottleneck. It is one command to add later.

CI

.github/workflows/ci.yml runs check → test → smoke on a container image with Godot preinstalled. All three are headless, so nothing extra is needed.

Sources