Godot Games skill for Claude Code
Typed GDScript, clean scenes, exports that build. Claude writes Godot 4 code the way experienced Godot developers do: fully typed GDScript, scenes that run in isolation, signals flowing upward, and tunable data in custom Resources. It picks the right physics body for each object, avoids Godot 3 syntax, keeps saves in user://, and verifies changes with headless imports, tests, and command-line exports.
- name
godot-games - size231 lines · ≈2,700 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds and fixes Godot 4 games with statically typed GDScript, scene composition, signals, custom Resources, CharacterBody and RigidBody physics, input actions, save files, and headless exports for desktop, web, and mobile. Use when editing .gd, .tscn, .tres, or project.godot files, when a node path or signal breaks, a character jitters or falls through floors, an export preset fails, or when structuring a Godot project that will grow past a prototype. Try asking
- My CharacterBody2D jitters on slopes and sometimes falls through the floor, fix it
- Refactor this Godot player script into health and hitbox components with signals
- Set up headless Godot exports for Windows, Linux and Web in GitHub Actions
Use it when
- Editing .gd, .tscn, .tres or project.godot files
- Structuring a Godot project that is outgrowing a prototype
- Character movement, collisions or signals misbehave
- An export preset or CI build fails
Skip it when
- Engine-agnostic physics bugs like tunneling or timestep math — use game-physics
- Frame-time spikes and profiling across engines — use game-performance
How to check the result
The edited scene runs alone with an empty Errors tab, the headless test suite passes, and a command-line export produces a build that launches.
The common mistake it prevents
Multiplying velocity by delta before move_and_slide(), which applies delta twice and makes movement frame-rate dependent and sluggish.
Done well means: every script is statically typed, each scene runs on its own (F6) without the rest of the game, nodes talk “call down, signal up”, data lives in Resources rather than hard-coded constants, and a headless export from the command line succeeds.
Ground rules
- Target Godot 4.x (4.4+ assumed: typed dictionaries,
.uidfiles, built-in Jolt). Checkconfig/featuresinproject.godotbefore using newer APIs; never write Godot 3 syntax (export var,onready var,yield,KinematicBody2D,connect("sig", self, "m")). - Type everything:
var speed: float = 200.0orvar speed := 200.0, typed function signatures,Array[Enemy],Dictionary[StringName, int]. Typed code runs faster and catches broken node references at parse time. - One scene = one responsibility. A scene must not reach into its parent with
get_parent()or../..paths. Parents call methods on children; children emit signals upward. - Before editing, read
project.godot(autoloads, input map, physics layer names, main scene) and the scene tree of the.tscnyou will touch. - Never hand-edit
.tscn[ext_resource]ids or uids unless you are fixing a known merge conflict; prefer making structural changes through code or describing editor steps.
Workflow
- [ ] 1. Read project.godot: version features, autoloads, input actions, layer names
- [ ] 2. Map the scene tree you are changing (root type, children, attached scripts)
- [ ] 3. Decide where state lives (node, Resource, autoload) using the table below
- [ ] 4. Write typed GDScript; wire signals in code or note editor connections explicitly
- [ ] 5. Run the scene alone, then the main scene; read the Debugger > Errors tab
- [ ] 6. Run headless checks/tests; export if the change affects builds
Where state and logic belong
| Need | Use |
|---|---|
| Behavior of one object | Script on that scene’s root node |
| Tunable data shared by many instances (stats, item defs) | Custom Resource with class_name, saved as .tres |
| Cross-cutting service (audio bus, save system, scene loader) | Autoload, kept small and stateless where possible |
| Broadcast “something happened” to unknown listeners | Signal on an autoload event bus, only for truly global events |
| Many objects of one kind to address together | Groups: add_to_group(&"enemies"), get_tree().call_group(...) |
| Reusable capability (health, hitbox) | Child component node with its own script and signals |
Prefer composition by child nodes over deep extends chains; inheritance beyond two levels
of custom scripts becomes hard to reason about in the inspector.
GDScript patterns
class_name Player
extends CharacterBody2D
signal health_changed(current: int, max_value: int)
signal died
@export var stats: PlayerStats # custom Resource
@export_range(0.0, 1000.0, 10.0) var speed: float = 220.0
@export var jump_velocity: float = -380.0
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var hurtbox: Area2D = %Hurtbox # unique name, survives tree reshuffles
var _health: int
func _ready() -> void:
_health = stats.max_health
hurtbox.area_entered.connect(_on_hurtbox_area_entered)
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
if Input.is_action_just_pressed(&"jump") and is_on_floor():
velocity.y = jump_velocity
var dir := Input.get_axis(&"move_left", &"move_right")
velocity.x = move_toward(velocity.x, dir * speed, speed * 8.0 * delta)
move_and_slide() # uses velocity and delta internally
func take_damage(amount: int) -> void:
_health = maxi(_health - amount, 0)
health_changed.emit(_health, stats.max_health)
if _health == 0:
died.emit()
set_physics_process(false)
func _on_hurtbox_area_entered(area: Area2D) -> void:
if area is Hitbox:
take_damage((area as Hitbox).damage)
# player_stats.gd
class_name PlayerStats
extends Resource
@export var max_health: int = 100
@export var move_speed: float = 220.0
Rules worth enforcing:
@onreadyor_ready()for node lookups; neverget_node()inside_process.- Use
StringNameliterals (&"jump") for actions, groups, and signal names on hot paths. await get_tree().create_timer(0.5).timeoutfor one-off delays; aTimernode for repeating ones. Checkis_instance_valid(self)after an await if the node may be freed.queue_free(), neverfree(), for nodes in the active tree.preload()for small always-needed assets with constant paths;load()orResourceLoader.load_threaded_request()for large or conditional ones.- Turn on Project Settings > Debug > GDScript >
untyped_declarationas a warning or error so untyped code gets flagged.
Signals
- Connect in code with callables:
button.pressed.connect(_on_pressed). Bind extra args with_on_slot_pressed.bind(index). - Emit with
died.emit(); declare parameters with types so connections are checked. - Disconnect is automatic when either node is freed; one-shot listeners use
sig.connect(cb, CONNECT_ONE_SHOT). - If a connection lives in the editor (green icon in the Node dock), say so in your summary; code-only readers will not see it.
- Event bus autoload only for events with no natural owner (achievement unlocked, settings changed). Overusing it recreates global spaghetti.
Physics decisions
| Object | Node | Move it by |
|---|---|---|
| Player/NPC with game-defined movement | CharacterBody2D/3D | Set velocity, call move_and_slide() in _physics_process |
| Thrown crate, ragdoll, debris | RigidBody2D/3D | Forces and impulses (apply_central_impulse), never position = |
| Moving platform, door | AnimatableBody2D/3D | Animate transform; sync_to_physics on |
| Trigger zone, hitbox, pickup | Area2D/3D | Signals body_entered, area_entered |
| Static level geometry | StaticBody2D/3D or TileMapLayer collision | Never moved |
- Collision layer = “what I am”, mask = “what I scan for”. Name layers in Project Settings > Layer Names and refer to them consistently.
- Gameplay movement goes in
_physics_process; visuals and UI in_process. - Enable physics interpolation (Project Settings > Physics > Common) when the display
refresh rate exceeds the tick rate and motion looks stuttery; call
reset_physics_interpolation()after teleports. - Fast bullets tunnel through thin walls: use raycasts (
RayCast2DorPhysicsDirectSpaceState2D.intersect_ray) or enable continuous CD on the body. - For a RigidBody that needs teleporting, set state inside
_integrate_forces(state).
Input
- Define actions in the Input Map; never hard-code key constants in gameplay code.
- Gameplay input in
_unhandled_input(event)or polled in_physics_process, so UI controls consume clicks first. Useget_viewport().set_input_as_handled()when done. Input.get_vector(&"left", &"right", &"up", &"down")returns a normalized, deadzoned Vector2 for analog and keyboard alike.
Scenes, saving, and loading
- Change scenes with
get_tree().change_scene_to_file("res://levels/level_2.tscn")or a loader autoload that shows a loading screen and uses threaded loading. - Save to
user://only (res://is read-only in exports):
func save_game(data: Dictionary) -> Error:
var f := FileAccess.open("user://save.json", FileAccess.WRITE)
if f == null:
return FileAccess.get_open_error()
f.store_string(JSON.stringify(data))
return OK
- JSON converts ints to floats on parse; cast on load (
int(d["level"])). - Never load
.tres/.resorbytes_to_var_with_objectsdata from untrusted sources (shared saves, mods downloaded at runtime): resources can embed scripts that execute. - Shared Resources are shared: modifying
stats.max_healthon one enemy changes all of them. Callstats = stats.duplicate()in_ready()or tick “Local to Scene”.
Project hygiene
.gitignore:.godot/,*.translationbuild outputs, export folders. Commit*.importand*.uidfiles; deleting.uidfiles breaks references across scenes.- Rename or move files inside the editor FileSystem dock so references update; moving files in the OS file browser breaks scenes.
- Folder by feature (
player/,enemies/slime/) rather than by type (scripts/,sprites/). - Tests: GUT or gdUnit4, run headless in CI.
Exporting
# Import assets once on a fresh checkout or CI runner
godot --headless --import
# Export using a preset name from export_presets.cfg
godot --headless --export-release "Linux" build/linux/game.x86_64
godot --headless --export-release "Web" build/web/index.html
godot --headless --export-debug "Android" build/android/game.apk
- Export templates matching the exact engine version must be installed first.
export_presets.cfgmust be committed; keep signing keys and keystore passwords out of it (use environment variables or CI secrets).- Web: threaded builds need cross-origin isolation headers
(
Cross-Origin-Opener-Policy: same-origin,Cross-Origin-Embedder-Policy: require-corp); use the non-threaded option for hosts like itch.io unless you control headers. - C# (.NET) projects have narrower platform support than GDScript; check the current docs before promising a web export for a C# project.
- Resources loaded only by string path at runtime may be stripped; include them via the
preset’s resource filters or
preloadthem.
Gotchas
velocity * deltapassed tomove_and_slide()double-applies delta; the property is already per-second.$Nodepaths break silently when a node is renamed; prefer%UniqueNameand typed@onreadyvars so the error shows at scene load.- Freed objects: storing a node reference and using it after
queue_free()errors; checkis_instance_valid(node). _ready()runs children first, then the parent. A child reading parent state in its own_ready()sees it uninitialized; have the parent push data down.- Integer division:
5 / 2 == 2in GDScript; use5 / 2.0. - Tweens created with
create_tween()are bound to the node and die with it; kill a running tween before starting another on the same property.
Verification
- Run the edited scene alone (F6 or
godot --path . res://path/scene.tscn) and the main scene; the Debugger Errors tab is empty and no “untyped” warnings were added. - Exercise the change: input actions, collisions on each relevant layer, scene reload.
- Headless test suite passes (GUT:
godot --headless -s addons/gut/gut_cmdln.gd). - For build-affecting changes, a headless
--export-releasesucceeds and the exported build launches and loads a save fromuser://.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.