Godot 4 GDScript skill for Codex
Typed scripts, small scenes, signals up. Codex reads project.godot for the engine version, autoloads and input map, then writes statically typed GDScript that follows call-down, signal-up communication, keeps tuning data in Resource files and avoids brittle node paths. It converts Godot 3 idioms such as yield and string connects, guards awaits against freed nodes, and parses every script headless before handing back.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- godot-gdscript Installs to ~/.agents/skills/godot-gdscript/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install godot-gdscript The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project godot-gdscript Installs to .agents/skills/godot-gdscript/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/godot-gdscript.zipunzip -o godot-gdscript.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) godot-gdscript PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $godot-gdscript, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes typed GDScript for Godot 4 - scene composition, nodes and scripts, signals with callables, @export and @onready, Resources for data, autoloads, CharacterBody2D/3D movement, tweens and await - and checks scripts headless from the CLI. Use when building or refactoring a Godot 4 game in GDScript, porting Godot 3 code (yield, connect strings, KinematicBody), wiring signals between scenes, structuring player, enemy and level scenes, or fixing node path and null instance errors.
Use it when
- Building gameplay in a Godot 4 project with GDScript
- Porting Godot 3.x scripts that use yield, KinematicBody or string-based connect
- Scenes are coupled through get_parent chains or absolute node paths
- Moving hard-coded stats into reusable Resource files
Use something else when
- The project uses C# scripts: use godot-csharp
- You are writing a Godot shader rather than a script: use glsl-shaders
How to check the result
Run godot --headless --check-only on each changed script, then run the scene and confirm the Debugger Errors tab stays empty.
The mistake it prevents
Mutating a shared Resource at runtime, which changes the value for every node that loaded it, instead of duplicating it first.
Done well means: scenes are small and reusable, scripts are statically typed, nodes talk
to children by direct reference and to parents or siblings by signals (“call down, signal
up”), data lives in Resource files, and every script parses cleanly with the headless
checker.
Before writing code
- Read
AGENTS.md, thenproject.godot:config/featuresshows the engine version (PackedStringArray("4.4", ...)),[autoload]lists singletons,[input]lists actions. - Note the version. 4.4 added typed dictionaries and
.uidfiles; 4.5 added@abstract. Do not use a feature newer than the project. - Look at existing folder conventions (
scenes/,scripts/or feature folders) and follow them. - Commit
.uidfiles next to scripts and shaders (Godot 4.4+). Never edit.importfiles or the.godot/folder;.godot/is a cache and belongs in.gitignore. .tscnand.tresare text, but editing them by hand risks brokenext_resourceids. Prefer changing scenes through scripts or ask the user to make the editor change; if you must edit, change only property values and re-open the scene to verify.
Typed GDScript
Turn on warnings so untyped code is visible: Project Settings > Debug > GDScript, set
untyped_declaration to Warn (or Error for new projects).
class_name Player
extends CharacterBody2D
signal health_changed(current: int, maximum: int)
signal died
@export var max_speed: float = 220.0
@export var acceleration: float = 1400.0
@export var jump_velocity: float = -380.0
@export var stats: PlayerStats # a custom Resource, assigned in the Inspector
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var hurtbox: Area2D = %Hurtbox # scene-unique name, survives reparenting
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 # 4.3+; earlier: read physics/2d/default_gravity
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 * max_speed, acceleration * delta)
move_and_slide()
if dir != 0.0:
sprite.flip_h = dir < 0.0
func take_damage(amount: int) -> void:
health = maxi(health - amount, 0)
health_changed.emit(health, stats.max_health)
if health == 0:
died.emit()
func _on_hurtbox_area_entered(area: Area2D) -> void:
if area is Hitbox:
take_damage((area as Hitbox).damage)
:=infers the type; use it when the right side has a known type. Explicit: floatwhen it does not (Input.get_axisreturns float, so:=works there).- Typed code is faster at runtime (typed instructions) and catches errors at parse time.
class_nameregisters a global type. Use it for types other scripts check withisor reference as types; skip it for one-off scene scripts to keep the global namespace small.
Godot 3 to 4 changes that trip ports
| Godot 3 | Godot 4 |
|---|---|
yield(obj, "signal") | await obj.signal |
connect("pressed", self, "_on_pressed") | pressed.connect(_on_pressed) |
emit_signal("died") | died.emit() |
export var, onready var | @export var, @onready var |
KinematicBody2D, move_and_slide(vel) | CharacterBody2D, set velocity, call move_and_slide() |
tool | @tool |
instance() | instantiate() |
deg2rad, rand_range | deg_to_rad, randf_range |
$Tween node | create_tween() (not a node) |
Communication rules
| Direction | Mechanism |
|---|---|
| Parent to child | Direct call through an @onready or @export reference |
| Child to parent / sibling | Signal; the parent connects it |
| Unrelated systems (achievements, audio, UI) | Autoload event bus with signals, or groups |
| Many nodes of a kind | add_to_group("enemies") and get_tree().call_group(...) |
- Avoid
get_parent().get_parent()and long absolute paths like/root/Main/World/Player; they break the moment a scene is reused. @export var target: Node2Dlets the level designer wire references in the Inspector.- Connect with
CONNECT_ONE_SHOTfor single-fire waits, and bind extra arguments withcallable.bind(value)instead of lambdas that capture loop variables.
Data with Resources
class_name EnemyStats
extends Resource
@export var display_name: String = "Slime"
@export_range(1, 999) var max_health: int = 20
@export var speed: float = 60.0
@export var loot: Array[ItemData] = []
Save as res://data/enemies/slime.tres. Loaded Resources are shared: modifying one at
runtime changes it for every user. Call stats.duplicate() (or enable
resource_local_to_scene) before mutating per-instance values.
Scenes and instancing
const BULLET := preload("res://scenes/bullet.tscn")
func fire(dir: Vector2) -> void:
var b := BULLET.instantiate() as Bullet
b.global_position = muzzle.global_position
b.direction = dir
get_tree().current_scene.add_child(b) # not as a child of the moving shooter
preloadfor small always-needed assets;ResourceLoader.load_threaded_requestfor levels.- Free with
queue_free(), neverfree()during physics callbacks or signal emission. - Changing collision shapes or adding bodies inside a physics callback: use
call_deferredorset_deferred("disabled", true).
Await, timers and tweens
func flash() -> void:
var tw := create_tween()
tw.tween_property(sprite, "modulate", Color.RED, 0.05)
tw.tween_property(sprite, "modulate", Color.WHITE, 0.15)
await tw.finished
func respawn_later() -> void:
await get_tree().create_timer(2.0).timeout
if not is_inside_tree(): # node may have been freed or removed while waiting
return
respawn()
Gotchas
@onreadyvalues are null before_ready. Setters on@exportvars run before the node enters the tree; guard withif is_node_ready().- Frame-rate dependent movement: always multiply by
delta, and do physics in_physics_process, not_process. move_and_slide()uses thevelocityproperty and already accounts for delta; do not multiply velocity by delta before calling it.- Integer division:
5 / 2is2. Use5 / 2.0orfloat(). - Input actions must exist in Project Settings > Input Map; a typo returns 0 silently and prints an error only at runtime.
is_instance_valid(node)before using references held across frames to nodes that might have been freed.
Verification
godot --headless --check-only --script res://scripts/player.gd # parse one script
godot --headless --import # 4.3+: import assets, then quit
- Run the project or scene from the editor and watch the Debugger > Errors tab: it must be empty.
- For logic tests use GUT or gdUnit4 if the project already has one (check
addons/); both run headless from the CLI. Do not add a test addon without asking. - If the Godot binary is not on PATH (
godot,godot4, or aGodot.appbundle), say so and give the user the exact commands to run.
Hand-off summary
**Scenes:** player.tscn (new Hurtbox Area2D with unique name), bullet.tscn
**Scripts:** player.gd (typed, signals health_changed/died), enemy_stats.gd (Resource)
**Data:** res://data/enemies/slime.tres, bat.tres
**Wiring needed in editor:** HUD connects Player.health_changed (done in hud.gd _ready)
**Checks:** --check-only clean on 4 scripts; ran level_1, no debugger errors What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Godot 4 GDScript"
short_description: "Typed scripts, small scenes, signals up"
default_prompt: "$godot-gdscript Write a platformer player in Godot 4 with coyote time and a health signal" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.