skills/catalog/godot/godot-core/SKILL.md
Use when writing GDScript, creating scenes, adding nodes, or doing day-to-day Godot 4.x development. Covers project structure, script patterns, signals, resources, and MCP tools.
npx skillsauth add erikstmartin/dotfiles godot-coreInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Standard folder layout — organize by feature, not by class type:
project/
├── project.godot
├── features/
│ ├── player/ # scene + script + resources + tests together
│ ├── combat/
│ └── ui/
├── assets/
│ ├── sprites/
│ ├── audio/
│ └── shaders/
└── resources/ # shared .tres files
MCP Tools — use these to act instead of explaining manually:
| Tool | When |
|------|------|
| mcp__godot__launch_editor | Open project in editor |
| mcp__godot__run_project | Run the game |
| mcp__godot__get_debug_output | Read console errors |
| mcp__godot__stop_project | Stop running game |
| mcp__godot__create_scene | Create a new .tscn |
| mcp__godot__add_node | Add node to scene |
| mcp__godot__load_sprite | Load texture into Sprite2D |
| mcp__godot__save_scene | Persist scene changes |
| mcp__godot__get_uid | Get file UID (4.4+) |
| mcp__godot__list_projects | Find projects in directory |
Common task — create a 2D character:
mcp__godot__create_scene with CharacterBody2D rootmcp__godot__add_node → Sprite2D childmcp__godot__add_node → CollisionShape2D child_physics_processEvery script follows this layout order — never deviate:
class_name PlayerCharacter # NEVER skip — required for type hints elsewhere
extends CharacterBody2D
# 1. Signals (typed parameters always)
signal health_changed(new_health: int)
signal died
# 2. Enums / Constants
enum State { IDLE, RUN, JUMP, DEAD }
const MAX_SPEED: float = 600.0
# 3. @export variables
@export var speed: float = 200.0
@export_group("Combat")
@export var attack_damage: int = 10
@export var weapon: WeaponData # custom Resource type
# 4. @onready references (cache everything — never $ in _process)
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim: AnimationPlayer = $AnimationPlayer
@onready var health_bar: ProgressBar = %HealthBar # scene-unique name
# 5. Public variables
var current_state: State = State.IDLE
# 6. Private variables (_prefix by convention)
var _health: int
var _can_attack: bool = true
func _ready() -> void:
_health = 100
func _physics_process(delta: float) -> void:
var dir := Input.get_vector("left", "right", "up", "down")
velocity = dir * speed
move_and_slide()
func take_damage(amount: int) -> void:
_health = max(_health - amount, 0)
health_changed.emit(_health)
if _health <= 0:
died.emit()
func _apply_knockback(direction: Vector2) -> void:
velocity += direction * 300.0
Type system — static type everything:
var speed: float = 300.0 # explicit
var direction := Vector2.ZERO # inferred (:= when type is obvious)
var enemies: Array[Enemy] = [] # typed arrays
func calculate_damage(base: int, multiplier: float) -> int:
return int(base * multiplier)
⚠️ Untyped code is significantly slower — the GDScript compiler only optimises typed paths.
func _ready() -> void:
health_component.died.connect(_on_died)
health_component.health_changed.connect(_on_health_changed)
# One-shot: auto-disconnects after first fire
get_tree().create_timer(3.0).timeout.connect(_on_timer, CONNECT_ONE_SHOT)
func _exit_tree() -> void:
# Always disconnect manually-connected signals
if health_component.died.is_connected(_on_died):
health_component.died.disconnect(_on_died)
⚠️ Pitfall: connect("signal_name", target, "method") is Godot 3 syntax — it compiles silently in Godot 4 but does nothing. Always use signal_name.connect(callable).
Use Resource subclasses for all designer-editable data. Never embed data tables in scripts.
# weapon_data.gd
class_name WeaponData
extends Resource
@export var damage: int
@export var attack_speed: float
@export var icon: Texture2D
@export var sound_attack: AudioStream
# character.gd — consuming the resource
@export var base_stats: CharacterStats
var stats: CharacterStats
func _ready() -> void:
# MANDATORY: duplicate() to prevent shared-resource bug
stats = base_stats.duplicate() as CharacterStats
stats.stat_changed.connect(_on_stat_changed)
⚠️ Pitfall: Multiple scene instances share the same Resource object by default. Call duplicate() in _ready() or enable "Local to Scene" in the Inspector. This is the #1 Godot 4 newcomer bug.
anim.play(&"run") # O(1) pointer compare vs O(n) string compare
stats[&"health"] = 100
if group == &"enemies": pass
Use &"name" (StringName literal) anywhere the value is a key, animation name, or repeated identifier in _process.
func _load_level_async(path: String) -> void:
ResourceLoader.load_threaded_request(path)
while ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
await get_tree().process_frame
var scene: PackedScene = ResourceLoader.load_threaded_get(path)
add_child(scene.instantiate())
Use preload() at script top for small assets known at edit-time. Use ResourceLoader.load_threaded_request() for levels and large assets.
var x = 5 is slower; var x: int = 5 enables compiler optimisation.get_node() or $ inside _process() — cache all node refs in @onready.await inside _physics_process() — yields cause physics frames to be skipped; move async ops to a signal-triggered method.String keys in hot-path dictionaries — use StringName (&"key") for O(1) pointer comparison.dict["key"] — use dict.get("key", default) to avoid crashes on missing keys.class_name — without it you can't use the script as a type hint elsewhere in the project.load() inside _process or a loop — synchronous disk read blocks the main thread; use preload() or load_threaded_request().Callable refs to freed objects — disconnect signals in _exit_tree() or use CONNECT_ONE_SHOT.@export Resource without duplicate() in _ready() — shared Resource across instances is the #1 Godot 4 newcomer bug.connect("name", target, "method") compiles but does nothing in Godot 4.move_and_slide() API change: velocity is now a property. Set velocity = dir * speed before calling move_and_slide() — do not pass it as an argument.@onready timing: runs during _ready(), after _init(). Never mix constructor-time setup with @onready; use _init() for pre-tree setup.Tween is not a Node: created via create_tween(), bound to the creating node's lifetime. Use get_tree().create_tween() for tweens that must outlive the node.collision_layer = "what I am"; collision_mask = "what I detect". Setting both to the same value causes self-collision or missed detections.testing
Use when creating new skills, editing existing skills, or verifying skills work before deployment
development
Use when you have a spec or requirements for a multi-step task, before touching code
data-ai
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
tools
Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions