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
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
# 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](https://github.com/bitwes/Gut) is vendored into `addons/gut/`.
|
||||
Chosen over [gdUnit4](https://github.com/godot-gdunit-labs/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](https://github.com/Coding-Solo/godot-mcp) (~5.5k stars,
|
||||
Node ≥18):
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
- [Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp)
|
||||
- [GUT](https://github.com/bitwes/Gut) · [gdUnit4](https://github.com/godot-gdunit-labs/gdUnit4)
|
||||
- [Godot: high-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html)
|
||||
- [Godot: command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html)
|
||||
Reference in New Issue
Block a user