Can Claude Opus 5.5 write working Godot 4 code? 15 of 15 on the first try
By Eduardo Orellana · Updated
Claude Opus 5.5 passed 15 of 15 Godot 4 tasks on the first try and 15 of 15 after one fix with the test's output, each run headless on Godot 4.7.2. It missed none.
Every model measured on the same Godot 4 tasks, each run headless on Godot 4.7.2 at a fixed 60 frames per second.
Measured by Flockbay: the same tasks, the same checks, one fix allowed with the test output.
| Model | First try | After one fix | Agent | Run on | Read from |
|---|---|---|---|---|---|
| Claude Opus 5.5 | 15 of 15 | 15 of 15 | Claude Code | 2026-10-06 | Claude Opus 5.5 results |
| Claude Sonnet 5.5 | 15 of 15 | 15 of 15 | Claude Code | 2026-10-06 | Claude Sonnet 5.5 results |
| GPT-6.1 Sol | 15 of 15 | 15 of 15 | Codex | 2026-10-06 | GPT-6.1 Sol results |
| Claude Haiku 4.5 | 11 of 15 | 14 of 15 | Claude Code | 2026-10-06 | Claude Haiku 4.5 results |
How the test works
Each task asks for one GDScript file with a stated API, in a fresh Godot project. Claude Opus 5.5 ran as a coding agent (Claude Code, headless, allowed to read and write files and nothing else). A check scene then loaded the file and drove it on Godot 4.7.2, headless, at a fixed 60 frames per second: it pressed input actions, stepped physics, waited on timers and signals, and compared the results.
When a task failed, the model got the check's output once and could fix its file. Every task touches an API that changed between Godot 3 and Godot 4, or was added since: move_and_slide and is_on_floor, FileAccess and JSON, signals, tweens, Timer nodes, PackedScene, TileMapLayer, ray queries, AStarGrid2D, AnimationLibrary, and lambdas, which capture local variables by value.
The checks themselves are tested first: each passes on a reference answer and fails on an empty script. Run on 2026-10-06.
Move a CharacterBody2D with an input action: pass
The task: Write res://player.gd for Godot 4: a script for a CharacterBody2D that moves right at 200 pixels per second while the input action "ui_right" is held, and stands still otherwise.
Passed on the first try.
extends CharacterBody2D
const SPEED := 200.0
func _physics_process(_delta: float) -> void:
if Input.is_action_pressed("ui_right"):
velocity = Vector2(SPEED, 0.0)
else:
velocity = Vector2.ZERO
move_and_slide()
Fall with gravity and land on the floor: pass
The task: Write res://ball.gd for Godot 4: a script for a CharacterBody2D that falls with a gravity of 980 pixels per second squared and stops when it lands on the floor. The scene gives it its collision shape.
Passed on the first try.
extends CharacterBody2D
const GRAVITY := 980.0
func _physics_process(delta: float) -> void:
if is_on_floor():
velocity.y = 0.0
else:
velocity.y += GRAVITY * delta
move_and_slide()
A score label: pass
The task: Write res://score_label.gd for Godot 4: a script for a Label that shows "Score: 0" when it enters the scene, and has a method add(points: int) that adds to the score and updates the text to "Score: <total>".
Passed on the first try.
extends Label
var score: int = 0
func _ready() -> void:
score = 0
_update_text()
func add(points: int) -> void:
score += points
_update_text()
func _update_text() -> void:
text = "Score: %d" % score
Save and load a dictionary as JSON: pass
The task: Write res://save_game.gd for Godot 4: a script extending RefCounted with two static functions: save_data(data: Dictionary, path: String) -> void writes the dictionary to the file as JSON, and load_data(path: String) -> Dictionary reads it back.
Passed on the first try.
extends RefCounted
static func save_data(data: Dictionary, path: String) -> void:
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
push_error("Could not open %s for writing: %s" % [path, error_string(FileAccess.get_open_error())])
return
file.store_string(JSON.stringify(data, "\t"))
file.close()
static func load_data(path: String) -> Dictionary:
if not FileAccess.file_exists(path):
return {}
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
push_error("Could not open %s for reading: %s" % [path, error_string(FileAccess.get_open_error())])
return {}
var text := file.get_as_text()
file.close()
var parsed: Variant = JSON.parse_string(text)
if parsed is Dictionary:
return parsed
push_error("Save file %s does not contain a JSON object" % path)
return {}
Health with signals: pass
The task: Write res://health.gd for Godot 4: a script extending Node with health starting at 100, a signal health_changed(old_value: int, new_value: int), a signal died, and a method take_damage(amount: int) that lowers health without going below 0, emits health_changed when health changes, and emits died once, when health reaches 0.
Passed on the first try.
extends Node
signal health_changed(old_value: int, new_value: int)
signal died
var health: int = 100
var _is_dead: bool = false
func take_damage(amount: int) -> void:
if amount <= 0 or _is_dead:
return
var old_value := health
health = max(health - amount, 0)
if health != old_value:
health_changed.emit(old_value, health)
if health == 0 and not _is_dead:
_is_dead = true
died.emit()
Fade out with a tween: pass
The task: Write res://fader.gd for Godot 4: a script extending Node2D with a method fade_out(duration: float) that fades the node's modulate alpha from 1 to 0 over duration seconds with a tween, and a signal faded emitted when the fade ends.
Passed on the first try.
extends Node2D
signal faded
var _tween: Tween
func fade_out(duration: float) -> void:
if _tween and _tween.is_valid():
_tween.kill()
modulate.a = 1.0
_tween = create_tween()
_tween.tween_property(self, "modulate:a", 0.0, duration)
_tween.finished.connect(func() -> void: faded.emit())
Spawn on a timer, then stop: pass
The task: Write res://spawner.gd for Godot 4: a script extending Node that, once in the scene, adds a new child Node2D every 0.25 seconds, using a Timer, and stops after 4 children. It keeps the number spawned in a variable spawned_count.
Passed on the first try.
extends Node
const MAX_SPAWNS := 4
const SPAWN_INTERVAL := 0.25
var spawned_count: int = 0
var _timer: Timer
func _ready() -> void:
_timer = Timer.new()
_timer.wait_time = SPAWN_INTERVAL
_timer.one_shot = false
_timer.timeout.connect(_on_timer_timeout)
add_child(_timer)
_timer.start()
func _on_timer_timeout() -> void:
if spawned_count >= MAX_SPAWNS:
_timer.stop()
return
var child := Node2D.new()
child.name = "Spawned%d" % (spawned_count + 1)
add_child(child)
spawned_count += 1
if spawned_count >= MAX_SPAWNS:
_timer.stop()
A door with states: pass
The task: Write res://door.gd for Godot 4: a script extending Node for a door with the states CLOSED, OPENING and OPEN. It starts CLOSED. A method interact() starts opening a closed door; 0.5 seconds of game time later it is OPEN. A method get_state_name() -> String returns "CLOSED", "OPENING" or "OPEN".
Passed on the first try.
extends Node
enum State { CLOSED, OPENING, OPEN }
const OPEN_DURATION := 0.5
var state: State = State.CLOSED
var _elapsed := 0.0
func interact() -> void:
if state == State.CLOSED:
state = State.OPENING
_elapsed = 0.0
func _process(delta: float) -> void:
if state == State.OPENING:
_elapsed += delta
if _elapsed >= OPEN_DURATION:
state = State.OPEN
func get_state_name() -> String:
match state:
State.CLOSED:
return "CLOSED"
State.OPENING:
return "OPENING"
_:
return "OPEN"
An inventory with counts: pass
The task: Write res://inventory.gd for Godot 4: a script extending RefCounted with add_item(item_name: String, count: int), remove_item(item_name: String, count: int) -> bool that removes only when there are enough and returns whether it did, and get_count(item_name: String) -> int that is 0 for an item never added.
Passed on the first try.
extends RefCounted
var _items: Dictionary = {}
func add_item(item_name: String, count: int) -> void:
if count <= 0:
return
_items[item_name] = get_count(item_name) + count
func remove_item(item_name: String, count: int) -> bool:
if count <= 0:
return false
var current := get_count(item_name)
if current < count:
return false
var remaining := current - count
if remaining == 0:
_items.erase(item_name)
else:
_items[item_name] = remaining
return true
func get_count(item_name: String) -> int:
return int(_items.get(item_name, 0))
Load a level scene from a path: pass
The task: Write res://level_loader.gd for Godot 4: a script extending Node with a method load_level(path: String) -> Node that loads the scene file at path, creates an instance of it, adds it as a child, and returns it.
Passed on the first try.
extends Node
func load_level(path: String) -> Node:
var scene := load(path) as PackedScene
if scene == null:
push_error("Could not load scene: %s" % path)
return null
var level := scene.instantiate()
add_child(level)
return level
Paint cells on a TileMapLayer: pass
The task: Write res://grid_painter.gd for Godot 4.7: a script extending TileMapLayer with paint_row(y: int, from_x: int, to_x: int) that sets the cells from (from_x, y) to (to_x, y) inclusive to tile source 0, atlas coordinates (0, 0), and is_painted(cell: Vector2i) -> bool that says whether a cell has a tile.
Passed on the first try.
extends TileMapLayer
func paint_row(y: int, from_x: int, to_x: int) -> void:
for x in range(mini(from_x, to_x), maxi(from_x, to_x) + 1):
set_cell(Vector2i(x, y), 0, Vector2i(0, 0))
func is_painted(cell: Vector2i) -> bool:
return get_cell_source_id(cell) != -1
Cast a physics ray: pass
The task: Write res://line_of_sight.gd for Godot 4: a script extending Node2D with first_hit(from: Vector2, to: Vector2) -> Vector2 that casts a physics ray through the 2D world from `from` to `to` and returns the point where it first hits a collider, or `to` when it hits nothing. It is called from physics processing.
Passed on the first try.
extends Node2D
## Casts a ray from `from` to `to` (global coordinates) and returns the first
## collision point, or `to` if nothing is hit. Call from _physics_process.
func first_hit(from: Vector2, to: Vector2) -> Vector2:
var space_state := get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(from, to)
var result := space_state.intersect_ray(query)
if result.is_empty():
return to
return result["position"]
A path on a grid around walls: pass
The task: Write res://path_finder.gd for Godot 4: a script extending RefCounted with find_path(size: Vector2i, walls: Array[Vector2i], from: Vector2i, to: Vector2i) -> Array[Vector2i] that returns the shortest path of grid cells from `from` to `to`, both included, on a grid of `size` cells, never entering a wall cell and moving only up, down, left or right.
Passed on the first try.
extends RefCounted
const DIRECTIONS: Array[Vector2i] = [
Vector2i(0, -1),
Vector2i(0, 1),
Vector2i(-1, 0),
Vector2i(1, 0),
]
## Returns the shortest 4-directional path from `from` to `to` (both included),
## avoiding wall cells. Returns an empty array if no path exists.
func find_path(size: Vector2i, walls: Array[Vector2i], from: Vector2i, to: Vector2i) -> Array[Vector2i]:
var result: Array[Vector2i] = []
if size.x <= 0 or size.y <= 0:
return result
if not _in_bounds(size, from) or not _in_bounds(size, to):
return result
var cell_count := size.x * size.y
var blocked := PackedByteArray()
blocked.resize(cell_count)
for wall in walls:
if _in_bounds(size, wall):
blocked[wall.y * size.x + wall.x] = 1
var start_index := from.y * size.x + from.x
var goal_index := to.y * size.x + to.x
if blocked[start_index] == 1 or blocked[goal_index] == 1:
return result
if start_index == goal_index:
result.append(from)
return result
var came_from := PackedInt32Array()
came_from.resize(cell_count)
came_from.fill(-1)
came_from[start_index] = start_index
var queue := PackedInt32Array()
queue.append(start_index)
var head := 0
var found := false
while head < queue.size():
var current := queue[head]
head += 1
var cell := Vector2i(current % size.x, current / size.x)
for dir in DIRECTIONS:
var next := cell + dir
if not _in_bounds(size, next):
continue
var next_index := next.y * size.x + next.x
if blocked[next_index] == 1 or came_from[next_index] != -1:
continue
came_from[next_index] = current
if next_index == goal_index:
found = true
break
queue.append(next_index)
if found:
break
if not found:
return result
var index := goal_index
while index != start_index:
result.append(Vector2i(index % size.x, index / size.x))
index = came_from[index]
result.append(from)
result.reverse()
return result
func _in_bounds(size: Vector2i, cell: Vector2i) -> bool:
return cell.x >= 0 and cell.y >= 0 and cell.x < size.x and cell.y < size.y
Build and play an animation in code: pass
The task: Write res://blinker.gd for Godot 4: a script extending Node2D that, when it enters the scene, creates an AnimationPlayer child and a looping animation named "blink", 1 second long, that keeps the node visible for the first half of each second and hidden for the second half, and plays it.
Passed on the first try.
extends Node2D
func _ready() -> void:
var player := AnimationPlayer.new()
player.name = "AnimationPlayer"
add_child(player)
var anim := Animation.new()
anim.length = 1.0
anim.loop_mode = Animation.LOOP_LINEAR
var track := anim.add_track(Animation.TYPE_VALUE)
# The player's root_node defaults to "..", which is this node.
anim.track_set_path(track, NodePath(".:visible"))
anim.value_track_set_update_mode(track, Animation.UPDATE_DISCRETE)
anim.track_insert_key(track, 0.0, true)
anim.track_insert_key(track, 0.5, false)
var library := AnimationLibrary.new()
library.add_animation("blink", anim)
player.add_animation_library("", library)
player.play("blink")
Wait for a signal or a timeout: pass
The task: Write res://waiter.gd for Godot 4: a script extending Node with wait_for(sig: Signal, seconds: float) -> bool, a coroutine that waits until the signal is emitted or `seconds` of game time pass, whichever comes first, and returns true if the signal came first. The signal has no arguments.
Passed on the first try.
extends Node
## Resolves once, with whichever outcome arrives first.
class _Race extends RefCounted:
signal finished(result: bool)
var done := false
func resolve(result: bool) -> void:
if done:
return
done = true
finished.emit(result)
## Waits until `sig` is emitted or `seconds` of game time pass.
## Returns true if the signal came first, false on timeout.
func wait_for(sig: Signal, seconds: float) -> bool:
var race := _Race.new()
var on_signal := func() -> void: race.resolve(true)
sig.connect(on_signal, CONNECT_ONE_SHOT)
var tree := get_tree() if is_inside_tree() else Engine.get_main_loop() as SceneTree
var timer := tree.create_timer(maxf(seconds, 0.0))
timer.timeout.connect(func() -> void: race.resolve(false))
var result: bool = await race.finished
if is_instance_valid(sig.get_object()) and sig.is_connected(on_signal):
sig.disconnect(on_signal)
return result
Questions
- Is Claude Opus 5.5 good at Godot?
- On these 15 tasks it passed 15 on the first try and 15 after one fix. Each task is small and checked by running it, so the score says whether its Godot 4 code works, not how well it designs a game.
Related
- Claude Sonnet 5.5 on the same tasks
- GPT-6.1 Sol on the same tasks
- Claude Haiku 4.5 on the same tasks
- Every verified Godot 4 answer
- Invalid access to property or key on a base object of type 'previously freed' in Godot 4
- Invalid type in static function '…' in type '…'. Cannot convert argument in Godot 4
- Trying to assign a non-object value to a variable of type '…' in Godot 4
- The best AI for GDScript: what good output looks like
- Godot 4.6 to 4.7: every API change, class by class
Flockbay runs your own agent, Claude or ChatGPT on the official Godot 4.7.2, and gives it Godot's errors the way this test does.
Download Flockbay free