From godot-prompter
Provides GDScript patterns for Godot 4.3+: static typing with hints/collections/casting, await signals/coroutines, lambdas, match patterns, export annotations, inner classes, idioms.
npx claudepluginhub jame581/godotprompter --plugin godot-prompterThis skill uses the workspace's default tool permissions.
All examples target Godot 4.3+ with no deprecated APIs.
Provides Godot 4 GDScript patterns for architecture, signals, scenes, state machines, and optimization. Useful for building games, game systems, and best practices.
Guides Godot 4.x GDScript best practices for naming, type hints, scene organization, signals, resources, state machines, object pooling, and performance. Use when generating code, scenes, architecture, or patterns.
Provides Godot 4 GDScript patterns for signals, scenes, state machines, and optimization. Use for building games, implementing systems, or learning best practices.
Share bugs, ideas, or general feedback.
All examples target Godot 4.3+ with no deprecated APIs.
Related skills: 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.
# 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
# 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 is# '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.
await pauses the function until a signal fires, then resumes. The function becomes a coroutine.
func 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: 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.
# 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")
# 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)
var 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"])
func 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.
match 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()
@export var speed: float = 200.0
@export var health: int = 100
@export var player_name: String = "Hero"
@export var color: Color = Color.WHITE
@export var texture: Texture2D
@export var scene: PackedScene
@export_range(0.0, 100.0, 0.5) var volume: float = 50.0
@export_range(1, 10) var level: int = 1
@export_range(0.0, 1.0, 0.01, "or_greater") var scale: float = 1.0
@export_multiline var description: String = ""
@export_file("*.tscn") var level_path: String
@export_dir var save_directory: String
@export_color_no_alpha var outline_color: Color
# Enum export — creates dropdown in Inspector
@export_enum("Sword", "Bow", "Staff") var weapon: int = 0
# Or use a real enum:
enum Weapon { SWORD, BOW, STAFF }
@export var weapon_type: Weapon = Weapon.SWORD
@export_group("Movement")
@export var speed: float = 200.0
@export var acceleration: float = 1500.0
@export var friction: float = 1200.0
@export_group("Combat")
@export var attack_damage: int = 10
@export var attack_speed: float = 1.0
@export_subgroup("Defense")
@export var armor: int = 5
@export var dodge_chance: float = 0.1
@export_category("Advanced Settings")
@export var debug_mode: bool = false
# Node references (assigned in editor by dragging)
@export var target: Node2D
@export var spawn_point: Marker2D
@export var health_bar: ProgressBar
# Array of nodes/resources
@export var patrol_points: Array[Marker2D] = []
@export var loot_table: Array[ItemResource] = []
Register a script as a global class name — available everywhere without preload.
# 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)
In Godot 4, overridden virtual methods (_ready(), _process(), _physics_process(), _enter_tree(), _exit_tree(), etc.) do not automatically call the parent implementation. You must call super() explicitly if the parent class has logic in that method.
# parent.gd
class_name EnemyBase
extends CharacterBody2D
func _ready() -> void:
add_to_group("enemies")
$HealthComponent.health_depleted.connect(_on_health_depleted)
# child.gd — BUG: parent _ready() never runs!
extends EnemyBase
func _ready() -> void:
$NavigationAgent2D.velocity_computed.connect(_on_velocity_computed)
# Parent's group registration and signal connection are LOST
# child.gd — CORRECT: call super() to run parent _ready()
extends EnemyBase
func _ready() -> void:
super() # runs EnemyBase._ready() — group add + signal connect
$NavigationAgent2D.velocity_computed.connect(_on_velocity_computed)
C# uses base.MethodName() — same concept:
public partial class SpecialEnemy : EnemyBase
{
public override void _Ready()
{
base._Ready(); // runs EnemyBase._Ready()
GetNode<NavigationAgent2D>("NavigationAgent2D").VelocityComputed += OnVelocityComputed;
}
}
| Scenario | Call super()? |
|---|---|
| Extending a built-in Godot class (Node, CharacterBody2D) | Not needed — engine handles internal callbacks |
| Extending your own base class with logic in the virtual | Yes — always |
| Extending a third-party class (addon, plugin) | Yes — assume it has logic |
| Multiple inheritance levels (A → B → C) | Each level calls super() to chain up |
| Symptom | Likely Cause |
|---|---|
Child node doesn't join a group set in parent _ready() | Missing super() in child _ready() |
Signals connected in parent _ready() never fire | Missing super() — connections never made |
| Parent animation logic stops working in child | Missing super() in child _process() or _physics_process() |
@onready vars in parent are null when child accesses them | Parent _ready() body never ran — those vars never initialized |
Rule of thumb: If you extend a script that you or someone else wrote (not a bare Godot class), always call
super()as the first line of any overridden virtual method.
var label := "alive" if health > 0 else "dead"
var direction := -1 if facing_left else 1
velocity.x = speed * (1.5 if sprinting else 1.0)
# % operator (printf-style)
var msg := "Player %s has %d HP" % [player_name, health]
var formatted := "%.2f seconds" % elapsed_time
# String interpolation — no built-in f-strings, use % or format()
var text := "Score: %d / %d" % [current_score, max_score]
# Check if a node reference is valid
if is_instance_valid(target):
target.take_damage(10)
# Check if a variable is null
if weapon != null:
weapon.attack()
# Shorthand for non-null (works because null is falsy)
if weapon:
weapon.attack()
# Check array/dictionary/string emptiness
if inventory.is_empty():
show_empty_message()
if not player_name.is_empty():
display_name(player_name)
var stats: Dictionary = {"health": 100, "attack": 15, "defense": 8}
# Safe access with default
var hp: int = stats.get("health", 0)
var missing: int = stats.get("magic", 0) # returns 0, no error
# Check existence
if stats.has("attack"):
apply_damage(stats["attack"])
# Merge dictionaries
var defaults := {"health": 100, "attack": 10}
var overrides := {"attack": 20, "speed": 5}
defaults.merge(overrides, true) # true = overwrite existing keys
var items: Array[String] = ["sword", "shield", "potion", "sword"]
items.append("bow") # add to end
items.erase("sword") # remove first occurrence
items.has("shield") # true
items.find("potion") # index or -1
items.pick_random() # random element
items.shuffle() # randomize order
items.reverse() # reverse in place
items.slice(1, 3) # sub-array [index 1 to 3)
# Remove duplicates
var unique: Array[String] = []
for item in items:
if item not in unique:
unique.append(item)
var health: int = 100:
set(value):
health = clampi(value, 0, max_health)
health_changed.emit(health)
if health == 0:
died.emit()
get:
return health
| 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 |
Godot 4.5 adds variadic function support to GDScript. Append ... before the last parameter name to collect all trailing arguments passed at the call site into an Array. This replaces patterns that required callers to pass an explicit array literal.
Note: This skill is GDScript-specific by design. For C# patterns, see csharp-godot and csharp-signals.
# The ...args parameter collects any number of trailing arguments as an Array.
func log_message(level: String, ...args: Array) -> void:
var text := " ".join(args.map(func(a) -> String: return str(a)))
print("[%s] %s" % [level, text])
# Call with any number of trailing arguments — no array literal needed.
log_message("INFO", "Player", "joined", "the", "game")
log_message("WARN", "Health low:", current_health)
# Variadic math helper
func sum(...values: Array) -> float:
var total := 0.0
for v in values:
total += float(v)
return total
print(sum(1, 2, 3, 4, 5)) # 15.0
# Combine required parameters with variadic trailing args
func spawn_enemies(scene: PackedScene, ...positions: Array) -> void:
for pos in positions:
var enemy: Node2D = scene.instantiate()
enemy.global_position = pos
add_child(enemy)
spawn_enemies(enemy_scene,
Vector2(100, 200),
Vector2(300, 200),
Vector2(500, 200),
)
Rules: The variadic parameter must be the last parameter. It always arrives as a plain
Array(not typed). A function may have at most one variadic parameter.
The @abstract annotation prevents a class from being instantiated directly and forces subclasses to implement any method annotated with @abstract. This is the GDScript equivalent of C#'s abstract keyword.
Note: This skill is GDScript-specific by design. For C# patterns, see csharp-godot and csharp-signals.
# base_enemy.gd — abstract base class; cannot be instantiated directly
class_name BaseEnemy
extends CharacterBody2D
@abstract
## Subclasses must implement this to define their attack behavior.
@abstract func perform_attack() -> void
## Subclasses must implement this to return their display name.
@abstract func get_display_name() -> String
# Non-abstract methods are fine — they provide shared behavior.
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
die()
func die() -> void:
print(get_display_name(), " has died")
queue_free()
var health: int = 100
# melee_enemy.gd — concrete subclass
class_name MeleeEnemy
extends BaseEnemy
func perform_attack() -> void:
$HitboxArea.monitoring = true
await get_tree().create_timer(0.2).timeout
$HitboxArea.monitoring = false
func get_display_name() -> String:
return "Melee Enemy"
# ranged_enemy.gd — another concrete subclass
class_name RangedEnemy
extends BaseEnemy
@export var projectile_scene: PackedScene
func perform_attack() -> void:
var proj: Node2D = projectile_scene.instantiate()
proj.global_position = global_position
get_tree().root.add_child(proj)
func get_display_name() -> String:
return "Ranged Enemy"
Instantiation guard: GDScript raises an error at runtime if you call
BaseEnemy.new()directly. The@abstractannotation on the class is the guard — no constructor override needed.
Partial abstraction: Only the methods annotated
@abstractare required by subclasses. A class can be@abstractwithout any abstract methods (to signal "don't instantiate this directly") or can have abstract methods without the class-level annotation (each method still enforces implementation).
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+)