Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when writing GDScript — static typing, await/coroutines, lambdas, match patterns, export annotations, inner classes, and common idioms
.claude/skills/jame581-gdscript-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 584% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 216% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 120% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 223% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 211% | 0% |
All examples target Godot 4.3+ with no deprecated APIs.
> Related skills: gdscript-advanced for production-grade depth (performance idioms, metaprogramming, @tool lifecycle, profiler-driven idioms), godot-code-review for style rules and anti-patterns, csharp-godot for GDScript-to-C# translation, state-machine for state patterns, event-bus for signal architecture.
> Note: This skill is GDScript-specific by design. For C# patterns, see csharp-godot and csharp-signals.
Always add type hints — they catch bugs at parse time, improve autocomplete, and boost performance.
gdscript# Variables var health: int = 100 var speed: float = 200.0 var player_name: String = "Hero" var direction: Vector2 = Vector2.ZERO # Constants const MAX_HEALTH: int = 100 const GRAVITY: float = 980.0 # Functions — parameters and return type func take_damage(amount: int) -> void: health -= amount func get_direction() -> Vector2: return Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") # Inferred typing with := var pos := Vector2(100, 200) # inferred as Vector2 var items := [] # inferred as Array (untyped) var count := 0 # inferred as int
gdscript# Typed arrays — only accepts the specified type var enemies: Array[Enemy] = [] var scores: Array[int] = [10, 20, 30] var names: Array[String] = ["Alice", "Bob"] # Typed dictionaries (Godot 4.4+) var inventory: Dictionary[String, int] = {"sword": 1, "potion": 5} # Typed loop variable for enemy: Enemy in enemies: enemy.take_damage(10) # Typed array methods work with type safety var filtered: Array[Enemy] = enemies.filter(func(e: Enemy) -> bool: return e.health > 0)
as and isgdscript# 'is' — type check (returns bool) func _on_body_entered(body: Node2D) -> void: if body is Player: var player: Player = body as Player player.take_damage(10) # 'as' — cast (returns null on failure, no error) var sprite := get_node("Sprite") as Sprite2D if sprite: sprite.modulate = Color.RED # Prefer 'is' check + cast over bare 'as' to avoid null surprises
In Project > Project Settings > Debug > GDScript:
| Warning | Effect | |-------------------------|---------------------------------------------| | UNTYPED_DECLARATION | Warns on any untyped variable/parameter | | INFERRED_DECLARATION | Warns on := (prefers explicit types) | | UNSAFE_CAST | Warns on unsafe as casts | | UNSAFE_CALL_ARGUMENT | Warns when passing wrong type to a function |
> Set warnings to Error for strict enforcement in team projects.
> ⚠️ Changed in Godot 4.7: Methods that override a method with a typed return now inherit the return type, so an override without an explicit return statement becomes an error. Add return null (or a typed return value) at the end of the override. See the 4.7 migration guide.
gdscriptclass Enemy: var weapon: Node func get_weapon() -> Node: return weapon class UnarmedEnemy extends Enemy: func get_weapon(): # 4.7+: inherits -> Node from Enemy return null # explicit return now required — omitting it is an error
await pauses the function until a signal fires, then resumes. The function becomes a coroutine.
gdscriptfunc death_sequence() -> void: $AnimationPlayer.play("death") await $AnimationPlayer.animation_finished # pauses here $Sprite2D.visible = false await get_tree().create_timer(1.0).timeout # wait 1 second queue_free()
gdscript# Signal that passes data signal dialogue_choice_made(choice: int) func show_dialogue(options: Array[String]) -> int: # ... display UI ... var choice: int = await dialogue_choice_made return choice # Caller: func _on_npc_interact() -> void: var result := await show_dialogue(["Yes", "No"]) if result == 0: print("Player said yes")
gdscript# One-shot delay await get_tree().create_timer(0.5).timeout # Repeating with await (simple but blocks the function) for i in 5: do_something() await get_tree().create_timer(0.2).timeout # Non-blocking timer — use SceneTreeTimer or Tween instead get_tree().create_timer(2.0).timeout.connect(_on_delayed_action)
gdscript# DANGER: node may be freed while awaiting func unsafe_coroutine() -> void: await get_tree().create_timer(5.0).timeout position = Vector2.ZERO # crash if node was freed during wait! # SAFE: check validity after await func safe_coroutine() -> void: await get_tree().create_timer(5.0).timeout if not is_instance_valid(self): return position = Vector2.ZERO
Lambdas are inline anonymous functions, useful for callbacks, sorting, filtering.
gdscript# Single-expression lambda var double := func(x: int) -> int: return x * 2 # Multi-line lambda var greet := func(name: String) -> void: print("Hello, %s!" % name) print("Welcome!") # Calling a lambda double.call(5) # returns 10 greet.call("Player")
gdscript# Inline signal connection (one-off use) $Button.pressed.connect(func(): print("Button pressed!")) # With arguments $Timer.timeout.connect(func(): health -= 1 if health <= 0: die() ) # One-shot connection (auto-disconnects after first call) $Timer.timeout.connect(func(): print("Once!"), CONNECT_ONE_SHOT)
gdscriptvar numbers: Array[int] = [1, 2, 3, 4, 5, 6, 7, 8] # Filter — keep elements where lambda returns true var evens: Array[int] = numbers.filter(func(n: int) -> bool: return n % 2 == 0) # [2, 4, 6, 8] # Map — transform each element var doubled: Array[int] = numbers.map(func(n: int) -> int: return n * 2) # [2, 4, 6, 8, 10, 12, 14, 16] # Reduce — accumulate into single value var total: int = numbers.reduce(func(acc: int, n: int) -> int: return acc + n, 0) # 36 # Any / All var has_negative: bool = numbers.any(func(n: int) -> bool: return n < 0) var all_positive: bool = numbers.all(func(n: int) -> bool: return n > 0) # Sort with custom comparison var items: Array[Dictionary] = [{"name": "B", "value": 2}, {"name": "A", "value": 1}] items.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["value"] < b["value"])
gdscriptfunc create_counter(start: int) -> Callable: var count := start return func() -> int: count += 1 return count var counter := create_counter(0) print(counter.call()) # 1 print(counter.call()) # 2
GDScript's match is like switch but with pattern support.
gdscriptmatch state: State.IDLE: play_idle() State.RUNNING: play_run() State.JUMPING, State.FALLING: # multiple patterns play_air() _: # default (wildcard) push_warning("Unknown state: %s" % state)
gdscript# Literal patterns match value: 42: print("The answer") "hello": print("Greeting") true: print("Boolean true") # Binding pattern — captures value into a variable match command: ["move", var direction]: move(direction) ["attack", var target, var damage]: attack(target, damage) # Array pattern match input: [1, 2, 3]: print("Exact match") [1, ..]: print("Starts with 1") [var first, _, var last]: print("First: %s, Last: %s" % [first, last]) # Dictionary pattern match event: {"type": "damage", "amount": var amt}: take_damage(amt) {"type": "heal", "amount": var amt}: heal(amt) # Nested condition inside a branch match enemy_type: "boss": if health < 50: enter_rage_mode() else: normal_attack()
@export exposes a variable to the Inspector. Hint variants (@export_range, @export_enum, @export_file) constrain editor input. Use @export_group and @export_subgroup to organize. Node and Resource exports use NodePath / typed Resource references.
> See references/export-annotations.md for the full export annotation catalog (basic exports, range/hint variants, groups, node and Resource exports).
Register a script as a global class name — available everywhere without preload.
gdscript# item_data.gd class_name ItemData extends Resource @export var name: String @export var icon: Texture2D @export var value: int # Now usable anywhere: # var item: ItemData = ItemData.new() # var items: Array[ItemData] = []
gdscript# Define a class inside another script class HitResult: var damage: int var critical: bool var knockback: Vector2 func _init(dmg: int, crit: bool, kb: Vector2 = Vector2.ZERO) -> void: damage = dmg critical = crit knockback = kb # Usage func calculate_hit() -> HitResult: var crit := randf() < 0.2 var dmg := 10 * (2 if crit else 1) return HitResult.new(dmg, crit, Vector2.RIGHT * 50)
When you override a virtual method that the engine calls (_ready, _process, _input, etc.) and your parent class also implements it, call super() to chain the parent's behavior. Forgetting to call super._ready() is the most common cause of "my base class init didn't run" bugs.
> See references/super-in-virtual-methods.md for the full pattern (problem / fix), the C# base.X() equivalent, and a catalog of bugs from missing super calls.
The recurring small patterns: ternary expressions (value if cond else other), printf-style string formatting ("%s %d" % [a, b]), null/empty checks (is_instance_valid vs != null, empty Array/String checks), Dictionary access (get(key, default)), Array operations (Array.has, Array.find, Array.has_all), setget via set and get accessors.
> See references/common-idioms.md for full code examples of each idiom.
| Annotation | Purpose | |-----------------------|--------------------------------------------| | @export | Expose variable in Inspector | | @export_range | Numeric with slider | | @export_enum | Dropdown from string list | | @export_file | File path picker | | @export_dir | Directory picker | | @export_multiline | Multi-line text box | | @export_group | Group heading in Inspector | | @export_subgroup | Subgroup heading | | @export_category | Category divider | | @onready | Initialize when node enters tree, just before _ready() body runs | | @tool | Run script in editor | | @icon | Custom icon for the script | | @warning_ignore | Suppress specific warning on next line | | @static_unload | Allow static variables to be freed |
| Symptom | Cause | Fix | |---------------------------------------|----------------------------------------------|------------------------------------------------------------------| | as cast silently returns null | Type mismatch — as doesn't error | Use is check first, then cast | | Await never resumes | Signal never emitted, or node freed | Check is_instance_valid(self) after await; ensure signal fires | | Lambda captures stale variable | Loop variable captured by reference | Copy to local var before lambda: var local := i | | UNTYPED_DECLARATION warnings flood | Warning enabled but codebase isn't typed | Type incrementally; use @warning_ignore for legacy code | | Typed array rejects valid items | Item type doesn't match exactly | Ensure items match the declared type (no implicit upcasting) | | @onready is null | Accessed before _ready() runs | Never access @onready vars in _init() or variable declarations | | Match doesn't enter any branch | No matching pattern and no _: wildcard | Always add _: default branch | | class_name conflict | Two scripts with same class_name | Use unique names; check for duplicates in Project | | Export group applies to wrong vars | Group scope continues until next group | Add a new @export_group("") to end the group scope | | Parent _ready() logic doesn't run in child | Missing super() call in child's _ready() | Add super() as first line; see Section 7 | | type_exists() flagged as deprecated | Deprecated in Godot 4.7 | Use ClassDB.class_exists() instead |
> ⚠️ Changed in Godot 4.7: The global type_exists() function is deprecated — replace type_exists("Sprite2D") with ClassDB.class_exists("Sprite2D"). See GH-116899.
Godot 4.5 added trailing-argument arrays via ...args. The args are collected into an Array. Useful for printf-style helpers and flexible APIs without overloads.
> See references/variadic-functions.md for the full syntax, common patterns, and notes on when to prefer overloads.
The @abstract annotation prevents direct instantiation of a class and forces subclasses to implement any @abstract-annotated method (similar to C#'s abstract keyword).
> See references/abstract-classes.md for full base-class patterns and subclass implementation rules.
Array[Type]) are used instead of untyped Array where possibleawait calls are followed by is_instance_valid(self) checks when the node could be freedmatch statements include a _: default branch@export variables use appropriate hints (@export_range, @export_enum, etc.)@export_group organizes Inspector properties into logical sectionsclass_name is only used for scripts that need global visibilityis type check precedes as cast when the type isn't guaranteedsuper() when extending non-built-in base classes...args) used when the number of trailing arguments is open-ended (Godot 4.5+)@abstract; required methods use @abstract func (Godot 4.5+)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 4,202 | 3,607 | -14% | 1 | 1 | 0% | 722 | 4,935 | +584% | 0 | 0 | — |
case-02 | pass→pass | 10,310 | 7,573 | -27% | 1 | 1 | 0% | 1,669 | 5,395 | +223% | 0 | 0 | — |
case-03 | pass→pass | 10,143 | 7,002 | -31% | 1 | 1 | 0% | 1,756 | 5,459 | +211% | 0 | 0 | — |
case-04 | fail→pass | 10,116 | 8,545 | -16% | 1 | 1 | 0% | 1,812 | 5,733 | +216% | 0 | 0 | — |
case-05 | pass→pass | 7,649 | 4,366 | -43% | 1 | 1 | 0% | 1,265 | 5,048 | +299% | 0 | 0 | — |
case-06 | pass→pass | 5,034 | 4,526 | -10% | 1 | 1 | 0% | 898 | 5,030 | +460% | 0 | 0 | — |
case-07 | pass→pass | 6,979 | 5,205 | -25% | 1 | 1 | 0% | 1,141 | 5,110 | +348% | 0 | 0 | — |
case-08 | fail→pass | 14,081 | 5,527 | -61% | 1 | 1 | 0% | 2,375 | 5,234 | +120% | 0 | 0 | — |
case-09 | pass→pass | 9,413 | 4,467 | -53% | 1 | 1 | 0% | 1,497 | 5,029 | +236% | 0 | 0 | — |
case-10 | pass→pass | 3,659 | 2,671 | -27% | 1 | 1 | 0% | 626 | 4,703 | +651% | 0 | 0 | — |
case-11 | pass→pass | 5,764 | 3,768 | -35% | 1 | 1 | 0% | 951 | 4,954 | +421% | 0 | 0 | — |
case-12 | pass→pass | 9,040 | 2,578 | -71% | 1 | 1 | 0% | 1,422 | 4,692 | +230% | 0 | 0 | — |
case-13 | pass→pass | 5,604 | 3,066 | -45% | 1 | 1 | 0% | 796 | 4,782 | +501% | 0 | 0 | — |
case-14 | pass→pass | 2,401 | 2,253 | -6% | 1 | 1 | 0% | 381 | 4,606 | +1109% | 0 | 0 | — |
case-15 | pass→pass | 3,249 | 2,295 | -29% | 1 | 1 | 0% | 526 | 4,610 | +776% | 0 | 0 | — |
case-16 | pass→pass | 6,834 | 4,883 | -29% | 1 | 1 | 0% | 1,177 | 5,117 | +335% | 0 | 0 | — |
case-17 | pass→pass | 7,906 | 4,207 | -47% | 1 | 1 | 0% | 1,339 | 4,969 | +271% | 0 | 0 | — |
case-18 | pass→pass | 6,656 | 4,333 | -35% | 1 | 1 | 0% | 1,106 | 5,000 | +352% | 0 | 0 | — |
case-19 | pass→pass | 2,976 | 2,614 | -12% | 1 | 1 | 0% | 434 | 4,722 | +988% | 0 | 0 | — |
case-20 | pass→pass | 7,949 | 6,577 | -17% | 1 | 1 | 0% | 1,466 | 5,459 | +272% | 0 | 0 | — |
case-21 | pass→pass | 13,111 | 11,819 | -10% | 1 | 1 | 0% | 2,396 | 6,369 | +166% | 0 | 0 | — |
case-22 | pass→pass | 23,638 | 18,387 | -22% | 1 | 1 | 0% | 4,172 | 7,861 | +88% | 0 | 0 | — |
case-23 | pass→pass | 15,955 | 17,773 | +11% | 1 | 1 | 0% | 2,464 | 7,076 | +187% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +13 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.