Skip to content
Development
Skill

/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

From plugin
godot-prompter
90361 skills9 agents1 hook

Claude Code

$ npx -y skills add jame581/GodotPrompter --skill gdscript-advanced --agent claude-code

SKILL.md

gdscript-advanced.SKILL.md
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 coroutines that never resume 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** (static vars since Godot 4.1) 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_score

Avoid singletons-as-autoloads when a static method on a class would do.

**Vector2i vs Vector2 / Vector3i vs Vector3** — integer vectors keep tile coords and grid math exact (no float drift, usable as `Dictionary` keys). Measure before assuming a speed gain. 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+) validate key and value types on write and give the analyzer static types for reads (values are still stored as Variant):

var stats: Dictionary[String, int] = {}
stats["hp"] = 100  # type-checked; stats["hp"] is statically an int

**`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)` for many targets every frame — each call is an object-database lookup; small per call, but it multiplies.

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 at the end of the current frame (idle time), 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("free")` lets the data delete the object. 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`):

func _notification(what: int) -> void:
    if what == NOTIFICATION_EDITOR_PRE_SAVE:
        _bake_preview()

> Common pitfall: a `@tool` setter that calls `get_tree()` while the scene is loading. Exported values are set before the node enters the tree, so `get_tree()` returns `null` with an error — guard with `is_inside_tree()`.

> 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` suspends the function and hands control back to its caller until the signal fires. It has three trap shapes:

**Trap 1 — `await` in `_ready`** returns early, so the node reports ready before it is initialized:

# BAD: the first await returns control, so `ready` is emitted and the
Read more
@jame581
Ships withgodot-prompter

Agentic skills framework for Godot 4.x game development. Gives AI coding agents domain-specific expertise for GDScript and C# projects.

90361 skills9 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
903
Star history of jame581/GodotPrompter on GitHub, a chart by star-history.com

Other skills on godot-prompter.

2d-essentials

2d-essentials

Use when working with 2D-specific systems — TileMaps, parallax scrolling, 2D lights and shadows, canvas layers, particles 2D,…

3d-essentials

3d-essentials

Use when working with 3D-specific systems — materials, lighting, shadows, environment, global illumination, fog, LOD, occlusion…

ability-system

ability-system

Use when building character abilities — Resource-based abilities with cost/cooldown/cast, buffs/debuffs, stat modifiers, gameplay…

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc12d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react1d252k
@facebook@facebookInstall Skill

Questions about gdscript-advanced.

Which coding agents can use gdscript-advanced?
gdscript-advanced installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.