<< All versions
Skill v1.0.0
currentAutomated scan100/100925236118/alphaagent/godot-gdscript-patterns
──Details
PublishedOctober 31, 2026 at 07:45 PM
Content Hashsha256:0046830d8a80a769...
Git SHAac185ab97f58
──Files
Files (1 file, 19.4 KB)
SKILL.md19.4 KBactive
SKILL.md · 808 lines · 19.4 KB
name: godot-gdscript-patterns description: 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. metadata: version: "1.0.0"
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 Playerextends CharacterBody2D# Signalssignal 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: intvar _can_attack: bool = truefunc _ready() -> void:_health = max_healthfunc _physics_process(delta: float) -> void:var direction := Input.get_vector("left", "right", "up", "down")velocity = direction * speedmove_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.gdclass_name StateMachineextends Nodesignal state_changed(from_state: StringName, to_state: StringName)@export var initial_state: Statevar current_state: Statevar states: Dictionary = {}func _ready() -> void:# Register all State childrenfor child in get_children():if child is State:states[child.name] = childchild.state_machine = selfchild.process_mode = Node.PROCESS_MODE_DISABLED# Start initial stateif initial_state:current_state = initial_statecurrent_state.process_mode = Node.PROCESS_MODE_INHERITcurrent_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)returnvar previous_state := current_stateprevious_state.exit()previous_state.process_mode = Node.PROCESS_MODE_DISABLEDcurrent_state = states[state_name]current_state.process_mode = Node.PROCESS_MODE_INHERITcurrent_state.enter(msg)state_changed.emit(previous_state.name, current_state.name)
gdscript
# state.gdclass_name Stateextends Nodevar state_machine: StateMachinefunc enter(_msg: Dictionary = {}) -> void:passfunc exit() -> void:passfunc update(_delta: float) -> void:passfunc physics_update(_delta: float) -> void:passfunc handle_input(_event: InputEvent) -> void:pass
gdscript
# player_idle.gdclass_name PlayerIdleextends State@export var player: Playerfunc 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 Nodesignal game_startedsignal 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.MENUvar score: int = 0:set(value):score = valuescore_changed.emit(score)var high_score: int = 0func _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 = 0state = GameState.PLAYINGgame_started.emit()func toggle_pause() -> void:var is_paused := state != GameState.PAUSEDif is_paused:state = GameState.PAUSEDget_tree().paused = trueelse:state = GameState.PLAYINGget_tree().paused = falsegame_paused.emit(is_paused)func end_game(won: bool) -> void:state = GameState.GAME_OVERif score > high_score:high_score = score_save_high_score()game_over.emit(won)func add_score(points: int) -> void:score += pointsfunc _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 eventssignal player_spawned(player: Node2D)signal player_died(player: Node2D)signal player_health_changed(health: int, max_health: int)# Enemy eventssignal enemy_spawned(enemy: Node2D)signal enemy_died(enemy: Node2D, position: Vector2)# Item eventssignal item_collected(item_type: StringName, value: int)signal powerup_activated(powerup_type: StringName)# Level eventssignal 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.gdclass_name WeaponDataextends 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.gdclass_name CharacterStatsextends Resourcesignal 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: floatfunc _init() -> void:_current_health = max_healthfunc get_current_health() -> float:return _current_healthfunc 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_damagefunc 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 CharacterStatscopy._current_health = copy.max_healthreturn copy
gdscript
# Using resourcesclass_name Characterextends CharacterBody2D@export var base_stats: CharacterStats@export var weapon: WeaponDatavar stats: CharacterStatsfunc _ready() -> void:# Create runtime copy to avoid modifying the resourcestats = 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.gdclass_name ObjectPoolextends Node@export var pooled_scene: PackedScene@export var initial_size: int = 10@export var can_grow: bool = truevar _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_DISABLEDinstance.visible = falseadd_child(instance)_available.append(instance)# Connect return signal if existsif instance.has_signal("returned_to_pool"):instance.returned_to_pool.connect(_return_to_pool.bind(instance))return instancefunc get_instance() -> Node:var instance: Nodeif _available.is_empty():if can_grow:instance = _create_instance()_available.erase(instance)else:push_warning("Pool exhausted and cannot grow")return nullelse:instance = _available.pop_back()instance.process_mode = Node.PROCESS_MODE_INHERITinstance.visible = true_in_use.append(instance)if instance.has_method("on_spawn"):instance.on_spawn()return instancefunc _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_DISABLEDinstance.visible = false_available.append(instance)func return_all() -> void:for instance in _in_use.duplicate():_return_to_pool(instance)
gdscript
# pooled_bullet.gdclass_name PooledBulletextends Area2Dsignal returned_to_pool@export var speed: float = 500.0@export var lifetime: float = 5.0var direction: Vector2var _timer: floatfunc on_spawn() -> void:_timer = lifetimefunc on_despawn() -> void:direction = Vector2.ZEROfunc initialize(pos: Vector2, dir: Vector2) -> void:global_position = posdirection = dir.normalized()rotation = direction.angle()func _physics_process(delta: float) -> void:position += direction * speed * delta_timer -= deltaif _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.gdclass_name HealthComponentextends Nodesignal 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.0var current_health: int:set(value):var old := current_healthcurrent_health = clampi(value, 0, max_health)if current_health != old:health_changed.emit(current_health, max_health)var _invincible: bool = falsefunc _ready() -> void:current_health = max_healthfunc take_damage(amount: int, source: Node = null) -> int:if _invincible or current_health <= 0:return 0var actual := mini(amount, current_health)current_health -= actualdamaged.emit(actual, source)if current_health <= 0:died.emit()elif invincibility_time > 0:_start_invincibility()return actualfunc heal(amount: int) -> int:var actual := mini(amount, max_health - current_health)current_health += actualif actual > 0:healed.emit(actual)return actualfunc _start_invincibility() -> void:_invincible = trueawait get_tree().create_timer(invincibility_time).timeout_invincible = false
gdscript
# hitbox_component.gdclass_name HitboxComponentextends Area2Dsignal hit(hurtbox: HurtboxComponent)@export var damage: int = 10@export var knockback_force: float = 200.0var owner_node: Nodefunc _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 HurtboxComponentif hurtbox.owner_node != owner_node:hit.emit(hurtbox)hurtbox.receive_hit(self)
gdscript
# hurtbox_component.gdclass_name HurtboxComponentextends Area2Dsignal hurt(hitbox: HitboxComponent)@export var health_component: HealthComponentvar owner_node: Nodefunc _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 Nodesignal scene_loading_started(scene_path: String)signal scene_loading_progress(progress: float)signal scene_loaded(scene: Node)signal transition_startedsignal transition_finished@export var transition_scene: PackedScene@export var loading_scene: PackedScenevar _current_scene: Nodevar _transition: CanvasLayervar _loader: ResourceLoaderfunc _ready() -> void:_current_scene = get_tree().current_sceneif transition_scene:_transition = transition_scene.instantiate()add_child(_transition)_transition.visible = falsefunc 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 loadedif ResourceLoader.has_cached(path):var scene := load(path) as PackedScene_swap_scene(scene.instantiate())return# Async loadingResourceLoader.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_frameResourceLoader.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)returnfunc _swap_scene(new_scene: Node) -> void:if _current_scene:_current_scene.queue_free()_current_scene = new_sceneget_tree().root.add_child(_current_scene)get_tree().current_scene = _current_scenescene_loaded.emit(_current_scene)await _play_transition_in()func _play_transition_out() -> void:if not _transition:returntransition_started.emit()_transition.visible = trueif _transition.has_method("transition_out"):await _transition.transition_out()else:await get_tree().create_timer(0.3).timeoutfunc _play_transition_in() -> void:if not _transition:transition_finished.emit()returnif _transition.has_method("transition_in"):await _transition.transition_in()else:await get_tree().create_timer(0.3).timeout_transition.visible = falsetransition_finished.emit()
Pattern 7: Save System
gdscript
# save_manager.gd (Autoload)extends Nodeconst SAVE_PATH := "user://savegame.save"const ENCRYPTION_KEY := "your_secret_key_here"signal save_completedsignal load_completedsignal 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")returnvar 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 parsedfunc 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 Saveableextends Node@export var save_id: Stringfunc _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 datafunc 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 pathsvar _reusable_array: Array = []func _process(_delta: float) -> void:_reusable_array.clear() # Reuse instead of creating new# 4. Use static typingfunc calculate(value: float) -> float: # Goodreturn value * 2.0# 5. Disable processing when not neededfunc _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