· 6 min read ·

Generate the Builder, Not the Format: An Architectural Lesson from Godogen

Source: hackernews

There are two ways to generate a machine-readable format in a code generation pipeline. You can generate the format directly, treating the target file as a template to be filled in. Or you can generate code that, when executed, produces the format through the system’s own API. The first approach is simpler to reason about but requires the generator to maintain invariants that the format’s authors encoded in their tooling. The second approach is more indirect but offloads correctness to the system that was designed to produce correct output in the first place.

Godogen, a pipeline that generates complete playable Godot 4 games from text prompts, chose the second path for scene generation, and the consequences of that choice are worth tracing in detail.

What Makes the .tscn Format Hostile to Direct Generation

Godot stores scenes as .tscn files, an INI-like text format that the Godot editor writes and reads. A minimal scene file looks something like this:

[gd_scene load_steps=3 format=3 uid="uid://abc123"]

[ext_resource type="Script" path="res://player.gd" id="1_abc"]

[node name="Player" type="CharacterBody2D"]
script = ExtResource("1_abc")
position = Vector2(100, 200)

The load_steps count in the header must equal the number of ext_resource and sub_resource declarations in the file exactly. UIDs must be unique across the project. Resource IDs referenced in node properties must match declarations earlier in the file. External resource paths break silently when the project directory structure changes. A single malformed line, a missing quote, an off-by-one in load_steps, produces a scene that fails to load with an error message that is rarely informative about which specific invariant was violated.

For a human editor, the Godot editor maintains all of these invariants automatically. For an LLM generating the format as text, every invariant is a potential failure mode. The model generating a scene with twelve nodes, several external script references, and a handful of sub-resources has to track integer IDs, format-specific path syntax, and count declarations against a header field, all while also getting the game logic right. That is a category of failure that has nothing to do with whether the model understands game development.

Generating the Builder Instead

Godogen’s approach is to generate GDScript that constructs the scene in memory using Godot’s own runtime API, then let the engine serialize it:

func build_scene() -> Node:
    var root = Node2D.new()
    root.name = "Main"
    
    var player = CharacterBody2D.new()
    player.name = "Player"
    player.position = Vector2(100, 200)
    root.add_child(player)
    player.owner = root  # Critical: must be set explicitly
    
    ResourceSaver.save(PackedScene.new().pack(root), "res://scenes/main.tscn")
    return root

The resulting .tscn file is valid by construction because the engine that wrote it also defines what valid means. Load step counts are computed by the serializer. Resource IDs are assigned consistently. UID uniqueness is maintained by Godot’s internal resource management. The category of format invariant violations disappears entirely.

This is not a novel insight in software engineering. Kubernetes operators that construct resources using client-go rather than handwriting YAML avoid the same class of structural errors. ORMs that generate SQL from typed object models prevent the manual SQL construction issues that plagued the pre-ORM era. Microsoft’s Open XML SDK exists specifically because Office Open XML’s invariants are complex enough that generating it as raw XML text is error-prone even for experienced developers. The Godot .tscn format documentation itself is explicit that the format is designed for tool output, not hand-authoring.

The principle is: when a format is designed to be written by a tool, the knowledge required to generate it correctly is encoded in that tool’s implementation, not in the format specification. Using the tool’s API to produce the format delegates format correctness to the party that maintains the format’s invariants.

The Tradeoff: API Lifecycle Instead of Format Syntax

Using the engine’s API to build scenes rather than generating the format directly shifts the correctness problem, but does not eliminate it. The format’s structural invariants are gone. The API’s runtime invariants remain, and some of them are harder to reason about.

Godot’s scene-building API exists in two distinct execution phases. During headless construction, the code runs before any scene tree is live. During runtime, nodes have entered the tree, the _ready() callback has fired, and the full game loop is active. Many APIs mean different things, or mean nothing at all, depending on which phase they execute in.

The @onready annotation is the most common way this surfaces:

@onready var player: CharacterBody2D = $Player

This annotation defers the variable assignment until _ready() fires after the node enters the scene tree. In a running game, it works as expected. In a headless construction script, there is no scene tree to enter. The annotation executes as a no-op. The variable is never assigned. The resulting scene might serialize without error, but the runtime behavior is wrong.

Signal connections embedded in .tscn files have the same phase dependency. They exist as metadata in the serialized scene but become live bindings only after instantiation. Writing a headless script that tries to connect signals using node path references before those paths exist produces connections that silently fail or reference null.

The most consequential phase-specific contract in Godot’s headless API is node ownership. Every node created programmatically must have its owner property set to the scene root before the scene is serialized, or the node disappears silently on reload. This is not a structural invariant of the .tscn format; the file is written without error. It is a contract between the node and the serialization system that has no presence in the API documentation for add_child() or Node.owner. You discover it by losing nodes and working backward.

var node = Node2D.new()
scene_root.add_child(node)
# Without this line, the node is present in memory and writes to disk
# but disappears when the .tscn is reloaded:
node.owner = scene_root

This is precisely the class of implicit contract that the Godot docs XML source cannot provide. Godogen’s response is a structured quirks database of these behaviors, compiled through building headless scenes until they broke, and injected selectively into the model’s context when the generation task involves operations where the contract applies.

What the Shift in Problem Structure Means for LLM Pipelines

Moving from format generation to API-mediated generation changes the knowledge requirements for an LLM pipeline in a useful direction. Format invariants are precise but opaque: the load_steps count must match, and if it doesn’t, nothing informative tells you why. API lifecycle invariants are implicit but documentable: they describe when operations are valid, what preconditions must hold, and what the consequences of violation look like. That last property matters. The node ownership failure produces a specific, recognizable pattern: the node vanishes on reload. Once you know the cause, you can encode it. Once it is encoded in a quirks database, it can be injected into the model’s context whenever the generation task involves programmatic scene construction.

Format syntax errors are recoverable through static validation before the file is ever loaded. API lifecycle errors require execution to surface. But the execution feedback loop that Godogen runs, using Godot’s headless mode to execute generated scenes and capture output, provides exactly that signal. A node vanishing on reload is observable in the headless output. A load_steps mismatch in a hand-generated .tscn might produce an error that the model has no schema for interpreting.

For domain-specific LLM pipelines targeting formats with complex invariants, this tradeoff favors the API-mediated approach, even when the API’s own contracts require careful documentation. Format invariants resist debugging; they produce opaque failures. API lifecycle invariants, once discovered and encoded, can be provided to the model as structured context that covers the specific generation task at hand. The model does not need to know all of Godot’s lifecycle contracts; it needs to know the ones relevant to the operation it is performing. Lazy-loading that knowledge, as Godogen does with both its API reference and its quirks database, makes that tractable.

Godogen spent a year and four rewrites arriving at this architecture. The format-avoidance choice is not prominent in how the project describes itself, but it is load-bearing. Everything downstream, the phase documentation, the quirks database, the headless evaluation loop, depends on having made the right call about what kind of knowledge the pipeline needs to maintain. Structural knowledge about the .tscn format is the wrong kind. Lifecycle knowledge about the scene-building API is the right kind. The four rewrites were partly about learning that distinction the hard way.

Godogen is open source on GitHub.

Was this interesting?