Godot Gdscript Patterns

Master production-ready Godot 4 GDScript patterns for scalable games

✨ The solution you've been looking for

Verified
Tested and verified by our team
25450 Stars

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.

godot gdscript game-development patterns state-machine optimization architecture signals
Repository

See It In Action

Interactive preview & real-world examples

Live Demo
Skill Demo Animation

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

1

Install

claude-code skill install godot-gdscript-patterns

claude-code skill install godot-gdscript-patterns
2

Config

3

First Trigger

@godot-gdscript-patterns help

Commands

CommandDescriptionRequired Args
@godot-gdscript-patterns implementing-player-state-managementCreate 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-systemsDesign autoload singletons for game management, event communication, and persistent data across scenes.None
@godot-gdscript-patterns optimizing-performance-with-object-poolingImplement 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

Godot 4.0+

Framework Support

Godot Engine 4.x ✓ (recommended)

Context Window

Token Usage ~3K-8K tokens for complete pattern implementations

Security & Privacy

Information

Author
wshobson
Updated
2026-01-30
Category
cms-platforms