--- name: gdscript-patterns description: Use when writing GDScript — static typing, await/coroutines, lambdas, match patterns, export annotations, inner classes, and common idioms --- # GDScript Patterns in Godot 4.3+ 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**. --- ## 1. Static Typing ### Type Hints 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 ``` ### Typed Collections ```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) ``` ### Casting with `as` and `is` ```gdscript # '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 ``` ### Enabling Strict Typing Warnings 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. ### Typed Return Inheritance in Overrides > ⚠️ **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](https://docs.godotengine.org/en/latest/tutorials/migrating/upgrading_to_godot_4.7.html). ```gdscript class 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 ``` --- ## 2. Await & Coroutines ### Awaiting Signals `await` pauses the function until a signal fires, then resumes. The function becomes a coroutine. ```gdscript 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() ``` ### Awaiting with Return Values ```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") ``` ### Timer Patterns ```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) ``` ### Coroutine Safety ```gdscript # 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.ZERO ``` A freed node's own coroutine never resumes, so `is_instance_valid(self)` after `await` guards nothing. --- ## 3. Lambda Functions 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](references/lambda-functions.md) for the syntax, signal, `Array` method, and closure examples. --- ## 4. Match / Pattern Matching GDScript's `match` is like `switch` but with pattern support. ### Basic Match ```gdscript 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) ``` ### Pattern Types ```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() ``` --- ## 5. Export Annotations `@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](references/export-annotations.md) for the full export annotation catalog (basic exports, range/hint variants, groups, node and Resource exports). --- ## 6. Inner Classes & class_name ### class_name 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] = [] ``` ### Inner Classes ```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) ``` --- ## 7. super() in Virtual Methods 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](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. --- ## 8. Common Idioms 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](references/common-idioms.md) for full code examples of each idiom. --- ## 9. Annotations Reference | 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 | --- ## 10. Common Pitfalls | 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 — replace `type_exists("Sprite2D")` with `ClassDB.class_exists("Sprite2D")`. See [GH-116899](https://github.com/godotengine/godot/pull/116899). --- ## 11. Variadic Functions (Godot 4.5+) 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](references/variadic-functions.md) for the full syntax, common patterns, and notes on when to prefer overloads. --- ## 12. Abstract Classes and Methods (Godot 4.5+) 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](references/abstract-classes.md) for full base-class patterns and subclass implementation rules. --- ## 13. Implementation Checklist - [ ] All variables, parameters, and return types have explicit type hints - [ ] Typed arrays (`Array[Type]`) are used instead of untyped `Array` where possible - [ ] Other nodes used after an `await` are re-checked with `is_instance_valid()` (`self` needs no check) - [ ] Lambdas connected to signals are simple — complex logic goes in named methods - [ ] `match` statements include a `_:` default branch - [ ] `@export` variables use appropriate hints (`@export_range`, `@export_enum`, etc.) - [ ] `@export_group` organizes Inspector properties into logical sections - [ ] `class_name` is only used for scripts that need global visibility - [ ] `is` type check precedes `as` cast when the type isn't guaranteed - [ ] Properties with setters validate and clamp values - [ ] Overridden virtual methods call `super()` when extending non-built-in base classes - [ ] Variadic functions (`...args`) used when the number of trailing arguments is open-ended (Godot 4.5+) - [ ] Base classes that must not be instantiated use `@abstract`; required methods use `@abstract func` (Godot 4.5+)