· 6 min read ·

Three Engineering Problems That Stand Between an LLM and a Playable Game

Source: hackernews

The idea sounds simple: type a prompt, get a playable game. The execution is not. Godogen is a pipeline built over roughly a year, through four major rewrites, that takes a text description and produces a complete, runnable Godot 4 project, including scene files, GDScript, and 2D/3D assets. The author describes three specific engineering bottlenecks that had to be solved before the output was reliable. Each one is worth examining in detail because they point at structural problems that apply to any domain-specific code generation project, not just game development.

The Python Trap in GDScript

GDScript looks like Python. That is the problem.

Shared syntax: indentation-based blocks, for x in array:, if condition:, func instead of def, and optional type annotations that resemble PEP 484. An LLM that has consumed millions of Python files will pattern-match GDScript prompts to Python solutions with high confidence and be wrong in ways that are hard to debug.

The failure modes are concrete. len(arr) is valid Python but invalid GDScript; the correct call is arr.size(). array.append(x) becomes array.push_back(x). None becomes null. There are no import statements in GDScript; resources are loaded with preload() or load(). Every GDScript file that attaches to a node must begin with extends NodeType, and omitting that line produces silent failures at scene load rather than a clear error. Signal connection syntax changed entirely between Godot 3 and Godot 4:

# Godot 3 (wrong in Godot 4)
$Timer.connect("timeout", self, "_on_timer_timeout")

# Godot 4 (correct)
$Timer.timeout.connect(_on_timer_timeout)

LLMs trained on tutorials, Stack Overflow answers, and documentation from before Godot 4’s release (April 2022) will emit Godot 3 signal syntax confidently. The code passes a syntax check, runs, and silently fails to wire up the connection.

Then there is the class scope issue. Godot 4 ships with around 850 built-in classes, ranging from Node and Node2D through CharacterBody2D, Area2D, RigidBody2D, AnimationPlayer, Control, and down into physics servers, rendering objects, and audio buses. Models frequently invent class names that don’t exist (Sprite instead of Sprite2D), use class names that existed in Godot 3 but were removed in Godot 4 (KinematicBody2D became CharacterBody2D), or call methods that belong to a parent class they haven’t looked up.

Godogen’s fix is a hand-authored reference system: a language spec written explicitly for LLM consumption, full API documentation converted from Godot’s XML source, and a quirks database for engine behaviors that aren’t documented anywhere. The 850-class total can’t be injected wholesale into a context window, so the agent lazy-loads only the specific class documentation it needs at each generation step. This is effectively a lightweight retrieval-augmented generation setup, where the retrieval is triggered by the agent recognizing which classes it intends to use, not by vector similarity.

Build Time Versus Runtime in Godot’s Scene Model

Godot’s scene format is a plain-text, INI-like structure. A minimal scene looks like this:

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

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

[node name="Player" type="CharacterBody2D"]
script = ExtResource("1_abc")

[node name="Sprite2D" type="Sprite2D" parent="Player"]
texture = ExtResource("2_def")

The load_steps header must match the exact count of [ext_resource] and [sub_resource] entries. The uid values are globally unique identifiers that Godot’s editor manages; generating plausible-looking but incorrect UIDs causes scene loading failures. Node hierarchy is encoded through parent= path strings, not through bracket nesting, so a mistyped parent path silently re-parents a node to a location that may not exist. Hand-generating .tscn files by LLM is a bookkeeping problem more than a reasoning problem, and LLMs are poor at bookkeeping.

Godogen avoids this by having headless scripts build the scene node graph in memory using Godot’s own runtime API and then serializing to .tscn. The engine writes the file correctly because the engine knows what correct looks like. The trade-off is that this approach imposes the build-time versus runtime distinction as a hard constraint on the generation pipeline.

The @onready annotation is the clearest example of why this matters:

@onready var sprite: Sprite2D = $Sprite2D
@onready var timer: Timer = $Timer

@onready is a build-time annotation that defers expression evaluation to _ready(), the lifecycle method called after the node and all its children have entered the scene tree. Without it, $Sprite2D evaluated at script parse time returns null because the tree doesn’t exist yet. Signal connections face the same constraint: you can wire them in the .tscn file itself (done at scene load, before _ready()), or you can connect them in code inside _ready() or later, but not before.

When the scene is being built by a headless script rather than the editor, @onready and scene-file signal connections don’t exist. The headless script runs in an environment where there is no active scene tree to wire into. This means the code generator has to understand which APIs are available during the headless build phase, which are only available when the game actually runs, and produce code that uses the right APIs in the right phase. Getting this wrong produces errors that appear at runtime, not at generation time, which makes the evaluation loop significantly harder.

The Evaluation Loop Problem

Testing generated code is the hardest part of any LLM code generation pipeline, and game code is harder than most. A syntax error is easy: run the script parser, get a line number, send it back to the model. A logic error is harder: the game runs but plays incorrectly, or a character falls through the floor, or a signal fires twice. A visual error is harder still: the assets are misaligned, animations don’t trigger, or the camera follows the wrong node.

The author notes that a coding agent is “inherently biased toward its own” output, which suggests that naively asking the model to evaluate its own generated code produces low-quality feedback. The model that wrote the code tends to read it as correct because it is pattern-matching its own generation, not independently verifying behavior.

Godot’s headless mode (godot --headless) allows running the engine without a display, which makes automated testing at least partially viable. A headless run can catch import errors, syntax errors in scripts, and _ready() crashes. It cannot catch most gameplay logic errors or visual errors. Godogen uses visual testing as one of its validation strategies, which suggests screenshot-based or frame-capture-based comparison, though the specifics of how that loop closes aren’t fully detailed in the project description.

This is the unsolved part of LLM-based game generation. The inner loop from code to feedback works reasonably well for syntax and startup crashes. The outer loop from generated game to “is this actually a good game” has no clean automated solution. Human review remains the backstop.

What This Reveals About Domain-Specific Code Generation

Godogen’s year-long, four-rewrite development arc is instructive. The problems it solved are not specific to Godot; they are general problems in LLM-based code generation for any domain-specific environment with sparse training data, complex runtime models, and limited automated evaluation.

Sparse training data is the rule, not the exception. Most specialized frameworks, engines, and APIs don’t have the training data coverage of Python’s standard library or JavaScript’s npm ecosystem. The standard response is to augment the model with documentation, but documentation alone doesn’t capture the quirks, the version differences, the implicit rules that experienced practitioners know and documentation omits. Godogen’s explicit quirks database for undocumented Godot behaviors is one approach; retrieval over real codebases from GitHub is another, though it has the same version-skew problem that affects GDScript generation.

The build-time versus runtime distinction matters in every framework with a lifecycle model, which is most of them. React’s rules of hooks, Godot’s scene tree lifecycle, Django’s ORM initialization, Unity’s Awake()/Start()/Update() sequence, Rails’ before-filters: all of these create contexts where certain APIs are or aren’t available, where certain operations must happen in a specific order, and where violating those constraints produces errors that may be distant in time and code from the original mistake. Correctly modeling lifecycle for code generation requires explicit, structured knowledge about which APIs belong to which phase, not just pattern matching on examples.

The lazy API loading strategy Godogen uses is worth borrowing for any project doing similar work. Rather than injecting full documentation up front or relying on a vector retrieval system, a structured two-pass approach, where the model first identifies which classes or APIs it intends to use and then receives full documentation for those specific items, keeps context focused and avoids the quality degradation that comes from stuffing a context window with loosely relevant text.

The project is open source and worth following for anyone building generation pipelines over domain-specific runtimes. The engineering choices documented there are directly applicable well beyond Godot.

Was this interesting?