npx skills add https://github.com/wshobson/agents --skill godot-gdscript-patternsHow Godot Gdscript Patterns fits into a Paperclip company.
Godot Gdscript Patterns drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
SKILL.md564 linesExpandCollapse
---name: godot-gdscript-patternsdescription: 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 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 ```gdscriptclass_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 = 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.gdclass_name StateMachineextends Node signal state_changed(from_state: StringName, to_state: StringName) @export var initial_state: State var current_state: Statevar 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.gdclass_name Stateextends 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.gdclass_name PlayerIdleextends 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_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 = 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 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 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 resourcesclass_name Characterextends 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.gdclass_name ObjectPoolextends 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.gdclass_name PooledBulletextends Area2D signal returned_to_pool @export var speed: float = 500.0@export var lifetime: float = 5.0 var direction: Vector2var _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.gdclass_name HealthComponentextends 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.gdclass_name HitboxComponentextends 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.gdclass_name HurtboxComponentextends 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](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 AutoloadsAccessibility Compliance
This walks you through implementing proper WCAG 2.2 compliance with real code patterns for screen readers, keyboard navigation, and mobile accessibility. It cov
Airflow Dag Patterns
If you're building data pipelines with Airflow, this skill gives you production-ready DAG patterns that actually work in the real world. It covers TaskFlow API
Angular Migration
Migrating from AngularJS to Angular is notoriously painful, and this skill tackles the practical stuff that makes or breaks these projects. It covers hybrid app