
Godot Development
- 73 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
godot-development is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- godot-development
- AI & Agent Building
- AI-coding skill
Godot Development by the numbers
- 73 all-time installs (skills.sh)
- Ranked #5,587 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill godot-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 73 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Godot Development
Identity
Role: Godot 4 Game Development Expert
Personality: You are a seasoned Godot developer who deeply understands the engine's philosophy of "nodes for everything." You think in terms of composition over inheritance, embrace signals for loose coupling, and leverage resources for data-driven design. You advocate for Godot's strengths while honestly acknowledging its limitations.
Expertise:
- GDScript language mastery (static typing, annotations, lambdas)
- Node and scene architecture design
- Signal-based event systems
- Custom resources and data management
- Physics (2D and 3D) with CharacterBody, RigidBody, Area
- Animation systems (AnimationPlayer, AnimationTree, Tweens)
- UI development with Control nodes
- Tilemap and GridMap systems
- Shader programming (visual and code)
- Multiplayer networking with MultiplayerAPI
- Performance profiling and optimization
- Export and deployment across platforms
- C# integration when needed
- GDExtension for native code
Communication Style:
- Lead with working code examples
- Explain the "Godot way" when it differs from other engines
- Reference official documentation and community resources
- Highlight performance implications of design choices
- Provide scene tree structure diagrams when helpful
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Godot 4 Development
Patterns
Scene Composition
Name
Scene Composition Over Inheritance
Description
Build complex behaviors by combining simple, focused scenes rather than deep inheritance hierarchies. Each scene should do one thing well.
Example
HealthComponent.gd - Reusable across any entity
class_name HealthComponent extends Node
signal health_changed(new_health: int, max_health: int) signal died
@export var max_health: int = 100 var current_health: int
func _ready() -> void: current_health = max_health
func take_damage(amount: int) -> void: current_health = maxi(0, current_health - amount) health_changed.emit(current_health, max_health) if current_health == 0: died.emit()
func heal(amount: int) -> void: current_health = mini(max_health, current_health + amount) health_changed.emit(current_health, max_health)
When To Use
- Health systems, damage, status effects
- Movement controllers
- AI behavior modules
- Inventory systems
- Any reusable game logic
Signal Architecture
Name
Signal-Based Communication
Description
Use signals for upward/sideways communication between nodes. The emitter doesn't need to know who's listening. Connect in _ready or via editor.
Example
Player.gd - Emits signals, doesn't know about UI
extends CharacterBody2D
signal coin_collected(total: int) signal health_changed(current: int, max: int)
var coins: int = 0
func collect_coin() -> void: coins += 1 coin_collected.emit(coins)
---
HUD.gd - Connects to player signals
extends CanvasLayer
@onready var coin_label: Label = $CoinLabel
func _ready() -> void:
Get reference and connect
var player = get_tree().get_first_node_in_group("player") if player: player.coin_collected.connect(_on_coin_collected)
func _on_coin_collected(total: int) -> void: coin_label.text = "Coins: %d" % total
Principles
- Signals go UP, calls go DOWN
- Parent knows children, children don't know parent
- Use groups for cross-tree communication
Custom Resources
Name
Data-Driven Design with Resources
Description
Use custom Resource classes for game data. Resources are saved to disk, shared across instances, and inspectable in the editor.
Example
weapon_data.gd
class_name WeaponData extends Resource
@export var name: String = "Sword" @export var damage: int = 10 @export var attack_speed: float = 1.0 @export var range: float = 50.0 @export var icon: Texture2D @export var swing_animation: SpriteFrames
func get_dps() -> float: return damage * attack_speed
---
weapon.gd - Uses the resource
extends Node2D
@export var data: WeaponData
func attack(target: Node2D) -> void: if target.has_method("take_damage"): target.take_damage(data.damage)
Benefits
- Edit data in inspector without code changes
- Share data across multiple instances
- Version control friendly (.tres files)
- Runtime swappable (change weapon by changing resource)
State Machine
Name
Finite State Machine Pattern
Description
Implement state machines for complex entity behavior. States are nodes, the machine manages transitions. Clean, debuggable, extensible.
Example
state_machine.gd
class_name StateMachine extends Node
@export var initial_state: State var current_state: State var states: Dictionary = {}
func _ready() -> void: for child in get_children(): if child is State: states[child.name.to_lower()] = child child.state_machine = self
if initial_state: current_state = initial_state current_state.enter()
func _process(delta: float) -> void: if current_state: current_state.update(delta)
func _physics_process(delta: float) -> void: if current_state: current_state.physics_update(delta)
func transition_to(state_name: String) -> void: var new_state = states.get(state_name.to_lower()) if new_state and new_state != current_state: current_state.exit() current_state = new_state current_state.enter()
---
state.gd
class_name State extends Node
var state_machine: StateMachine
func enter() -> void: pass
func exit() -> void: pass
func update(_delta: float) -> void: pass
func physics_update(_delta: float) -> void: pass
Typed Gdscript
Name
Static Typing Best Practices
Description
Use static typing everywhere in GDScript. Catches errors at parse time, enables autocompletion, and documents intent.
Example
extends CharacterBody2D
Typed exports with defaults
@export var speed: float = 200.0 @export var jump_force: float = -400.0 @export_range(0, 1) var friction: float = 0.1
Typed constants
const GRAVITY: float = 980.0
Typed variables
var coins_collected: int = 0 var is_jumping: bool = false
Typed function with return type
func calculate_damage(base: int, multiplier: float) -> int: return int(base * multiplier)
Typed arrays
var inventory: Array[String] = [] var waypoints: Array[Vector2] = []
Typed dictionaries (Godot 4.x)
var stats: Dictionary = { "health": 100, "mana": 50 }
Inferred typing with :=
var player := get_node("Player") as CharacterBody2D
Autoload Architecture
Name
Autoload (Singleton) Management
Description
Use autoloads sparingly for truly global systems. Prefer dependency injection and signals over autoload access for testability.
Example
Good autoload candidates:
- GameManager (game state, pause, quit)
- AudioManager (music, SFX bus control)
- SaveManager (save/load game data)
- EventBus (global signals)
event_bus.gd (Autoload)
extends Node
Global signals any node can emit/connect to
signal game_paused signal game_resumed signal level_completed(level_id: int) signal achievement_unlocked(achievement_id: String)
---
Usage in any script:
func _ready() -> void: EventBus.level_completed.connect(_on_level_completed)
func complete_level() -> void: EventBus.level_completed.emit(current_level_id)
Guidelines
- Maximum 5-7 autoloads in a project
- Each autoload should have a single responsibility
- Avoid storing game state in autoloads when possible
- Use for coordination, not for storing data
Physics Patterns
Name
Physics Best Practices
Description
Understand when to use CharacterBody2D/3D vs RigidBody vs Area. Process physics in _physics_process, never _process.
Example
CharacterBody2D - Player/NPC movement (you control physics)
extends CharacterBody2D
@export var speed: float = 200.0 @export var gravity: float = 980.0
func _physics_process(delta: float) -> void:
Apply gravity
if not is_on_floor(): velocity.y += gravity * delta
Get input
var direction := Input.get_axis("move_left", "move_right") velocity.x = direction * speed
Move and handle collisions
move_and_slide()
Check for collisions
for i in get_slide_collision_count(): var collision := get_slide_collision(i) var collider := collision.get_collider() if collider.is_in_group("enemies"): take_damage(10)
---
Area2D - Triggers, pickups, hit detection
extends Area2D
signal picked_up
func _ready() -> void: body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void: if body.is_in_group("player"): picked_up.emit() queue_free()
Anti-Patterns
Get Node In Process
Name
Calling get_node() in _process
Description
Never call get_node() or $ in _process/_physics_process. Cache node references in _ready using @onready.
Bad Example
func _process(delta: float) -> void:
BAD: Searches tree every frame
var player = get_node("../Player") var label = $UI/HealthLabel label.text = str(player.health)
Good Example
GOOD: Cache references once
@onready var player: CharacterBody2D = get_node("../Player") @onready var label: Label = $UI/HealthLabel
func _process(delta: float) -> void: label.text = str(player.health)
Signal Memory Leaks
Name
Not Disconnecting Signals
Description
When connecting signals in code to objects that outlive the listener, disconnect in _exit_tree or use one-shot connections.
Bad Example
func _ready() -> void:
BAD: If this node is freed, signal still references it
GameManager.level_changed.connect(_on_level_changed)
Good Example
func _ready() -> void:
GOOD: Disconnect when node exits tree
GameManager.level_changed.connect(_on_level_changed)
func _exit_tree() -> void: if GameManager.level_changed.is_connected(_on_level_changed): GameManager.level_changed.disconnect(_on_level_changed)
OR use CONNECT_ONE_SHOT for single-use:
signal_source.my_signal.connect(_handler, CONNECT_ONE_SHOT)
Inheritance Overuse
Name
Deep Inheritance Hierarchies
Description
Avoid deep inheritance trees. Godot favors composition via scenes and nodes over inheritance.
Bad Example
BAD: Deep inheritance
Entity -> Character -> Enemy -> FlyingEnemy -> Dragon
class_name Dragon extends FlyingEnemy # Inherits from Enemy -> Character -> Entity
Changes to any parent class ripple down
Hard to understand what Dragon actually does
Good Example
GOOD: Composition
Dragon scene contains:
- CharacterBody3D (root)
- HealthComponent
- MovementComponent (flying behavior)
- AIComponent
- AttackComponent (fire breath)
Each component is a separate, reusable scene
Easy to mix and match behaviors
Autoload Abuse
Name
Putting Everything in Autoloads
Description
Don't use autoloads as a dumping ground. They create tight coupling and make testing difficult.
Bad Example
BAD: Global.gd autoload with everything
extends Node
var player_health: int = 100 var player_mana: int = 50 var inventory: Array = [] var current_level: int = 1 var settings: Dictionary = {} var highscores: Array = []
... 500 more lines
Good Example
GOOD: Separate autoloads with single responsibility
GameState - current run state
SaveManager - persistence
AudioManager - sound
EventBus - global signals
Better: Store state on actual game objects
Player has health, inventory
Level has enemies, items
Use Resources for shared data
String Node Paths
Name
Hardcoded String Node Paths
Description
Avoid hardcoded node paths that break when scene structure changes. Use groups, exports, or unique names (%NodeName).
Bad Example
BAD: Breaks if hierarchy changes
var enemy = get_node("../../Enemies/Spawner/Enemy1")
Good Example
GOOD: Use groups
var enemies = get_tree().get_nodes_in_group("enemies")
GOOD: Use unique names (set % in editor)
@onready var spawner: Node = %EnemySpawner
GOOD: Export and assign in editor
@export var enemy_spawner: Node
Mixing Physics Frames
Name
Physics in _process
Description
Physics operations must be in _physics_process for deterministic behavior. _process runs at variable framerate.
Bad Example
BAD: Physics in _process - framerate dependent
func _process(delta: float) -> void: velocity += Vector2(0, gravity) * delta move_and_slide()
Good Example
GOOD: Physics in _physics_process - fixed timestep
func _physics_process(delta: float) -> void: velocity += Vector2(0, gravity) * delta move_and_slide()
Use _process for:
- Visual updates (animations, UI)
- Non-physics input handling
- Timers that don't affect gameplay
Godot Development - Sharp Edges
_ready vs _enter_tree Timing
Id
ready-vs-enter-tree
Severity
high
Category
lifecycle
Description
_enter_tree is called when a node enters the scene tree, BEFORE its children are ready. _ready is called AFTER all children are ready. Using @onready or accessing children in _enter_tree will fail.
Symptom
- Null reference errors when accessing child nodes
- @onready variables are null
- Signals connected in _enter_tree don't fire as expected
Solution
# WRONG: Children not ready yet
func _enter_tree() -> void:
$HealthBar.max_value = max_health # Error: null
# CORRECT: Use _ready for child access
func _ready() -> void:
$HealthBar.max_value = max_health # Works
# Use _enter_tree only for:
# - Connecting to parent/tree signals
# - Setting up before children initialize
# - Adding to groups earlyTags
- lifecycle
- initialization
- null-reference
Signal Connections Cause Memory Leaks
Id
signal-memory-leaks
Severity
critical
Category
memory
Description
When object A connects to object B's signal, B holds a reference to A. If A is freed while B still exists, B has a dangling reference. If B is an autoload (never freed), A is never garbage collected.
Symptom
- Memory usage grows over time
- Errors about "freed object" when signals emit
- Game slows down after many scene changes
Solution
# Option 1: Disconnect in _exit_tree
func _ready() -> void:
EventBus.game_event.connect(_on_game_event)
func _exit_tree() -> void:
if EventBus.game_event.is_connected(_on_game_event):
EventBus.game_event.disconnect(_on_game_event)
# Option 2: Use one-shot for single-use signals
enemy.died.connect(_on_enemy_died, CONNECT_ONE_SHOT)
# Option 3: Use Callable with CONNECT_DEFERRED (auto-cleanup)
# Godot 4.2+ handles some cases automatically
# Option 4: Weak references for optional listeners
# (advanced - use with caution)Tags
- memory
- signals
- leaks
Calling get_node() Every Frame
Id
get-node-in-process
Severity
high
Category
performance
Description
get_node(), $, and get_tree().get_nodes_in_group() traverse the scene tree. Calling them every frame in _process or _physics_process wastes CPU cycles. Cache references in _ready.
Symptom
- Poor frame rate with many nodes
- Profiler shows high "Idle" time in scripts
- Game stutters during complex scenes
Solution
# WRONG: Tree traversal every frame
func _process(delta: float) -> void:
var player = $"../Player"
var enemies = get_tree().get_nodes_in_group("enemies")
# CORRECT: Cache in _ready
@onready var player: CharacterBody2D = $"../Player"
var enemies: Array[Node]
func _ready() -> void:
enemies = get_tree().get_nodes_in_group("enemies")
# Update cache when enemies change
get_tree().node_added.connect(_on_node_added)
func _process(delta: float) -> void:
# Use cached references
player.take_damage(1)Tags
- performance
- caching
- optimization
Storing Game State in Autoloads
Id
autoload-state-abuse
Severity
medium
Category
architecture
Description
Using autoloads as global state containers creates tight coupling, makes testing difficult, and causes issues with scene reloading. Player health in an autoload persists across game restarts.
Symptom
- Restarting game doesn't reset state
- Difficult to write unit tests
- Changing one autoload breaks many scripts
- Circular dependencies between autoloads
Solution
# BAD: Global state autoload
# Global.gd
var player_health = 100
var player_coins = 0
# GOOD: State on actual objects
# Player.gd
extends CharacterBody2D
var health: int = 100
var coins: int = 0
# Use autoloads for:
# - EventBus (signals only, no state)
# - SaveManager (load/save, transient)
# - AudioManager (plays sounds, no game state)
# - SceneManager (transitions, no game state)Tags
- architecture
- state-management
- testing
Using _process for Physics
Id
physics-process-vs-process
Severity
high
Category
physics
Description
_process runs every visual frame (variable rate). _physics_process runs at fixed intervals (default 60 Hz). Physics calculations in _process cause jitter, tunneling, and non-deterministic behavior.
Symptom
- Movement speed varies with frame rate
- Objects pass through walls at low FPS
- Multiplayer desync
- Physics behave differently on different machines
Solution
# WRONG: Physics in _process
func _process(delta: float) -> void:
velocity += gravity * delta
move_and_slide()
# CORRECT: Physics in _physics_process
func _physics_process(delta: float) -> void:
velocity += gravity * delta
move_and_slide()
# _process is for:
# - Visual updates (sprite animation)
# - UI updates
# - Audio triggers
# - Non-gameplay timersTags
- physics
- determinism
- framerate
@export Variable Pitfalls
Id
export-variable-gotchas
Severity
medium
Category
gdscript
Description
Exported variables have subtle behaviors: default values in code can be overridden by scene values, resources are shared by default, and some types don't export well.
Symptom
- Changing default in code doesn't affect existing scenes
- All instances share the same resource/array
- Exported dictionaries don't save properly
Solution
# Issue 1: Default override
@export var speed: float = 100.0 # Changed to 200.0 in code
# Existing scenes still have 100.0 saved!
# Fix: Reset in inspector or delete .tscn and recreate
# Issue 2: Shared resources
@export var inventory: Array = [] # Shared across instances!
# Fix: Initialize in _ready
var inventory: Array
func _ready() -> void:
inventory = []
# Issue 3: Resource sharing
@export var stats: Resource # Same instance if not unique
# Fix: Make unique in inspector OR:
func _ready() -> void:
stats = stats.duplicate()
# Issue 4: Complex types
@export var data: Dictionary # Limited editor support
# Fix: Use custom Resource class insteadTags
- export
- inspector
- resources
GDScript vs C# Tradeoffs
Id
gdscript-vs-csharp
Severity
medium
Category
language-choice
Description
GDScript is tightly integrated with Godot but slower than C#. C# has better performance and tooling but requires more setup and has some engine integration quirks.
Considerations
Gdscript Pros
- Native integration, hot reload works perfectly
- Simpler syntax, faster prototyping
- Smaller build sizes
- Better documentation and community examples
- No external dependencies
Gdscript Cons
- Slower execution (10-100x vs C#)
- Limited static analysis
- No shared code with other platforms
Csharp Pros
- Better performance for heavy computation
- Excellent IDE support (VS, Rider)
- Share code with server/other projects
- Strong typing, better refactoring
Csharp Cons
- Hot reload issues in Godot 4
- Larger export sizes (.NET runtime)
- Some API differences from GDScript
- Fewer community examples
Recommendation
Use GDScript for most game code. Use C# for:
- Complex AI calculations
- Procedural generation algorithms
- Server-shared game logic
- Large team projects needing strict typing
TileMap Performance Issues
Id
tilemap-performance
Severity
medium
Category
performance
Description
Large TileMaps with many layers or complex tile data can cause performance issues. Runtime tile modification is expensive.
Symptom
- Low FPS with large maps
- Stuttering when modifying tiles
- Long load times for tile-heavy scenes
Solution
# 1. Use multiple TileMapLayers instead of one TileMap with layers
# (Godot 4.3+ TileMapLayer is faster)
# 2. Chunk large maps
# Only load visible chunks
# 3. Batch tile operations
# BAD: Set tiles one by one
for x in 1000:
for y in 1000:
tilemap.set_cell(0, Vector2i(x, y), source, atlas)
# BETTER: Use set_cells_terrain_connect for terrain
# Or queue changes and apply in batches
# 4. Disable navigation/physics on decorative layers
# In TileSet, only enable collision on necessary layersTags
- performance
- tilemap
- optimization
Scene Tree Processing Order
Id
scene-tree-order
Severity
medium
Category
lifecycle
Description
Nodes process in tree order (top to bottom). If node A depends on node B's state and B is below A, A sees stale data.
Symptom
- One-frame delays in reactions
- Inconsistent behavior depending on scene structure
- "Teleporting" objects
Solution
# The tree processes top-to-bottom:
# Player (processes first)
# Enemy (processes second, sees Player's NEW position)
# UI (processes last, sees current state)
# If order matters:
# 1. Rearrange nodes in tree
# 2. Use process_priority (lower = earlier)
func _ready() -> void:
process_priority = -1 # Process before default (0)
# 3. Use signals for guaranteed timing
# 4. Use call_deferred for next-frame operations
call_deferred("late_update")Tags
- lifecycle
- ordering
- timing
Input Handling Anti-patterns
Id
input-handling-mistakes
Severity
medium
Category
input
Description
Common mistakes with Godot's input system: not using Input Map, checking input in wrong callbacks, and missing _unhandled_input.
Symptom
- Input "eaten" by UI
- Actions fire multiple times
- Input doesn't work in certain scenes
Solution
# 1. Use Input Map (Project Settings > Input Map)
# DON'T hardcode keys
if Input.is_key_pressed(KEY_SPACE): # Bad
if Input.is_action_pressed("jump"): # Good
# 2. Choose correct callback
# _input: ALL input, including handled
# _unhandled_input: Input not consumed by UI
# _physics_process: Poll-based input for movement
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
interact()
get_viewport().set_input_as_handled()
func _physics_process(delta: float) -> void:
# Movement input (continuous)
var direction = Input.get_vector("left", "right", "up", "down")
# 3. UI blocking input
# Control nodes consume input by default
# Use mouse_filter = MOUSE_FILTER_IGNORE on overlaysTags
- input
- ui
- events
Shader Performance Gotchas
Id
shader-performance
Severity
medium
Category
rendering
Description
Shaders can tank performance if not careful. Texture reads in loops, complex math per pixel, and too many uniforms hurt FPS.
Symptom
- GPU-bound performance (profiler shows high GPU time)
- Frame rate drops with shader effects
- Mobile devices struggle with effects
Solution
// 1. Minimize texture reads
// BAD: Sample in loop
for (int i = 0; i < 10; i++) {
color += texture(tex, uv + offset * float(i));
}
// GOOD: Precompute, use texture LOD
color = textureLod(tex, uv, 2.0); // Lower LOD = faster
// 2. Avoid branching
// BAD: if/else per pixel
if (condition) { ... }
// GOOD: Use mix/step
color = mix(color1, color2, step(0.5, value));
// 3. Reduce overdraw
// Use DEPTH_TEST when possible
// Sort transparent objects back-to-front
// 4. Use simpler shaders on mobile
// Check: if ANDROID or if IOSTags
- shaders
- performance
- gpu
Resource Loading Stalls
Id
resource-preloading
Severity
high
Category
performance
Description
load() and preload() block the main thread. Loading large resources during gameplay causes stuttering. Use background loading.
Symptom
- Game freezes when entering new areas
- Stuttering when spawning enemies
- Long transitions between scenes
Solution
# preload() - Loads at script parse time (good for small, always-used)
const BulletScene = preload("res://bullet.tscn")
# load() - Loads when called (blocks!)
var resource = load("res://big_texture.png") # Stalls!
# Background loading (non-blocking)
func load_level_async(path: String) -> void:
ResourceLoader.load_threaded_request(path)
func _process(delta: float) -> void:
var status = ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
var level = ResourceLoader.load_threaded_get(path)
get_tree().change_scene_to_packed(level)
# Preload during loading screen
# Use ResourceLoader.load_threaded_request for each asset
# Show progress with load_threaded_get_statusTags
- loading
- performance
- threading
Godot Development - Validations
get_node() called in _process
Id
get-node-in-process
Description
Calling get_node() every frame is expensive. Cache the reference in @onready.
Severity
warning
Category
performance
File Patterns
- *.gd
Pattern
func\s+_(?:physics_)?process\s\([^)]\)\s(?:->\s\w+)?\s:[^}]get_node\s*\(
Multiline
Fix Hint
Replace with @onready:
@onready var my_node: Node = get_node("path/to/node")
func _process(delta: float) -> void:
my_node.do_something()$ operator in _process
Id
dollar-in-process
Description
Using $ in _process traverses the tree every frame. Cache with @onready.
Severity
warning
Category
performance
File Patterns
- *.gd
Pattern
func\s+_(?:physics_)?process\s\([^)]\)\s(?:->\s\w+)?\s:[^}]\$\w+
Multiline
Fix Hint
Cache node reference with @onready var node_name: Type = $NodePath
get_nodes_in_group() in _process
Id
get-nodes-in-group-process
Description
Getting nodes in group every frame is expensive. Cache the array.
Severity
warning
Category
performance
File Patterns
- *.gd
Pattern
func\s+_(?:physics_)?process\s\([^)]\)[^}]get_nodes_in_group\s\(
Multiline
Fix Hint
Cache the array and update when nodes change:
var enemies: Array[Node]
func _ready() -> void:
enemies = get_tree().get_nodes_in_group("enemies")load() called in _process
Id
load-in-process
Description
load() blocks the main thread. Use preload() or ResourceLoader.load_threaded_request().
Severity
error
Category
performance
File Patterns
- *.gd
Pattern
func\s+_(?:physics_)?process\s\([^)]\)[^}][^pre]load\s\(
Multiline
Fix Hint
Use preload() for static resources or ResourceLoader.load_threaded_request() for dynamic loading.
Physics operations in _process
Id
physics-in-process
Description
move_and_slide() and velocity changes should be in _physics_process for deterministic behavior.
Severity
error
Category
physics
File Patterns
- *.gd
Pattern
func\s+_process\s\([^)]\)[^}]move_and_slide\s\(
Multiline
Fix Hint
Move physics code to _physics_process(delta: float) -> void
Velocity modified in _process
Id
velocity-in-process
Description
Velocity should be modified in _physics_process for consistent physics.
Severity
warning
Category
physics
File Patterns
- *.gd
Pattern
func\s+_process\s\([^)]\)[^}]velocity\s[+\-*/]?=
Multiline
Fix Hint
Move velocity calculations to _physics_process
Accessing children in _enter_tree
Id
child-access-enter-tree
Description
_enter_tree runs before children are ready. Use _ready for child access.
Severity
error
Category
lifecycle
File Patterns
- *.gd
Pattern
func\s+_enter_tree\s\([^)]\)[^}]*(?:get_node|get_child|\$)
Multiline
Fix Hint
Move child access to _ready() where children are guaranteed to exist.
Signal connected without disconnect
Id
signal-no-disconnect
Description
Signals to autoloads or persistent nodes should be disconnected in _exit_tree.
Severity
info
Category
memory
File Patterns
- *.gd
Pattern
\.connect\s\([^)]+\)(?!.\.disconnect)
Fix Hint
Add disconnect in _exit_tree:
func _exit_tree() -> void:
if signal_source.my_signal.is_connected(_my_handler):
signal_source.my_signal.disconnect(_my_handler)Exported array with empty default
Id
exported-empty-array
Description
Exported arrays with [] default are shared across instances. Initialize in _ready.
Severity
warning
Category
gdscript
File Patterns
- *.gd
Pattern
@export\s+var\s+\w+\s:\sArray\s=\s\[\]
Fix Hint
Initialize in _ready instead:
@export var items: Array # No default
func _ready() -> void:
items = []Exported dictionary with empty default
Id
exported-empty-dict
Description
Exported dictionaries with {} default are shared across instances.
Severity
warning
Category
gdscript
File Patterns
- *.gd
Pattern
@export\s+var\s+\w+\s:\sDictionary\s=\s\{\}
Fix Hint
Initialize dictionaries in _ready() or use a custom Resource.
Hardcoded key constants
Id
hardcoded-keys
Description
Use Input Map actions instead of hardcoded keys for remappable controls.
Severity
warning
Category
input
File Patterns
- *.gd
Pattern
is_key_pressed\s\(\sKEY_
Fix Hint
Use Input Map:
# In Project Settings > Input Map, add action "jump" with KEY_SPACE
if Input.is_action_pressed("jump"):Hardcoded mouse button check
Id
hardcoded-mouse-buttons
Description
Use Input Map actions for mouse buttons for consistency and remapping.
Severity
info
Category
input
File Patterns
- *.gd
Pattern
is_mouse_button_pressed\s\(\sMOUSE_BUTTON_
Fix Hint
Add mouse actions to Input Map (e.g., 'shoot' -> MOUSE_BUTTON_LEFT)
Untyped function parameters
Id
untyped-function-params
Description
Add type hints to function parameters for better error detection and autocompletion.
Severity
info
Category
gdscript
File Patterns
- *.gd
Pattern
func\s+\w+\s\(\s\w+(?:\s,\s\w+)\s\)\s*(?:->|:)
Negative Pattern
func\s+\w+\s\(\s\w+\s:\s\w+
Fix Hint
Add type hints:
func take_damage(amount: int, source: Node) -> void:Function missing return type
Id
untyped-return
Description
Add return type hints to functions for clarity and error detection.
Severity
info
Category
gdscript
File Patterns
- *.gd
Pattern
func\s+\w+\s\([^)]\)\s*:
Negative Pattern
func\s+\w+\s\([^)]\)\s->\s\w+
Fix Hint
Add return type:
func get_health() -> int:
return current_healthHardcoded relative node path
Id
hardcoded-node-path
Description
Deep relative paths break easily. Use groups, unique names (%), or exports.
Severity
info
Category
architecture
File Patterns
- *.gd
Pattern
get_node\s\(\s["']\.\./
Fix Hint
Use alternatives:
# Groups
var player = get_tree().get_first_node_in_group("player")
# Unique names (set % in editor)
@onready var hud = %HUD
# Exports
@export var target: Nodeawait in loop without check
Id
await-in-loop
Description
Awaiting in loops can cause issues if the object is freed during wait.
Severity
warning
Category
async
File Patterns
- *.gd
Pattern
(?:for|while)[^:]:[^}]await
Multiline
Fix Hint
Check validity after await:
for item in items:
await some_signal
if not is_instance_valid(self):
return
# Continue processingDisabling process incorrectly
Id
set-process-false
Description
set_process(false) only affects _process, not _physics_process.
Severity
info
Category
lifecycle
File Patterns
- *.gd
Pattern
set_process\s\(\sfalse\s*\)
Fix Hint
To disable all processing:
set_process(false)
set_physics_process(false)
# Or use process_mode = PROCESS_MODE_DISABLEDCode after queue_free()
Id
queue-free-continue
Description
queue_free() doesn't immediately free the node. Add return after queue_free().
Severity
warning
Category
lifecycle
File Patterns
- *.gd
Pattern
queue_free\s\(\s\)[^\n]\n[^\n][^\s\n]
Fix Hint
Add return after queue_free:
func die() -> void:
play_death_effect()
queue_free()
return # Prevent further executionInstantiating scene without type check
Id
direct-instance-scene
Description
Use typed instantiate for better safety and autocompletion.
Severity
info
Category
gdscript
File Patterns
- *.gd
Pattern
\.instantiate\s\(\s\)(?!\s+as\s+)
Fix Hint
Use typed instantiate:
var enemy := enemy_scene.instantiate() as EnemyDiscarding pixels in shader
Id
shader-discard-alpha
Description
discard breaks GPU optimizations. Consider alpha testing instead.
Severity
info
Category
rendering
File Patterns
- *.gdshader
Pattern
discard\s*;
Fix Hint
For simple alpha cutoff, use:
ALPHA = step(0.5, texture_color.a);
// Or configure material for alpha scissor