| name | godot-gdscript-mastery |
| description | Expert GDScript best practices including static typing (var x: int, func returns void), signal architecture (signal up call down), unique node access (%NodeName, @onready), script structure (extends, class_name, signals, exports, methods), and performance patterns (dict.get with defaults, avoid get_node in loops). Use for code review, refactoring, or establishing project standards. Trigger keywords: static_typing, signal_architecture, unique_nodes, @onready, class_name, signal_up_call_down, gdscript_style_guide. |
GDScript Mastery
Expert guidance for writing performant, maintainable GDScript following official Godot standards.
NEVER Do
- NEVER use dynamic typing for performance-critical code —
var x = 5 is 20-40% slower than var x: int = 5. Type everything.
- NEVER call parent methods from children ("Call Up") — Use "Signal Up, Call Down". Children emit signals, parents call child methods.
- NEVER use
get_node() in _process() or _physics_process() — Caches with @onready var sprite = $Sprite. get_node() is slow in loops.
- NEVER access dictionaries without
.get() default — dict["key"] crashes if missing. Use dict.get("key", default) for safety.
- NEVER skip
class_name for reusable scripts — Without class_name, you can't use as type hints (var item: Item). Makes code harder to maintain.
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
Higher-order functions in GDScript: filter/map with lambdas, factory functions returning Callables, typed array godot-composition, and static utility methods.
Scans codebase for missing type hints. Run before releases to enforce static typing standards.
Detects performance anti-patterns: get_node() in loops, string concat, unsafe dict access.
Enforces "Signal Up, Call Down" pattern. Detects get_parent() calls and untyped signals.
Do NOT Load performance_analyzer.gd unless profiling hot paths or optimizing frame rates.
Core Directives
1. Strong Typing
Always use static typing. It improves performance and catches bugs early.
Rule: Prefer var x: int = 5 over var x = 5.
Rule: Always specify return types for functions: func _ready() -> void:.
2. Signal Architecture
- Connect in
_ready(): Preferably connect signals in code to maintain visibility, rather than just in the editor.
- Typed Signals: Define signals with types:
signal item_collected(item: ItemResource).
- Pattern: "Signal Up, Call Down". Children should never call methods on parents; they should emit signals instead.
3. Node Access
- Unique Names: Use
%UniqueNames for nodes that are critical to the script's logic.
- Onready Overrides: Prefer
@onready var sprite = %Sprite2D over calling get_node() in every function.
4. Code Structure
Follow the standard Godot script layout:
extends
class_name
signals / enums / constants
@export / @onready / properties
_init() / _ready() / _process()
- Public methods
- Private methods (prefixed with
_)
Common "Architect" Patterns
The "Safe" Dictionary Lookup
Avoid dict["key"] if you aren't 100% sure it exists. Use dict.get("key", default).
Scene Unique Nodes
When building complex UI, always toggle "Access as Scene Unique Name" on critical nodes (Labels, Buttons) and access them via %Name.
Declarative Data Model → Renderer
For complex UI (tuning panels, option menus), declare data as typed classes and let a renderer build UI from the tree:
# Data declaration (TuningData)
static func build_groups() -> Array[TuningGroup]:
var steering := TuningSection.new()
steering.params = [
TuningParam.slider("Steer Speed", "steering_speed", 1.0, 20.0, 0.25),
]
# Renderer walks tree, builds sliders automatically
renderer.build(groups, root_control, bridge)
Adding a new slider = adding one TuningParam entry. No UI code changes needed.
Shared UI Factory Pattern
Never duplicate slider/dropdown/button creation. Use project helpers:
UIFactory — themed sliders (FOCUS_NONE enforced), styleboxes, circle textures
MenuControlFactory — tab templates, section headers, control rows
MenuListBuilder — filterable/sortable/searchable list UI
Property Bridge Pattern
Replace 10 getter/setter pairs with a single bridge class:
# Instead of _get_environment_prop / _set_environment_prop / _get_weather_prop / ...
var bridge := TuningPropertyBridge.new()
bridge.vehicle = vehicle
var value = bridge.read(param) # routes to correct target
bridge.write(param, new_value) # routes to correct target
Signal Wiring Table
For 5+ signal connections, use a const array instead of manual .connect() lines:
const _SIGNALS := [["tool_changed", "_on_tool_changed"], ...]
for entry in _SIGNALS:
source.connect(entry[0], Callable(self, entry[1]))
Test Validation Runner
For tests checking arrays against rules, use a shared validation runner:
var failures := TuningTestHelpers.validate_entries(entries, rule_callable)
assert_eq(failures.size(), 0, str(failures))
Extraction Thresholds
- 3+ instances of same pattern → extract helper
- 200+ lines of match/if routing → use bridge/descriptor
- 10+ signal connections → wiring table
- 5+ setup methods → subsystem registry
Reference
- Official Docs:
tutorials/scripting/gdscript/gdscript_styleguide.rst
- Official Docs:
tutorials/best_practices/logic_preferences.rst
- Project:
.ai/knowledge/architecture/coding-standards.md — full DRY patterns with code examples
Related