/gdscript-advanced
Use when writing production-grade GDScript — performance idioms, metaprogramming, @tool lifecycle, async pitfalls, signal/Callable trade-offs, profiler-driven idioms, and common pitfalls
$ npx -y skills add jame581/GodotPrompter --skill gdscript-advanced --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/gdscript-advanced
Context preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing production-grade GDScript — performance idioms, metaprogramming, @tool lifecycle, async pitfalls, signal/Callable trade-offs, profiler-driven idioms, and common pitfalls
SKILL.md
gdscript-advanced.SKILL.mdname: gdscript-advanced
description: Use when writing production-grade GDScript — performance idioms, metaprogramming, @tool lifecycle, async pitfalls, signal/Callable trade-offs, profiler-driven idioms, and common pitfalls
GDScript Advanced
Production-grade GDScript depth — for shipping games, not for learning the language. Pair with **gdscript-patterns** for fundamentals.
> **Related skills:** **gdscript-patterns** for language fundamentals, **godot-optimization** for engine-side perf work, **godot-debugging** for runtime diagnosis, **csharp-godot** for the C# alternative.
> **Intent:** This skill is GDScript-only by design (allowlisted). C# users should read `csharp-godot`. Adding C# parity here would undermine the audience split.
1. When to reach for advanced GDScript
You're past `gdscript-patterns` when:
- You're hitting a profiler bottleneck and need to know which idioms are fast
- You're writing editor tools and need `@tool` lifecycle correctness
- You're seeing `await` deadlocks or `Callable` lifetime bugs
- You need metaprogramming (calling functions by name, dynamic dispatch) without footguns
- You're shipping a real game and want to avoid the patterns that look fine but break under load
This skill assumes you already know typed parameters, `@onready`, `await`, `match`, and lambdas (covered in `gdscript-patterns`).
2. Performance idioms
**Static vars and methods** (Godot 4.4+) avoid per-instance overhead:
class_name Tally extends Node
static var _global_score: int = 0
static func add_score(amount: int) -> void:
_global_score += amount
static func get_score() -> int:
return _global_scoreAvoid singletons-as-autoloads when a static method on a class would do.
**Vector2i vs Vector2 / Vector3i vs Vector3** — integer vectors are 30-40% faster on hot paths (tile coords, grid math). Convert to float only at the rendering boundary:
var grid_pos: Vector2i = Vector2i(8, 12) # cheap
var world_pos: Vector2 = Vector2(grid_pos) * TILE_SIZE # convert at boundary
**PackedArray\* over generic Array** — `PackedInt32Array`, `PackedFloat32Array`, `PackedVector2Array`, etc. allocate contiguous memory and skip Variant boxing. Use them for buffers, vertex arrays, hot-loop accumulators.
var positions: PackedVector3Array = PackedVector3Array()
positions.resize(1000) # one allocation
for i in 1000:
positions[i] = Vector3(i, 0, 0)**Typed Dictionary access** — typed dicts (Godot 4.4+) skip the Variant unbox per read:
var stats: Dictionary[String, int] = {}
stats["hp"] = 100 # no boxing**`is_instance_valid` vs `null` check** — `is_instance_valid()` does an engine-side lookup; `!= null` is a pointer compare. Prefer `!= null` after `@onready` assignment; reserve `is_instance_valid()` for nodes that may be `queue_free`'d while a reference is held.
> Common pitfall: `_process` doing `if is_instance_valid(target)` once per frame burns ~1µs per call — tiny per-call but multiplies fast.
3. Metaprogramming
`Callable.bind`, `Callable.call`, `Callable.call_deferred` give you dynamic dispatch without `Object.call(name)` security risks.
**Binding arguments:**
var greeter: Callable = print_named.bind("Player")
greeter.call() # prints "Hello, Player"
func print_named(name: String) -> void:
print("Hello, %s" % name)**Deferred calls** — run on the next frame's idle phase, useful for cross-thread or signal-storm safety:
heavy_recompute.call_deferred()
**`Object.set` / `Object.get` / `Object.has_method`** — for truly dynamic code (script reloading, modding):
if obj.has_method("on_damaged"):
obj.call("on_damaged", 25)> **Security gotcha:** Never pass `obj.call(user_string, ...)` where `user_string` comes from save files, network, or mod content without an allowlist. `call("queue_free")` is a free crash. Match against a known set:
const ALLOWED_RPCS: PackedStringArray = ["take_damage", "apply_buff", "set_position"]
if user_method in ALLOWED_RPCS and obj.has_method(user_method):
obj.call(user_method, args)> See [references/metaprogramming-recipes.md](references/metaprogramming-recipes.md) for full Callable patterns and the modding security model.
4. `@tool` lifecycle
`@tool` scripts run in the editor as well as in-game. Two failure modes dominate: 1. Editor-only logic accidentally runs at play time 2. In-game logic accidentally runs in the editor and crashes the editor
**The guard:**
@tool
extends Node
func _ready() -> void:
if Engine.is_editor_hint():
_setup_editor_preview()
else:
_setup_game_runtime()**Editor notifications** — use `_notification` for editor lifecycle events (`NOTIFICATION_EDITOR_PRE_SAVE`, `NOTIFICATION_EDITOR_POST_SAVE`, `NOTIFICATION_PARENTED`):
func _notification(what: int) -> void:
if what == NOTIFICATION_EDITOR_PRE_SAVE:
_bake_preview()> Common pitfall: a `@tool` script that calls `get_tree().create_timer()` at editor time. Editor has no main loop in some contexts — guard with `is_editor_hint()`.
> See [references/tool-script-recipes.md](references/tool-script-recipes.md) for full `@tool` patterns including editor preview, baking, and procedural mesh generation.
5. Async pitfalls
`await` is sugar over signal-yielding. It has three trap shapes:
**Trap 1 — `await` in `_ready`** delays children's ready order:
# BAD: children of this node ready BEFORE this _ready() finishes
func _ready() -> void:
await get_tree().create_timer(1.0).timeout
initialize_children() # children already ready'd against an uninitialized parentFix: do not `await` in `_ready`. Move the await to a separate setup function.
**Trap 2 — Awaiting a signal that never fires** deadlocks the calling coroutine:
# BAD if `health_changed` never fires (e.g., entity already at ful
Read more
name: gdscript-advanced description: Use when writing production-grade GDScript — performance idioms, metaprogramming, @tool lifecycle, async pitfalls, signal/Callable trade-offs, profiler-driven idioms, and common pitfalls
GDScript Advanced
Production-grade GDScript depth — for shipping games, not for learning the language. Pair with **gdscript-patterns** for fundamentals.
> **Related skills:** **gdscript-patterns** for language fundamentals, **godot-optimization** for engine-side perf work, **godot-debugging** for runtime diagnosis, **csharp-godot** for the C# alternative.
> **Intent:** This skill is GDScript-only by design (allowlisted). C# users should read `csharp-godot`. Adding C# parity here would undermine the audience split.
1. When to reach for advanced GDScript
You're past `gdscript-patterns` when:
- You're hitting a profiler bottleneck and need to know which idioms are fast
- You're writing editor tools and need `@tool` lifecycle correctness
- You're seeing `await` deadlocks or `Callable` lifetime bugs
- You need metaprogramming (calling functions by name, dynamic dispatch) without footguns
- You're shipping a real game and want to avoid the patterns that look fine but break under load
This skill assumes you already know typed parameters, `@onready`, `await`, `match`, and lambdas (covered in `gdscript-patterns`).
2. Performance idioms
**Static vars and methods** (Godot 4.4+) avoid per-instance overhead:
class_name Tally extends Node
static var _global_score: int = 0
static func add_score(amount: int) -> void:
_global_score += amount
static func get_score() -> int:
return _global_scoreAvoid singletons-as-autoloads when a static method on a class would do.
**Vector2i vs Vector2 / Vector3i vs Vector3** — integer vectors are 30-40% faster on hot paths (tile coords, grid math). Convert to float only at the rendering boundary:
var grid_pos: Vector2i = Vector2i(8, 12) # cheap var world_pos: Vector2 = Vector2(grid_pos) * TILE_SIZE # convert at boundary
**PackedArray\* over generic Array** — `PackedInt32Array`, `PackedFloat32Array`, `PackedVector2Array`, etc. allocate contiguous memory and skip Variant boxing. Use them for buffers, vertex arrays, hot-loop accumulators.
var positions: PackedVector3Array = PackedVector3Array()
positions.resize(1000) # one allocation
for i in 1000:
positions[i] = Vector3(i, 0, 0)**Typed Dictionary access** — typed dicts (Godot 4.4+) skip the Variant unbox per read:
var stats: Dictionary[String, int] = {}
stats["hp"] = 100 # no boxing**`is_instance_valid` vs `null` check** — `is_instance_valid()` does an engine-side lookup; `!= null` is a pointer compare. Prefer `!= null` after `@onready` assignment; reserve `is_instance_valid()` for nodes that may be `queue_free`'d while a reference is held.
> Common pitfall: `_process` doing `if is_instance_valid(target)` once per frame burns ~1µs per call — tiny per-call but multiplies fast.
3. Metaprogramming
`Callable.bind`, `Callable.call`, `Callable.call_deferred` give you dynamic dispatch without `Object.call(name)` security risks.
**Binding arguments:**
var greeter: Callable = print_named.bind("Player")
greeter.call() # prints "Hello, Player"
func print_named(name: String) -> void:
print("Hello, %s" % name)**Deferred calls** — run on the next frame's idle phase, useful for cross-thread or signal-storm safety:
heavy_recompute.call_deferred()
**`Object.set` / `Object.get` / `Object.has_method`** — for truly dynamic code (script reloading, modding):
if obj.has_method("on_damaged"):
obj.call("on_damaged", 25)> **Security gotcha:** Never pass `obj.call(user_string, ...)` where `user_string` comes from save files, network, or mod content without an allowlist. `call("queue_free")` is a free crash. Match against a known set:
const ALLOWED_RPCS: PackedStringArray = ["take_damage", "apply_buff", "set_position"]
if user_method in ALLOWED_RPCS and obj.has_method(user_method):
obj.call(user_method, args)> See [references/metaprogramming-recipes.md](references/metaprogramming-recipes.md) for full Callable patterns and the modding security model.
4. `@tool` lifecycle
`@tool` scripts run in the editor as well as in-game. Two failure modes dominate: 1. Editor-only logic accidentally runs at play time 2. In-game logic accidentally runs in the editor and crashes the editor
**The guard:**
@tool
extends Node
func _ready() -> void:
if Engine.is_editor_hint():
_setup_editor_preview()
else:
_setup_game_runtime()**Editor notifications** — use `_notification` for editor lifecycle events (`NOTIFICATION_EDITOR_PRE_SAVE`, `NOTIFICATION_EDITOR_POST_SAVE`, `NOTIFICATION_PARENTED`):
func _notification(what: int) -> void:
if what == NOTIFICATION_EDITOR_PRE_SAVE:
_bake_preview()> Common pitfall: a `@tool` script that calls `get_tree().create_timer()` at editor time. Editor has no main loop in some contexts — guard with `is_editor_hint()`.
> See [references/tool-script-recipes.md](references/tool-script-recipes.md) for full `@tool` patterns including editor preview, baking, and procedural mesh generation.
5. Async pitfalls
`await` is sugar over signal-yielding. It has three trap shapes:
**Trap 1 — `await` in `_ready`** delays children's ready order:
# BAD: children of this node ready BEFORE this _ready() finishes
func _ready() -> void:
await get_tree().create_timer(1.0).timeout
initialize_children() # children already ready'd against an uninitialized parentFix: do not `await` in `_ready`. Move the await to a separate setup function.
**Trap 2 — Awaiting a signal that never fires** deadlocks the calling coroutine:
# BAD if `health_changed` never fires (e.g., entity already at ful
Agentic skills framework for Godot 4.x game development. Gives AI coding agents domain-specific expertise for GDScript and C# projects.
Other skills on godot-prompter.
- /authoring-godot-prompter-skills
Use when writing or editing a SKILL.md or an agent definition in this repo — required frontmatter, section ordering, and the GDScript-then-C# example convention.
Open skill - /releasing-godot-prompter
Use when cutting a GodotPrompter release or bumping its version — the version-bump sequence, tag-triggered workflow, and the marketplace manifests that must follow.
Open skill - /2d-essentials
Use when working with 2D-specific systems — TileMaps, parallax scrolling, 2D lights and shadows, canvas layers, particles 2D, custom drawing, and 2D meshes in Godot 4.3+
Open skill - /3d-essentials
Use when working with 3D-specific systems — materials, lighting, shadows, environment, global illumination, fog, LOD, occlusion culling, and decals in Godot 4.3+
Open skill - /ability-system
Use when building character abilities — Resource-based abilities with cost/cooldown/cast, buffs/debuffs, stat modifiers, gameplay tags, and HUD binding
Open skill - /addon-development
Use when creating Godot editor plugins — EditorPlugin, @tool scripts, custom inspectors, and dock panels
Open skill

