godot-gdscript-patterns
Master Godot 4 GDScript patterns including signals, scenes, state machines, and optimization. Use when building Godot games, implementing game systems, or learning GDScript best practices.
Best use case
godot-gdscript-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Master Godot 4 GDScript patterns including signals, scenes, state machines, and optimization. Use when building Godot games, implementing game systems, or learning GDScript best practices.
Teams using godot-gdscript-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/godot-gdscript-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How godot-gdscript-patterns Compares
| Feature / Agent | godot-gdscript-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Master Godot 4 GDScript patterns including signals, scenes, state machines, and optimization. Use when building Godot games, implementing game systems, or learning GDScript best practices.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Godot GDScript Patterns
Production patterns for Godot 4.x game development with GDScript, covering architecture, signals, scenes, and optimization.
## When to Use This Skill
- Building games with Godot 4
- Implementing game systems in GDScript
- Designing scene architecture
- Managing game state
- Optimizing GDScript performance
- Learning Godot best practices
## Core Concepts
### 1. Godot Architecture
```
Node: Base building block
├── Scene: Reusable node tree (saved as .tscn)
├── Resource: Data container (saved as .tres)
├── Signal: Event communication
└── Group: Node categorization
```
### 2. GDScript Basics
```gdscript
class_name Player
extends CharacterBody2D
# Signals
signal health_changed(new_health: int)
signal died
# Exports (Inspector-editable)
@export var speed: float = 200.0
@export var max_health: int = 100
@export_range(0, 1) var damage_reduction: float = 0.0
@export_group("Combat")
@export var attack_damage: int = 10
@export var attack_cooldown: float = 0.5
# Onready (initialized when ready)
@onready var sprite: Sprite2D = $Sprite2D
@onready var animation: AnimationPlayer = $AnimationPlayer
@onready var hitbox: Area2D = $Hitbox
# Private variables (convention: underscore prefix)
var _health: int
var _can_attack: bool = true
func _ready() -> void:
_health = max_health
func _physics_process(delta: float) -> void:
var direction := Input.get_vector("left", "right", "up", "down")
velocity = direction * speed
move_and_slide()
func take_damage(amount: int) -> void:
var actual_damage := int(amount * (1.0 - damage_reduction))
_health = max(_health - actual_damage, 0)
health_changed.emit(_health)
if _health <= 0:
died.emit()
```
## Patterns
### Pattern 1: State Machine
```gdscript
# state_machine.gd
class_name StateMachine
extends Node
signal state_changed(from_state: StringName, to_state: StringName)
@export var initial_state: State
var current_state: State
var states: Dictionary = {}
func _ready() -> void:
# Register all State children
for child in get_children():
if child is State:
states[child.name] = child
child.state_machine = self
child.process_mode = Node.PROCESS_MODE_DISABLED
# Start initial state
if initial_state:
current_state = initial_state
current_state.process_mode = Node.PROCESS_MODE_INHERIT
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 _unhandled_input(event: InputEvent) -> void:
if current_state:
current_state.handle_input(event)
func transition_to(state_name: StringName, msg: Dictionary = {}) -> void:
if not states.has(state_name):
push_error("State '%s' not found" % state_name)
return
var previous_state := current_state
previous_state.exit()
previous_state.process_mode = Node.PROCESS_MODE_DISABLED
current_state = states[state_name]
current_state.process_mode = Node.PROCESS_MODE_INHERIT
current_state.enter(msg)
state_changed.emit(previous_state.name, current_state.name)
```
```gdscript
# state.gd
class_name State
extends Node
var state_machine: StateMachine
func enter(_msg: Dictionary = {}) -> void:
pass
func exit() -> void:
pass
func update(_delta: float) -> void:
pass
func physics_update(_delta: float) -> void:
pass
func handle_input(_event: InputEvent) -> void:
pass
```
```gdscript
# player_idle.gd
class_name PlayerIdle
extends State
@export var player: Player
func enter(_msg: Dictionary = {}) -> void:
player.animation.play("idle")
func physics_update(_delta: float) -> void:
var direction := Input.get_vector("left", "right", "up", "down")
if direction != Vector2.ZERO:
state_machine.transition_to("Move")
func handle_input(event: InputEvent) -> void:
if event.is_action_pressed("attack"):
state_machine.transition_to("Attack")
elif event.is_action_pressed("jump"):
state_machine.transition_to("Jump")
```
### Pattern 2: Autoload Singletons
```gdscript
# game_manager.gd (Add to Project Settings > Autoload)
extends Node
signal game_started
signal game_paused(is_paused: bool)
signal game_over(won: bool)
signal score_changed(new_score: int)
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
var state: GameState = GameState.MENU
var score: int = 0:
set(value):
score = value
score_changed.emit(score)
var high_score: int = 0
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
_load_high_score()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("pause") and state == GameState.PLAYING:
toggle_pause()
func start_game() -> void:
score = 0
state = GameState.PLAYING
game_started.emit()
func toggle_pause() -> void:
var is_paused := state != GameState.PAUSED
if is_paused:
state = GameState.PAUSED
get_tree().paused = true
else:
state = GameState.PLAYING
get_tree().paused = false
game_paused.emit(is_paused)
func end_game(won: bool) -> void:
state = GameState.GAME_OVER
if score > high_score:
high_score = score
_save_high_score()
game_over.emit(won)
func add_score(points: int) -> void:
score += points
func _load_high_score() -> void:
if FileAccess.file_exists("user://high_score.save"):
var file := FileAccess.open("user://high_score.save", FileAccess.READ)
high_score = file.get_32()
func _save_high_score() -> void:
var file := FileAccess.open("user://high_score.save", FileAccess.WRITE)
file.store_32(high_score)
```
```gdscript
# event_bus.gd (Global signal bus)
extends Node
# Player events
signal player_spawned(player: Node2D)
signal player_died(player: Node2D)
signal player_health_changed(health: int, max_health: int)
# Enemy events
signal enemy_spawned(enemy: Node2D)
signal enemy_died(enemy: Node2D, position: Vector2)
# Item events
signal item_collected(item_type: StringName, value: int)
signal powerup_activated(powerup_type: StringName)
# Level events
signal level_started(level_number: int)
signal level_completed(level_number: int, time: float)
signal checkpoint_reached(checkpoint_id: int)
```
### Pattern 3: Resource-based Data
```gdscript
# weapon_data.gd
class_name WeaponData
extends Resource
@export var name: StringName
@export var damage: int
@export var attack_speed: float
@export var range: float
@export_multiline var description: String
@export var icon: Texture2D
@export var projectile_scene: PackedScene
@export var sound_attack: AudioStream
```
```gdscript
# character_stats.gd
class_name CharacterStats
extends Resource
signal stat_changed(stat_name: StringName, new_value: float)
@export var max_health: float = 100.0
@export var attack: float = 10.0
@export var defense: float = 5.0
@export var speed: float = 200.0
# Runtime values (not saved)
var _current_health: float
func _init() -> void:
_current_health = max_health
func get_current_health() -> float:
return _current_health
func take_damage(amount: float) -> float:
var actual_damage := maxf(amount - defense, 1.0)
_current_health = maxf(_current_health - actual_damage, 0.0)
stat_changed.emit("health", _current_health)
return actual_damage
func heal(amount: float) -> void:
_current_health = minf(_current_health + amount, max_health)
stat_changed.emit("health", _current_health)
func duplicate_for_runtime() -> CharacterStats:
var copy := duplicate() as CharacterStats
copy._current_health = copy.max_health
return copy
```
```gdscript
# Using resources
class_name Character
extends CharacterBody2D
@export var base_stats: CharacterStats
@export var weapon: WeaponData
var stats: CharacterStats
func _ready() -> void:
# Create runtime copy to avoid modifying the resource
stats = base_stats.duplicate_for_runtime()
stats.stat_changed.connect(_on_stat_changed)
func attack() -> void:
if weapon:
print("Attacking with %s for %d damage" % [weapon.name, weapon.damage])
func _on_stat_changed(stat_name: StringName, value: float) -> void:
if stat_name == "health" and value <= 0:
die()
```
### Pattern 4: Object Pooling
```gdscript
# object_pool.gd
class_name ObjectPool
extends Node
@export var pooled_scene: PackedScene
@export var initial_size: int = 10
@export var can_grow: bool = true
var _available: Array[Node] = []
var _in_use: Array[Node] = []
func _ready() -> void:
_initialize_pool()
func _initialize_pool() -> void:
for i in initial_size:
_create_instance()
func _create_instance() -> Node:
var instance := pooled_scene.instantiate()
instance.process_mode = Node.PROCESS_MODE_DISABLED
instance.visible = false
add_child(instance)
_available.append(instance)
# Connect return signal if exists
if instance.has_signal("returned_to_pool"):
instance.returned_to_pool.connect(_return_to_pool.bind(instance))
return instance
func get_instance() -> Node:
var instance: Node
if _available.is_empty():
if can_grow:
instance = _create_instance()
_available.erase(instance)
else:
push_warning("Pool exhausted and cannot grow")
return null
else:
instance = _available.pop_back()
instance.process_mode = Node.PROCESS_MODE_INHERIT
instance.visible = true
_in_use.append(instance)
if instance.has_method("on_spawn"):
instance.on_spawn()
return instance
func _return_to_pool(instance: Node) -> void:
if not instance in _in_use:
return
_in_use.erase(instance)
if instance.has_method("on_despawn"):
instance.on_despawn()
instance.process_mode = Node.PROCESS_MODE_DISABLED
instance.visible = false
_available.append(instance)
func return_all() -> void:
for instance in _in_use.duplicate():
_return_to_pool(instance)
```
```gdscript
# pooled_bullet.gd
class_name PooledBullet
extends Area2D
signal returned_to_pool
@export var speed: float = 500.0
@export var lifetime: float = 5.0
var direction: Vector2
var _timer: float
func on_spawn() -> void:
_timer = lifetime
func on_despawn() -> void:
direction = Vector2.ZERO
func initialize(pos: Vector2, dir: Vector2) -> void:
global_position = pos
direction = dir.normalized()
rotation = direction.angle()
func _physics_process(delta: float) -> void:
position += direction * speed * delta
_timer -= delta
if _timer <= 0:
returned_to_pool.emit()
func _on_body_entered(body: Node2D) -> void:
if body.has_method("take_damage"):
body.take_damage(10)
returned_to_pool.emit()
```
### Pattern 5: Component System
```gdscript
# health_component.gd
class_name HealthComponent
extends Node
signal health_changed(current: int, maximum: int)
signal damaged(amount: int, source: Node)
signal healed(amount: int)
signal died
@export var max_health: int = 100
@export var invincibility_time: float = 0.0
var current_health: int:
set(value):
var old := current_health
current_health = clampi(value, 0, max_health)
if current_health != old:
health_changed.emit(current_health, max_health)
var _invincible: bool = false
func _ready() -> void:
current_health = max_health
func take_damage(amount: int, source: Node = null) -> int:
if _invincible or current_health <= 0:
return 0
var actual := mini(amount, current_health)
current_health -= actual
damaged.emit(actual, source)
if current_health <= 0:
died.emit()
elif invincibility_time > 0:
_start_invincibility()
return actual
func heal(amount: int) -> int:
var actual := mini(amount, max_health - current_health)
current_health += actual
if actual > 0:
healed.emit(actual)
return actual
func _start_invincibility() -> void:
_invincible = true
await get_tree().create_timer(invincibility_time).timeout
_invincible = false
```
```gdscript
# hitbox_component.gd
class_name HitboxComponent
extends Area2D
signal hit(hurtbox: HurtboxComponent)
@export var damage: int = 10
@export var knockback_force: float = 200.0
var owner_node: Node
func _ready() -> void:
owner_node = get_parent()
area_entered.connect(_on_area_entered)
func _on_area_entered(area: Area2D) -> void:
if area is HurtboxComponent:
var hurtbox := area as HurtboxComponent
if hurtbox.owner_node != owner_node:
hit.emit(hurtbox)
hurtbox.receive_hit(self)
```
```gdscript
# hurtbox_component.gd
class_name HurtboxComponent
extends Area2D
signal hurt(hitbox: HitboxComponent)
@export var health_component: HealthComponent
var owner_node: Node
func _ready() -> void:
owner_node = get_parent()
func receive_hit(hitbox: HitboxComponent) -> void:
hurt.emit(hitbox)
if health_component:
health_component.take_damage(hitbox.damage, hitbox.owner_node)
```
### Pattern 6: Scene Management
```gdscript
# scene_manager.gd (Autoload)
extends Node
signal scene_loading_started(scene_path: String)
signal scene_loading_progress(progress: float)
signal scene_loaded(scene: Node)
signal transition_started
signal transition_finished
@export var transition_scene: PackedScene
@export var loading_scene: PackedScene
var _current_scene: Node
var _transition: CanvasLayer
var _loader: ResourceLoader
func _ready() -> void:
_current_scene = get_tree().current_scene
if transition_scene:
_transition = transition_scene.instantiate()
add_child(_transition)
_transition.visible = false
func change_scene(scene_path: String, with_transition: bool = true) -> void:
if with_transition:
await _play_transition_out()
_load_scene(scene_path)
func change_scene_packed(scene: PackedScene, with_transition: bool = true) -> void:
if with_transition:
await _play_transition_out()
_swap_scene(scene.instantiate())
func _load_scene(path: String) -> void:
scene_loading_started.emit(path)
# Check if already loaded
if ResourceLoader.has_cached(path):
var scene := load(path) as PackedScene
_swap_scene(scene.instantiate())
return
# Async loading
ResourceLoader.load_threaded_request(path)
while true:
var progress := []
var status := ResourceLoader.load_threaded_get_status(path, progress)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
scene_loading_progress.emit(progress[0])
await get_tree().process_frame
ResourceLoader.THREAD_LOAD_LOADED:
var scene := ResourceLoader.load_threaded_get(path) as PackedScene
_swap_scene(scene.instantiate())
return
_:
push_error("Failed to load scene: %s" % path)
return
func _swap_scene(new_scene: Node) -> void:
if _current_scene:
_current_scene.queue_free()
_current_scene = new_scene
get_tree().root.add_child(_current_scene)
get_tree().current_scene = _current_scene
scene_loaded.emit(_current_scene)
await _play_transition_in()
func _play_transition_out() -> void:
if not _transition:
return
transition_started.emit()
_transition.visible = true
if _transition.has_method("transition_out"):
await _transition.transition_out()
else:
await get_tree().create_timer(0.3).timeout
func _play_transition_in() -> void:
if not _transition:
transition_finished.emit()
return
if _transition.has_method("transition_in"):
await _transition.transition_in()
else:
await get_tree().create_timer(0.3).timeout
_transition.visible = false
transition_finished.emit()
```
### Pattern 7: Save System
```gdscript
# save_manager.gd (Autoload)
extends Node
const SAVE_PATH := "user://savegame.save"
const ENCRYPTION_KEY := "your_secret_key_here"
signal save_completed
signal load_completed
signal save_error(message: String)
func save_game(data: Dictionary) -> void:
var file := FileAccess.open_encrypted_with_pass(
SAVE_PATH,
FileAccess.WRITE,
ENCRYPTION_KEY
)
if file == null:
save_error.emit("Could not open save file")
return
var json := JSON.stringify(data)
file.store_string(json)
file.close()
save_completed.emit()
func load_game() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var file := FileAccess.open_encrypted_with_pass(
SAVE_PATH,
FileAccess.READ,
ENCRYPTION_KEY
)
if file == null:
save_error.emit("Could not open save file")
return {}
var json := file.get_as_text()
file.close()
var parsed := JSON.parse_string(json)
if parsed == null:
save_error.emit("Could not parse save data")
return {}
load_completed.emit()
return parsed
func delete_save() -> void:
if FileAccess.file_exists(SAVE_PATH):
DirAccess.remove_absolute(SAVE_PATH)
func has_save() -> bool:
return FileAccess.file_exists(SAVE_PATH)
```
```gdscript
# saveable.gd (Attach to saveable nodes)
class_name Saveable
extends Node
@export var save_id: String
func _ready() -> void:
if save_id.is_empty():
save_id = str(get_path())
func get_save_data() -> Dictionary:
var parent := get_parent()
var data := {"id": save_id}
if parent is Node2D:
data["position"] = {"x": parent.position.x, "y": parent.position.y}
if parent.has_method("get_custom_save_data"):
data.merge(parent.get_custom_save_data())
return data
func load_save_data(data: Dictionary) -> void:
var parent := get_parent()
if data.has("position") and parent is Node2D:
parent.position = Vector2(data.position.x, data.position.y)
if parent.has_method("load_custom_save_data"):
parent.load_custom_save_data(data)
```
## Performance Tips
```gdscript
# 1. Cache node references
@onready var sprite := $Sprite2D # Good
# $Sprite2D in _process() # Bad - repeated lookup
# 2. Use object pooling for frequent spawning
# See Pattern 4
# 3. Avoid allocations in hot paths
var _reusable_array: Array = []
func _process(_delta: float) -> void:
_reusable_array.clear() # Reuse instead of creating new
# 4. Use static typing
func calculate(value: float) -> float: # Good
return value * 2.0
# 5. Disable processing when not needed
func _on_off_screen() -> void:
set_process(false)
set_physics_process(false)
```
## Best Practices
### Do's
- **Use signals for decoupling** - Avoid direct references
- **Type everything** - Static typing catches errors
- **Use resources for data** - Separate data from logic
- **Pool frequently spawned objects** - Avoid GC hitches
- **Use Autoloads sparingly** - Only for truly global systems
### Don'ts
- **Don't use `get_node()` in loops** - Cache references
- **Don't couple scenes tightly** - Use signals
- **Don't put logic in resources** - Keep them data-only
- **Don't ignore the Profiler** - Monitor performance
- **Don't fight the scene tree** - Work with Godot's design
## Resources
- [Godot Documentation](https://docs.godotengine.org/en/stable/)
- [GDQuest Tutorials](https://www.gdquest.com/)
- [Godot Recipes](https://kidscancode.org/godot_recipes/)Related Skills
Vitest Unit Patterns
> Design fast, isolated unit tests that validate business logic without network, database, or browser dependencies using Vitest.
Singleton Patterns
> Manage module-level state safely in serverless and edge environments using globalThis attachment, initialization guards, and connection pooling.
Server Component Patterns
> Design rendering strategies where Server Components are the default and client interactivity is pushed to the smallest possible boundary.
Prisma ORM Patterns
> Design database schemas, query patterns, and ORM configurations that are correct at the schema level, performant at the query level, and resilient at the connection level.
omega-gdscript-expert
Meta Godot/GDScript skill that composes all installed Godot skills, enforces MCP routing, and runs a self-evaluation loop for stable, high-performance, backward-compatible code.
Lazy Import Patterns
> Design import strategies that keep bundles small by loading optional dependencies only when they're actually needed.
godot-particles
Expert blueprint for GPU particle systems (explosions, magic effects, weather, trails) using GPUParticles2D/3D, ParticleProcessMaterial, gradients, sub-emitters, and custom shaders. Use when creating VFX, environmental effects, or visual feedback. Keywords GPUParticles2D, ParticleProcessMaterial, emission_shape, color_ramp, sub_emitter, one_shot.
godot-gdscript-mastery
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.
godot-debugging
Expert knowledge of Godot debugging, error interpretation, common bugs, and troubleshooting techniques. Use when helping fix Godot errors, crashes, or unexpected behavior.
godot-best-practices
Guide AI agents through Godot 4.x GDScript coding best practices including scene organization, signals, resources, state machines, and performance optimization. This skill should be used when generating GDScript code, creating Godot scenes, designing game architecture, implementing state machines, object pooling, save/load systems, or when the user asks about Godot patterns, node structure, or GDScript standards. Keywords: godot, gdscript, game development, signals, resources, scenes, nodes, state machine, object pooling, save system, autoload, export, type hints.
e2e-testing-patterns
Master end-to-end testing with Playwright and Cypress to build reliable test suites that catch bugs, improve confidence, and enable fast deployment. Use when implementing E2E tests, debugging flaky tests, or establishing testing standards.
django-orm-patterns
Use when Django ORM patterns with models, queries, and relationships. Use when building database-driven Django applications.