The official GDScript style guide covers naming and file ordering. This page goes further: the patterns that prevent bugs, the anti-patterns that cause them, and the performance habits that separate smooth games from stuttery ones.
Most GDScript problems fall into three buckets: untyped code that catches errors too late, signals wired in the wrong direction, and uncached node lookups in hot paths. Fix those three and you eliminate the majority of GDScript headaches. For the type system in depth, see types. For signal patterns and the signal bus, see signals. For Timer recipes, see timers & signals.
The Godot style guide specifies a 17-item ordering for every script file. Following it makes your codebase predictable, so anyone opening a file knows exactly where to look.
@tool,@icon,@static_unloadannotationsclass_nameextends- Documentation comments (
##) - Signals
- Enums
- Constants
- Static variables
@exportvariables- Regular variables
@onreadyvariables_static_init()- Other static methods
- Built-in virtual methods (
_init,_enter_tree,_ready,_process,_physics_process, etc.) - Custom overridden methods
- Remaining methods
- Inner classes
Within each section, public members come before private (underscore-prefixed) members. Use two blank lines between functions and one blank line inside functions to separate logical sections. Tabs for indentation, never spaces.
Yes. Static typing catches errors at parse time instead of during gameplay, gives you autocomplete in the editor, and generates faster bytecode. Independent benchmarks show 28-59% speedups depending on the operation. There is no good reason to skip it.
Use := when the type is obvious from the right-hand side: var speed := 50.0. Use explicit type annotations for get_node() results and anything ambiguous: @onready var timer: Timer = $Timer. The casting form also works: @onready var timer := $Timer as Timer.
To enforce typing across your project, enable the UNTYPED_DECLARATION warning in Project Settings. This flags every variable, parameter, and return type that lacks an annotation. Also enable UNSAFE_PROPERTY_ACCESS and UNSAFE_CAST for maximum safety.
For the full type system (Variant, typed arrays, typed dictionaries, casting behavior), see types.
Signal names use past tense to describe something that just happened: health_changed, died, door_opened, item_collected. For actions with a clear start and end, use _started and _finished suffixes: animation_started, level_loading_finished.
Callback functions follow the pattern _on_NodeName_signal_name. For self-connections where you listen to your own signal, omit the node name: _on_button_pressed.
The most important signal rule is directional. Signals go up the tree (child emits, parent listens). Direct method calls go down (parent calls child). This is the "call down, signal up" principle. When a child node directly references its parent with get_parent(), that is almost always a design mistake. Emit a signal instead and let the parent connect to it.
For signal connection syntax, see signals. For Timer patterns, see timers & signals.
Godot's node tree is a composition system. Build entities by combining specialized child nodes rather than writing monolithic scripts:
# Player (CharacterBody2D)
# |- Sprite2D
# |- CollisionShape2D
# |- AnimationPlayer
# |- HitboxComponent (Area2D)
# |- HealthComponent (Node)
Each node handles one responsibility. Reusable behaviors become their own scenes (HealthComponent.tscn) that you instance wherever needed. One script per node. If a script file grows past 200-300 lines, that is a signal to split behavior into child nodes.
Don't reinvent what Godot gives you for free. Signals are the observer pattern. The node tree is composition. Autoloads are singletons. State machines are worth building when you have three or more states, but you don't need a framework for it.
What to avoid: object pooling (GDScript uses reference counting, not garbage collection), full ECS architectures (don't replace Godot's node system), and over-engineering with design patterns when a simple approach works.
Uncached node lookups in _process(). Every $Node or get_node() call traverses the scene tree. In _process(), that runs 60+ times per second. Cache references with @onready:
# Cache once
@onready var sprite: Sprite2D = $Sprite2D
func _process(delta: float) -> void:
sprite.rotation += delta # Uses cached reference
Physics code in _process(). The _process() callback runs at the monitor's refresh rate, which varies. Movement, collisions, and raycasts belong in _physics_process(), which runs at a fixed rate (default 60 Hz). Mixing these up causes inconsistent behavior across different machines.
String comparisons instead of StringName. Regular string comparison checks every character. StringName (&"name") uses pointer comparison, which is instant. Use StringName for input actions, animation names, and any string compared frequently: Input.is_action_pressed(&"jump").
distance_to() when you only need comparison. The distance_to() method computes a square root. If you just need to compare distances, use distance_squared_to() and compare against a squared threshold.
Building nodes in code instead of instancing scenes. preload("res://enemy.tscn").instantiate() is faster than creating nodes one by one with add_child(). Let the engine do the work in optimized C++ code.
Spaghetti references. Reaching deep into other scenes with paths like get_node("../../World/Enemies/Boss") creates fragile code that breaks when you rearrange the tree. Use signals or pass references through exported variables.
God scripts. A 500-line script handling movement, combat, inventory, and UI is hard to debug and impossible to reuse. Split responsibilities into child nodes with their own scripts.
Missing delta. Writing position.x += 5 in _process() makes speed depend on frame rate. Always multiply by delta: position.x += speed * delta.
Autoload overuse. Having GameManager, AudioManager, UIManager, EnemyManager, and ScoreManager as autoloads means everything is global. Reserve autoloads for truly global state: audio bus management, save/load, scene transitions. Pass references for everything else.
Type-unsafe node access. Using var player = get_node("Player") without a type gives you no autocomplete and no compile-time safety. Cast it: var player := get_node("Player") as Player.
GDScript has three levels of Inspector organization. @export_category("Player") creates a bold, non-collapsible header. @export_group("Movement") creates a collapsible section. @export_subgroup("Jump Settings") nests within the current group. End a group with @export_group("").
Use categories sparingly for major divisions. Groups and subgroups handle most organization needs. Put the most-tweaked properties at the top of each group.
For the full export annotation reference, see exports tuning.
GDScript has no try/catch. The engine philosophy is "keep the game running." Error handling relies on guard clauses, logging functions, and defensive checks.
Guard clauses handle invalid input at the top of a function:
func take_damage(amount: int) -> void:
if amount <= 0:
return
if not is_alive:
return
health -= amount
Logging functions report problems without stopping execution. push_error() logs errors to the output panel. push_warning() logs non-critical issues. As of Godot 4.4, both print the calling function name automatically.
assert() validates invariants during development. Assertions only run in debug builds and are stripped from release exports. Use them for conditions that should never be violated: assert(max_health > 0, "max_health must be positive").
is_instance_valid() checks whether a node reference is still valid. Nodes freed with queue_free() become invalid, and accessing them crashes the game. Always check before using a stored reference that might have been freed. Similarly, get_node_or_null() returns null instead of crashing when a node path does not exist.
What You'll Practice: GDScript Bests
Common GDScript Bests Pitfalls
- Forgetting @onready and calling get_node() or using $ every frame in _process()
- Signals going the wrong direction (parent emitting to child instead of "call down, signal up")
- Using _process() for physics code (movement, collisions) instead of _physics_process()
- Comparing regular strings instead of StringName (&"name") in hot paths
- Reading @export values in _init() instead of _ready(), getting script defaults instead of Inspector values
GDScript Bests FAQ
What naming convention does GDScript use?
snake_case for functions and variables. PascalCase for classes (class_name) and node names. CONSTANT_CASE for constants and enum members. Booleans use is_, can_, or has_ prefixes. Signals use past tense (health_changed, died). Signal callbacks follow _on_NodeName_signal_name.
Should I use @onready or get the node in _ready()?
Either works. @onready var timer: Timer = $Timer is shorthand for assigning in _ready(). Both execute at the same time (right before _ready() runs). @onready is shorter and keeps the declaration next to other variables at the top of the file.
What is the difference between _process and _physics_process?
_process(delta) runs every rendered frame at a variable rate tied to the display. _physics_process(delta) runs at a fixed interval, 60 times per second by default. Use _physics_process for movement, collisions, raycasts, and anything involving move_and_slide(). Use _process for UI updates, animations, and visual effects.
How do I organize a large GDScript project?
Split behavior into child nodes with focused scripts. Use autoloads sparingly for truly global systems (audio, save/load). Use class_name to create shared types that other scripts can reference. Keep scripts under 200-300 lines. If a script grows beyond that, it is doing too much.
Does GDScript have a linter?
The Godot editor has a built-in warning system. Enable UNTYPED_DECLARATION to flag untyped variables and functions. Enable UNSAFE_PROPERTY_ACCESS and UNSAFE_CAST for stricter type checking. These warnings appear as yellow underlines in the script editor and in the output panel.
What is the difference between preload and load in GDScript?
preload() loads the resource at script parse time and requires a string literal path. load() loads at runtime and accepts dynamic paths from variables. Use preload for small, always-needed resources like bullet scenes. Use load for conditional or dynamically-determined resources. For large resources, use ResourceLoader.load_threaded_request() to avoid blocking the main thread.
How does error handling work in GDScript?
GDScript has no try/catch. Use guard clauses with early return for invalid input. Use push_error() and push_warning() to log issues without stopping execution. Use assert() for debug-only invariant checks (stripped in release builds). Use is_instance_valid() to check if a freed node reference is still safe to access.
When should I use class_name?
Use class_name when other scripts need to reference the type for static typing, type checking with is, or casting with as. Skip it for scripts that are only attached to a single node and never referenced by type elsewhere. Every class_name script loads at engine startup, so keep them lightweight.
What does the @tool annotation do?
@tool makes a script run inside the Godot editor, not just during gameplay. This lets you create custom Inspector previews, in-editor visualizations, and editor plugins. Place @tool as the very first line of the script. Be careful with _process() in @tool scripts since it runs in the editor too.
How do I share data between nodes in GDScript?
Follow the "call down, signal up" rule. Parents call methods on their children directly. Children emit signals that parents connect to. For truly global data, use an autoload singleton. For shared configuration, use custom Resources that multiple nodes reference.
GDScript Bests Syntax Quick Reference
class_name Player
extends CharacterBody2D
## Player controller with movement and combat.
signal died
signal health_changed(new_value: int)
enum State { IDLE, RUN, JUMP }
const MAX_SPEED: float = 300.0
@export var speed: float = 200.0
@export var jump_force: float = 400.0
var health: int = 100
var _internal_timer: float = 0.0
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
health = 100
func _physics_process(delta: float) -> void:
move_and_slide()
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health)var health: int = 100
var speed := 50.0
var direction: Vector2 = Vector2.ZERO
var enemies: Array[Node2D] = []
func heal(amount: int) -> void:
health = mini(health + amount, max_health)signal health_changed(new_value: int)
signal died
signal animation_started
signal animation_finished
func _on_Player_died() -> void:
queue_free()
func _on_Timer_timeout() -> void:
can_shoot = true# Cache node references once
@onready var timer := $CooldownTimer as Timer
@onready var sprite := $Sprite2D as Sprite2D
@onready var health_bar := $UI/HealthBar as ProgressBar
func _process(delta: float) -> void:
sprite.rotation += delta # Uses cached reference@export_category("Character")
@export_group("Movement")
@export var speed: float = 200.0
@export var acceleration: float = 50.0
@export_subgroup("Jump Settings")
@export var jump_height: float = 5.0
@export var coyote_time: float = 0.1
@export_group("Combat")
@export var max_health: int = 100
@export var damage: int = 10
@export_group("")
@export var debug_mode: bool = false