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 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
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 )
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
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
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 )
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
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
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
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
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 )
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
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
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 )
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 ) For advanced Godot patterns, performance tips, and best practices, see references/advanced-patterns.md : Pattern 6: Scene Management — Autoload SceneManager with async threaded loading ( ResourceLoader.load_threaded_request ), ResourceLoader.has_cached check, transition overlay support, and scene swapping with queue_free Pattern 7: Save System — Autoload SaveManager with AES-encrypted save files ( FileAccess.open_encrypted_with_pass ), JSON serialization, and a reusable Saveable component node for per-node save/load lifecycle Performance Tips — caching @onready references, avoiding allocations in _process , static typing benefits, disabling processing for off-screen nodes Best Practices — Do's and Don'ts covering signals, typing, resources, pooling, and Autoloads