npx skills add ...
npx skills add jame581/godotprompter --skill gdscript-patterns
Use when writing GDScript — static typing, await/coroutines, lambdas, match patterns, export annotations, inner classes, and common idioms
npx skills add jame581/godotprompter --skill gdscript-patterns
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.
as and isIn 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
returnstatement becomes an error. Addreturn null(or a typed return value) at the end of the override. See the 4.7 migration guide.
await pauses the function until a signal fires, then resumes. The function becomes a coroutine.
A freed node's own coroutine never resumes, so is_instance_valid(self) after await guards nothing.
Lambdas are inline anonymous functions for callbacks, sorting, and filtering, called with .call(). The reference covers single- and multi-line syntax, inline signal connections including CONNECT_ONE_SHOT, Array methods (filter, map, reduce, any, all, sort_custom), and closures, where locals are captured by value.
See references/lambda-functions.md for the syntax, signal,
Arraymethod, and closure examples.
GDScript's match is like switch but with pattern support.
@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.
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 this node was freed (coroutine dropped) | Check the condition before awaiting; race slow signals against a timer |
| Lambda's change to a variable is lost | Locals are captured by value | Keep shared state in a member, Array or Dictionary |
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 — replacetype_exists("Sprite2D")withClassDB.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 are re-checked with is_instance_valid() (self needs no check)match 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+)# 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)# '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 surprisesclass 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 errorfunc 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()# 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")# 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)# DANGER: `target` may be freed while this coroutine waits
func unsafe_coroutine(target: Node2D) -> void:
await get_tree().create_timer(5.0).timeout
target.position = Vector2.ZERO # error: previously freed instance
# SAFE: re-check other objects after every await
func safe_coroutine(target: Node2D) -> void:
await get_tree().create_timer(5.0).timeout
if not is_instance_valid(target):
return
target.position = Vector2.ZEROmatch 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)# 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()# 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] = []# 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)