Godot Gdscript Patterns
Master production-ready Godot 4 GDScript patterns for scalable games
✨ The solution you've been looking for
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.
See It In Action
Interactive preview & real-world examples
AI Conversation Simulator
See how users interact with this skill
User Prompt
Help me implement a state machine for my player character in Godot that handles idle, movement, attack, and jump states with proper transitions
Skill Processing
Analyzing request...
Agent Response
Complete state machine implementation with base State class, StateMachine controller, and example player states with proper enter/exit logic
Quick Start (3 Steps)
Get up and running in minutes
Install
claude-code skill install godot-gdscript-patterns
claude-code skill install godot-gdscript-patternsConfig
First Trigger
@godot-gdscript-patterns helpCommands
| Command | Description | Required Args |
|---|---|---|
| @godot-gdscript-patterns implementing-player-state-management | Create a robust state machine for player character behaviors like idle, moving, attacking, and jumping with clean transitions. | None |
| @godot-gdscript-patterns setting-up-global-game-systems | Design autoload singletons for game management, event communication, and persistent data across scenes. | None |
| @godot-gdscript-patterns optimizing-performance-with-object-pooling | Implement object pooling for frequently spawned game objects like bullets, enemies, or particles to eliminate garbage collection hitches. | None |
Typical Use Cases
Implementing Player State Management
Create a robust state machine for player character behaviors like idle, moving, attacking, and jumping with clean transitions.
Setting Up Global Game Systems
Design autoload singletons for game management, event communication, and persistent data across scenes.
Optimizing Performance with Object Pooling
Implement object pooling for frequently spawned game objects like bullets, enemies, or particles to eliminate garbage collection hitches.
Overview
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
1class_name Player
2extends CharacterBody2D
3
4# Signals
5signal health_changed(new_health: int)
6signal died
7
8# Exports (Inspector-editable)
9@export var speed: float = 200.0
10@export var max_health: int = 100
11@export_range(0, 1) var damage_reduction: float = 0.0
12@export_group("Combat")
13@export var attack_damage: int = 10
14@export var attack_cooldown: float = 0.5
15
16# Onready (initialized when ready)
17@onready var sprite: Sprite2D = $Sprite2D
18@onready var animation: AnimationPlayer = $AnimationPlayer
19@onready var hitbox: Area2D = $Hitbox
20
21# Private variables (convention: underscore prefix)
22var _health: int
23var _can_attack: bool = true
24
25func _ready() -> void:
26 _health = max_health
27
28func _physics_process(delta: float) -> void:
29 var direction := Input.get_vector("left", "right", "up", "down")
30 velocity = direction * speed
31 move_and_slide()
32
33func take_damage(amount: int) -> void:
34 var actual_damage := int(amount * (1.0 - damage_reduction))
35 _health = max(_health - actual_damage, 0)
36 health_changed.emit(_health)
37
38 if _health <= 0:
39 died.emit()
Patterns
Pattern 1: State Machine
1# state_machine.gd
2class_name StateMachine
3extends Node
4
5signal state_changed(from_state: StringName, to_state: StringName)
6
7@export var initial_state: State
8
9var current_state: State
10var states: Dictionary = {}
11
12func _ready() -> void:
13 # Register all State children
14 for child in get_children():
15 if child is State:
16 states[child.name] = child
17 child.state_machine = self
18 child.process_mode = Node.PROCESS_MODE_DISABLED
19
20 # Start initial state
21 if initial_state:
22 current_state = initial_state
23 current_state.process_mode = Node.PROCESS_MODE_INHERIT
24 current_state.enter()
25
26func _process(delta: float) -> void:
27 if current_state:
28 current_state.update(delta)
29
30func _physics_process(delta: float) -> void:
31 if current_state:
32 current_state.physics_update(delta)
33
34func _unhandled_input(event: InputEvent) -> void:
35 if current_state:
36 current_state.handle_input(event)
37
38func transition_to(state_name: StringName, msg: Dictionary = {}) -> void:
39 if not states.has(state_name):
40 push_error("State '%s' not found" % state_name)
41 return
42
43 var previous_state := current_state
44 previous_state.exit()
45 previous_state.process_mode = Node.PROCESS_MODE_DISABLED
46
47 current_state = states[state_name]
48 current_state.process_mode = Node.PROCESS_MODE_INHERIT
49 current_state.enter(msg)
50
51 state_changed.emit(previous_state.name, current_state.name)
1# state.gd
2class_name State
3extends Node
4
5var state_machine: StateMachine
6
7func enter(_msg: Dictionary = {}) -> void:
8 pass
9
10func exit() -> void:
11 pass
12
13func update(_delta: float) -> void:
14 pass
15
16func physics_update(_delta: float) -> void:
17 pass
18
19func handle_input(_event: InputEvent) -> void:
20 pass
1# player_idle.gd
2class_name PlayerIdle
3extends State
4
5@export var player: Player
6
7func enter(_msg: Dictionary = {}) -> void:
8 player.animation.play("idle")
9
10func physics_update(_delta: float) -> void:
11 var direction := Input.get_vector("left", "right", "up", "down")
12
13 if direction != Vector2.ZERO:
14 state_machine.transition_to("Move")
15
16func handle_input(event: InputEvent) -> void:
17 if event.is_action_pressed("attack"):
18 state_machine.transition_to("Attack")
19 elif event.is_action_pressed("jump"):
20 state_machine.transition_to("Jump")
Pattern 2: Autoload Singletons
1# game_manager.gd (Add to Project Settings > Autoload)
2extends Node
3
4signal game_started
5signal game_paused(is_paused: bool)
6signal game_over(won: bool)
7signal score_changed(new_score: int)
8
9enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
10
11var state: GameState = GameState.MENU
12var score: int = 0:
13 set(value):
14 score = value
15 score_changed.emit(score)
16
17var high_score: int = 0
18
19func _ready() -> void:
20 process_mode = Node.PROCESS_MODE_ALWAYS
21 _load_high_score()
22
23func _input(event: InputEvent) -> void:
24 if event.is_action_pressed("pause") and state == GameState.PLAYING:
25 toggle_pause()
26
27func start_game() -> void:
28 score = 0
29 state = GameState.PLAYING
30 game_started.emit()
31
32func toggle_pause() -> void:
33 var is_paused := state != GameState.PAUSED
34
35 if is_paused:
36 state = GameState.PAUSED
37 get_tree().paused = true
38 else:
39 state = GameState.PLAYING
40 get_tree().paused = false
41
42 game_paused.emit(is_paused)
43
44func end_game(won: bool) -> void:
45 state = GameState.GAME_OVER
46
47 if score > high_score:
48 high_score = score
49 _save_high_score()
50
51 game_over.emit(won)
52
53func add_score(points: int) -> void:
54 score += points
55
56func _load_high_score() -> void:
57 if FileAccess.file_exists("user://high_score.save"):
58 var file := FileAccess.open("user://high_score.save", FileAccess.READ)
59 high_score = file.get_32()
60
61func _save_high_score() -> void:
62 var file := FileAccess.open("user://high_score.save", FileAccess.WRITE)
63 file.store_32(high_score)
1# event_bus.gd (Global signal bus)
2extends Node
3
4# Player events
5signal player_spawned(player: Node2D)
6signal player_died(player: Node2D)
7signal player_health_changed(health: int, max_health: int)
8
9# Enemy events
10signal enemy_spawned(enemy: Node2D)
11signal enemy_died(enemy: Node2D, position: Vector2)
12
13# Item events
14signal item_collected(item_type: StringName, value: int)
15signal powerup_activated(powerup_type: StringName)
16
17# Level events
18signal level_started(level_number: int)
19signal level_completed(level_number: int, time: float)
20signal checkpoint_reached(checkpoint_id: int)
Pattern 3: Resource-based Data
1# weapon_data.gd
2class_name WeaponData
3extends Resource
4
5@export var name: StringName
6@export var damage: int
7@export var attack_speed: float
8@export var range: float
9@export_multiline var description: String
10@export var icon: Texture2D
11@export var projectile_scene: PackedScene
12@export var sound_attack: AudioStream
1# character_stats.gd
2class_name CharacterStats
3extends Resource
4
5signal stat_changed(stat_name: StringName, new_value: float)
6
7@export var max_health: float = 100.0
8@export var attack: float = 10.0
9@export var defense: float = 5.0
10@export var speed: float = 200.0
11
12# Runtime values (not saved)
13var _current_health: float
14
15func _init() -> void:
16 _current_health = max_health
17
18func get_current_health() -> float:
19 return _current_health
20
21func take_damage(amount: float) -> float:
22 var actual_damage := maxf(amount - defense, 1.0)
23 _current_health = maxf(_current_health - actual_damage, 0.0)
24 stat_changed.emit("health", _current_health)
25 return actual_damage
26
27func heal(amount: float) -> void:
28 _current_health = minf(_current_health + amount, max_health)
29 stat_changed.emit("health", _current_health)
30
31func duplicate_for_runtime() -> CharacterStats:
32 var copy := duplicate() as CharacterStats
33 copy._current_health = copy.max_health
34 return copy
1# Using resources
2class_name Character
3extends CharacterBody2D
4
5@export var base_stats: CharacterStats
6@export var weapon: WeaponData
7
8var stats: CharacterStats
9
10func _ready() -> void:
11 # Create runtime copy to avoid modifying the resource
12 stats = base_stats.duplicate_for_runtime()
13 stats.stat_changed.connect(_on_stat_changed)
14
15func attack() -> void:
16 if weapon:
17 print("Attacking with %s for %d damage" % [weapon.name, weapon.damage])
18
19func _on_stat_changed(stat_name: StringName, value: float) -> void:
20 if stat_name == "health" and value <= 0:
21 die()
Pattern 4: Object Pooling
1# object_pool.gd
2class_name ObjectPool
3extends Node
4
5@export var pooled_scene: PackedScene
6@export var initial_size: int = 10
7@export var can_grow: bool = true
8
9var _available: Array[Node] = []
10var _in_use: Array[Node] = []
11
12func _ready() -> void:
13 _initialize_pool()
14
15func _initialize_pool() -> void:
16 for i in initial_size:
17 _create_instance()
18
19func _create_instance() -> Node:
20 var instance := pooled_scene.instantiate()
21 instance.process_mode = Node.PROCESS_MODE_DISABLED
22 instance.visible = false
23 add_child(instance)
24 _available.append(instance)
25
26 # Connect return signal if exists
27 if instance.has_signal("returned_to_pool"):
28 instance.returned_to_pool.connect(_return_to_pool.bind(instance))
29
30 return instance
31
32func get_instance() -> Node:
33 var instance: Node
34
35 if _available.is_empty():
36 if can_grow:
37 instance = _create_instance()
38 _available.erase(instance)
39 else:
40 push_warning("Pool exhausted and cannot grow")
41 return null
42 else:
43 instance = _available.pop_back()
44
45 instance.process_mode = Node.PROCESS_MODE_INHERIT
46 instance.visible = true
47 _in_use.append(instance)
48
49 if instance.has_method("on_spawn"):
50 instance.on_spawn()
51
52 return instance
53
54func _return_to_pool(instance: Node) -> void:
55 if not instance in _in_use:
56 return
57
58 _in_use.erase(instance)
59
60 if instance.has_method("on_despawn"):
61 instance.on_despawn()
62
63 instance.process_mode = Node.PROCESS_MODE_DISABLED
64 instance.visible = false
65 _available.append(instance)
66
67func return_all() -> void:
68 for instance in _in_use.duplicate():
69 _return_to_pool(instance)
1# pooled_bullet.gd
2class_name PooledBullet
3extends Area2D
4
5signal returned_to_pool
6
7@export var speed: float = 500.0
8@export var lifetime: float = 5.0
9
10var direction: Vector2
11var _timer: float
12
13func on_spawn() -> void:
14 _timer = lifetime
15
16func on_despawn() -> void:
17 direction = Vector2.ZERO
18
19func initialize(pos: Vector2, dir: Vector2) -> void:
20 global_position = pos
21 direction = dir.normalized()
22 rotation = direction.angle()
23
24func _physics_process(delta: float) -> void:
25 position += direction * speed * delta
26
27 _timer -= delta
28 if _timer <= 0:
29 returned_to_pool.emit()
30
31func _on_body_entered(body: Node2D) -> void:
32 if body.has_method("take_damage"):
33 body.take_damage(10)
34 returned_to_pool.emit()
Pattern 5: Component System
1# health_component.gd
2class_name HealthComponent
3extends Node
4
5signal health_changed(current: int, maximum: int)
6signal damaged(amount: int, source: Node)
7signal healed(amount: int)
8signal died
9
10@export var max_health: int = 100
11@export var invincibility_time: float = 0.0
12
13var current_health: int:
14 set(value):
15 var old := current_health
16 current_health = clampi(value, 0, max_health)
17 if current_health != old:
18 health_changed.emit(current_health, max_health)
19
20var _invincible: bool = false
21
22func _ready() -> void:
23 current_health = max_health
24
25func take_damage(amount: int, source: Node = null) -> int:
26 if _invincible or current_health <= 0:
27 return 0
28
29 var actual := mini(amount, current_health)
30 current_health -= actual
31 damaged.emit(actual, source)
32
33 if current_health <= 0:
34 died.emit()
35 elif invincibility_time > 0:
36 _start_invincibility()
37
38 return actual
39
40func heal(amount: int) -> int:
41 var actual := mini(amount, max_health - current_health)
42 current_health += actual
43 if actual > 0:
44 healed.emit(actual)
45 return actual
46
47func _start_invincibility() -> void:
48 _invincible = true
49 await get_tree().create_timer(invincibility_time).timeout
50 _invincible = false
1# hitbox_component.gd
2class_name HitboxComponent
3extends Area2D
4
5signal hit(hurtbox: HurtboxComponent)
6
7@export var damage: int = 10
8@export var knockback_force: float = 200.0
9
10var owner_node: Node
11
12func _ready() -> void:
13 owner_node = get_parent()
14 area_entered.connect(_on_area_entered)
15
16func _on_area_entered(area: Area2D) -> void:
17 if area is HurtboxComponent:
18 var hurtbox := area as HurtboxComponent
19 if hurtbox.owner_node != owner_node:
20 hit.emit(hurtbox)
21 hurtbox.receive_hit(self)
1# hurtbox_component.gd
2class_name HurtboxComponent
3extends Area2D
4
5signal hurt(hitbox: HitboxComponent)
6
7@export var health_component: HealthComponent
8
9var owner_node: Node
10
11func _ready() -> void:
12 owner_node = get_parent()
13
14func receive_hit(hitbox: HitboxComponent) -> void:
15 hurt.emit(hitbox)
16
17 if health_component:
18 health_component.take_damage(hitbox.damage, hitbox.owner_node)
Pattern 6: Scene Management
1# scene_manager.gd (Autoload)
2extends Node
3
4signal scene_loading_started(scene_path: String)
5signal scene_loading_progress(progress: float)
6signal scene_loaded(scene: Node)
7signal transition_started
8signal transition_finished
9
10@export var transition_scene: PackedScene
11@export var loading_scene: PackedScene
12
13var _current_scene: Node
14var _transition: CanvasLayer
15var _loader: ResourceLoader
16
17func _ready() -> void:
18 _current_scene = get_tree().current_scene
19
20 if transition_scene:
21 _transition = transition_scene.instantiate()
22 add_child(_transition)
23 _transition.visible = false
24
25func change_scene(scene_path: String, with_transition: bool = true) -> void:
26 if with_transition:
27 await _play_transition_out()
28
29 _load_scene(scene_path)
30
31func change_scene_packed(scene: PackedScene, with_transition: bool = true) -> void:
32 if with_transition:
33 await _play_transition_out()
34
35 _swap_scene(scene.instantiate())
36
37func _load_scene(path: String) -> void:
38 scene_loading_started.emit(path)
39
40 # Check if already loaded
41 if ResourceLoader.has_cached(path):
42 var scene := load(path) as PackedScene
43 _swap_scene(scene.instantiate())
44 return
45
46 # Async loading
47 ResourceLoader.load_threaded_request(path)
48
49 while true:
50 var progress := []
51 var status := ResourceLoader.load_threaded_get_status(path, progress)
52
53 match status:
54 ResourceLoader.THREAD_LOAD_IN_PROGRESS:
55 scene_loading_progress.emit(progress[0])
56 await get_tree().process_frame
57 ResourceLoader.THREAD_LOAD_LOADED:
58 var scene := ResourceLoader.load_threaded_get(path) as PackedScene
59 _swap_scene(scene.instantiate())
60 return
61 _:
62 push_error("Failed to load scene: %s" % path)
63 return
64
65func _swap_scene(new_scene: Node) -> void:
66 if _current_scene:
67 _current_scene.queue_free()
68
69 _current_scene = new_scene
70 get_tree().root.add_child(_current_scene)
71 get_tree().current_scene = _current_scene
72
73 scene_loaded.emit(_current_scene)
74 await _play_transition_in()
75
76func _play_transition_out() -> void:
77 if not _transition:
78 return
79
80 transition_started.emit()
81 _transition.visible = true
82
83 if _transition.has_method("transition_out"):
84 await _transition.transition_out()
85 else:
86 await get_tree().create_timer(0.3).timeout
87
88func _play_transition_in() -> void:
89 if not _transition:
90 transition_finished.emit()
91 return
92
93 if _transition.has_method("transition_in"):
94 await _transition.transition_in()
95 else:
96 await get_tree().create_timer(0.3).timeout
97
98 _transition.visible = false
99 transition_finished.emit()
Pattern 7: Save System
1# save_manager.gd (Autoload)
2extends Node
3
4const SAVE_PATH := "user://savegame.save"
5const ENCRYPTION_KEY := "your_secret_key_here"
6
7signal save_completed
8signal load_completed
9signal save_error(message: String)
10
11func save_game(data: Dictionary) -> void:
12 var file := FileAccess.open_encrypted_with_pass(
13 SAVE_PATH,
14 FileAccess.WRITE,
15 ENCRYPTION_KEY
16 )
17
18 if file == null:
19 save_error.emit("Could not open save file")
20 return
21
22 var json := JSON.stringify(data)
23 file.store_string(json)
24 file.close()
25
26 save_completed.emit()
27
28func load_game() -> Dictionary:
29 if not FileAccess.file_exists(SAVE_PATH):
30 return {}
31
32 var file := FileAccess.open_encrypted_with_pass(
33 SAVE_PATH,
34 FileAccess.READ,
35 ENCRYPTION_KEY
36 )
37
38 if file == null:
39 save_error.emit("Could not open save file")
40 return {}
41
42 var json := file.get_as_text()
43 file.close()
44
45 var parsed := JSON.parse_string(json)
46 if parsed == null:
47 save_error.emit("Could not parse save data")
48 return {}
49
50 load_completed.emit()
51 return parsed
52
53func delete_save() -> void:
54 if FileAccess.file_exists(SAVE_PATH):
55 DirAccess.remove_absolute(SAVE_PATH)
56
57func has_save() -> bool:
58 return FileAccess.file_exists(SAVE_PATH)
1# saveable.gd (Attach to saveable nodes)
2class_name Saveable
3extends Node
4
5@export var save_id: String
6
7func _ready() -> void:
8 if save_id.is_empty():
9 save_id = str(get_path())
10
11func get_save_data() -> Dictionary:
12 var parent := get_parent()
13 var data := {"id": save_id}
14
15 if parent is Node2D:
16 data["position"] = {"x": parent.position.x, "y": parent.position.y}
17
18 if parent.has_method("get_custom_save_data"):
19 data.merge(parent.get_custom_save_data())
20
21 return data
22
23func load_save_data(data: Dictionary) -> void:
24 var parent := get_parent()
25
26 if data.has("position") and parent is Node2D:
27 parent.position = Vector2(data.position.x, data.position.y)
28
29 if parent.has_method("load_custom_save_data"):
30 parent.load_custom_save_data(data)
Performance Tips
1# 1. Cache node references
2@onready var sprite := $Sprite2D # Good
3# $Sprite2D in _process() # Bad - repeated lookup
4
5# 2. Use object pooling for frequent spawning
6# See Pattern 4
7
8# 3. Avoid allocations in hot paths
9var _reusable_array: Array = []
10
11func _process(_delta: float) -> void:
12 _reusable_array.clear() # Reuse instead of creating new
13
14# 4. Use static typing
15func calculate(value: float) -> float: # Good
16 return value * 2.0
17
18# 5. Disable processing when not needed
19func _on_off_screen() -> void:
20 set_process(false)
21 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
What Users Are Saying
Real feedback from the community
Environment Matrix
Dependencies
Framework Support
Context Window
Security & Privacy
Information
- Author
- wshobson
- Updated
- 2026-01-30
- Category
- cms-platforms
Related Skills
Godot Gdscript Patterns
Master Godot 4 GDScript patterns including signals, scenes, state machines, and optimization. Use …
View Details →Agent Review
Get external agent review and feedback. Routes Anthropic models through Claude Agent SDK (uses local …
View Details →Agent Review
Get external agent review and feedback. Routes Anthropic models through Claude Agent SDK (uses local …
View Details →