Game Development No. 449Editor's pick

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.

  • namegodot-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.

~/.claude/skills/godot-games/SKILL.md

--- name: godot-games description: Builds and fixes Godot 4 games with statically typed GDScript, scene composition, signals, custom Resources, CharacterBody and RigidBody… license: MIT ---

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, .uid files, built-in Jolt). Check config/features in project.godot before using newer APIs; never write Godot 3 syntax (export var, onready var, yield, KinematicBody2D, connect("sig", self, "m")).
  • Type everything: var speed: float = 200.0 or var 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 .tscn you 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

NeedUse
Behavior of one objectScript 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 listenersSignal on an autoload event bus, only for truly global events
Many objects of one kind to address togetherGroups: 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:

  • @onready or _ready() for node lookups; never get_node() inside _process.
  • Use StringName literals (&"jump") for actions, groups, and signal names on hot paths.
  • await get_tree().create_timer(0.5).timeout for one-off delays; a Timer node for repeating ones. Check is_instance_valid(self) after an await if the node may be freed.
  • queue_free(), never free(), for nodes in the active tree.
  • preload() for small always-needed assets with constant paths; load() or ResourceLoader.load_threaded_request() for large or conditional ones.
  • Turn on Project Settings > Debug > GDScript > untyped_declaration as 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

ObjectNodeMove it by
Player/NPC with game-defined movementCharacterBody2D/3DSet velocity, call move_and_slide() in _physics_process
Thrown crate, ragdoll, debrisRigidBody2D/3DForces and impulses (apply_central_impulse), never position =
Moving platform, doorAnimatableBody2D/3DAnimate transform; sync_to_physics on
Trigger zone, hitbox, pickupArea2D/3DSignals body_entered, area_entered
Static level geometryStaticBody2D/3D or TileMapLayer collisionNever 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 (RayCast2D or PhysicsDirectSpaceState2D.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. Use get_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/.res or bytes_to_var_with_objects data from untrusted sources (shared saves, mods downloaded at runtime): resources can embed scripts that execute.
  • Shared Resources are shared: modifying stats.max_health on one enemy changes all of them. Call stats = stats.duplicate() in _ready() or tick “Local to Scene”.

Project hygiene

  • .gitignore: .godot/, *.translation build outputs, export folders. Commit *.import and *.uid files; deleting .uid files 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.cfg must 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 preload them.

Gotchas

  • velocity * delta passed to move_and_slide() double-applies delta; the property is already per-second.
  • $Node paths break silently when a node is renamed; prefer %UniqueName and typed @onready vars so the error shows at scene load.
  • Freed objects: storing a node reference and using it after queue_free() errors; check is_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 == 2 in GDScript; use 5 / 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

  1. 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.
  2. Exercise the change: input actions, collisions on each relevant layer, scene reload.
  3. Headless test suite passes (GUT: godot --headless -s addons/gut/gut_cmdln.gd).
  4. For build-affecting changes, a headless --export-release succeeds and the exported build launches and loads a save from user://.

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.

esc