All skills
jwynia avatar

/godot-best-practices

@99a8797
by J Wyniajwynia/agent-skills160 stars
20

Guide AI agents through Godot 4.x GDScript coding best practices including scene organization, signals, resources, state machines, and performance optimization. This skill should be used when generating GDScript code, creating Godot scenes, designing game architecture, implementing state machines, object pooling, save/load systems, or when the user asks about Godot patterns, node structure, or GDScript standards. Keywords: godot, gdscript, game development, signals, resources, scenes, nodes, state machine, object pooling, save system, autoload, export, type hints.

Use this Skill: https://skilld.dev/gh/jwynia/agent-skills/godot-best-practices

This session only. Nothing lands on disk.

referencesarchitecturenode-communication.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Node Communication Patterns

Guide to communication between nodes in Godot 4.x.

The Golden Rule

Signal up, call down.

  • Parents know about children (can call their methods)
  • Children don't know about parents (emit signals instead)
  • Siblings communicate through shared parent or groups

Pattern 1: Signals (Decoupled Communication)

Best for: Events, state changes, child-to-parent communication.

# button.gd - Emits signal (doesn't know who listens)
class_name InteractButton
extends Area2D

signal pressed

func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player"):
        pressed.emit()
# door.gd - Connects to signal
class_name Door
extends Node2D

@export var button: InteractButton

func _ready() -> void:
    if button:
        button.pressed.connect(_on_button_pressed)

func _on_button_pressed() -> void:
    open()

Typed Signals (Godot 4.x)

# Define signals with typed parameters
signal health_changed(new_health: int, max_health: int)
signal item_collected(item: Item, collector: Node2D)
signal damage_dealt(target: Node2D, amount: int, type: DamageType)

# Emit with correct types
health_changed.emit(50, 100)
item_collected.emit(sword_item, player)

Anonymous Signals (One-Time Use)

# Wait for animation to finish
await $AnimationPlayer.animation_finished

# Wait for timer
await get_tree().create_timer(1.0).timeout

# Wait for custom signal
await some_node.some_signal

Pattern 2: Direct Method Calls (Parent to Child)

Best for: Commands, immediate actions, when relationship is known.

# player.gd - Calls child methods directly
class_name Player
extends CharacterBody2D

@onready var weapon: Weapon = $Weapon
@onready var animator: AnimationPlayer = $AnimationPlayer
@onready var health: HealthComponent = $HealthComponent

func attack() -> void:
    weapon.fire()           # Direct call to child
    animator.play("attack") # Direct call to child

func take_damage(amount: int) -> void:
    health.damage(amount)   # Direct call to child

Safe Access with Optional Nodes

# Node might not exist in all scene variations
@onready var optional_shield: Shield = get_node_or_null("Shield")

func block() -> void:
    if optional_shield:
        optional_shield.activate()

Pattern 3: Groups (Cross-Scene Communication)

Best for: Finding nodes across scene boundaries, one-to-many communication.

# Add nodes to groups in _ready() or editor
func _ready() -> void:
    add_to_group("enemies")
    add_to_group("damageable")
# explosion.gd - Affects all nodes in group
func explode() -> void:
    var blast_pos := global_position

    for node in get_tree().get_nodes_in_group("damageable"):
        var distance := node.global_position.distance_to(blast_pos)
        if distance < blast_radius:
            var damage := calculate_falloff_damage(distance)
            node.take_damage(damage)

Scene-Specific Groups

Prefix groups with scene identifier for isolation:

# In level_01
add_to_group("level_01_enemies")

# Clean up when level ends
func _exit_tree() -> void:
    get_tree().call_group("level_01_enemies", "queue_free")

Pattern 4: Autoloads (Global State)

Best for: Services, managers, truly global state.

# event_bus.gd - Global event system (autoload)
extends Node

signal player_died
signal level_completed(level_name: String)
signal achievement_unlocked(achievement_id: String)

# Any node can emit
# EventBus.player_died.emit()

# Any node can connect
# EventBus.player_died.connect(_on_player_died)

When to Use Autoloads

Use Case Autoload? Why
Audio playback Yes Persists between scenes
Save/load Yes Global service
Player stats Maybe Consider level-owned
Score Maybe Consider game state resource
Level manager No Scene should manage itself
Enemy spawning No Level-specific logic

Pattern 5: Dependency Injection

Pass dependencies instead of hardcoding:

# weapon.gd - Receives owner, doesn't find it
class_name Weapon
extends Node2D

var _owner_stats: CharacterStats

func setup(stats: CharacterStats) -> void:
    _owner_stats = stats

func calculate_damage() -> int:
    return base_damage + _owner_stats.strength
# player.gd - Injects dependency
func _ready() -> void:
    $Weapon.setup(stats)

Pattern 6: Callable/Lambda

Pass behavior as parameter:

# timer_utils.gd
static func delayed_call(node: Node, delay: float, callback: Callable) -> void:
    await node.get_tree().create_timer(delay).timeout
    callback.call()

# Usage
TimerUtils.delayed_call(self, 2.0, func(): print("2 seconds later"))
TimerUtils.delayed_call(self, 1.0, queue_free)

Anti-Patterns to Avoid

Anti-Pattern 1: get_parent() Chains

# Bad: Fragile, breaks on refactor
var player = get_parent().get_parent().get_parent()

# Good: Use groups or signals
var player = get_tree().get_first_node_in_group("player")

Anti-Pattern 2: Global find_node()

# Bad: Searches entire tree, slow and fragile
var enemy = get_tree().root.find_child("Enemy", true, false)

# Good: Use groups or explicit references
var enemies = get_tree().get_nodes_in_group("enemies")

Anti-Pattern 3: Bidirectional References

# Bad: Circular reference, unclear ownership
# player.gd
var current_weapon: Weapon
# weapon.gd
var owner_player: Player

# Good: Parent references child, child signals parent
# player.gd
var weapon: Weapon
# weapon.gd
signal ammo_depleted  # Player connects to this

Anti-Pattern 4: String-Based Signals (Legacy)

# Bad: No autocomplete, typo-prone (Godot 3.x style)
emit_signal("health_changed", 50)
connect("health_changed", self, "_on_health_changed")

# Good: Typed signals (Godot 4.x)
health_changed.emit(50)
health_changed.connect(_on_health_changed)

Decision Guide

Scenario Pattern
Child notifies parent of state change Signal
Parent commands child to act Direct call
Siblings need to communicate Signal through parent or group
Any node to any node Group or autoload event bus
Waiting for something to happen await signal
Global service (audio, saves) Autoload
Need to find multiple nodes Groups
Node needs external configuration Dependency injection

Performance Considerations

  1. Signals are fast - Don't avoid them for performance
  2. Groups are cached - get_nodes_in_group() is efficient
  3. Avoid per-frame group queries - Cache references in _ready()
  4. Direct calls are fastest - Use when relationship is stable
# Cache group results if queried frequently
var _cached_enemies: Array[Node]

func _ready() -> void:
    _cached_enemies = get_tree().get_nodes_in_group("enemies")
    get_tree().node_added.connect(_on_node_added)
    get_tree().node_removed.connect(_on_node_removed)

func _on_node_added(node: Node) -> void:
    if node.is_in_group("enemies"):
        _cached_enemies.append(node)

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides a comprehensive guide and templates for Godot 4.x GDScript development. It contains only instructional content, code snippets, and architectural patterns. No security risks or malicious behaviors were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    13/13 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 99a8797. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago
compatibility
Requires Godot 4.x project. GDScript only (not C#).
Other metadata
metadata
{
  "author": "agent-skills",
  "version": "1.0",
  "godot_version": "4.x",
  "type": "utility",
  "mode": "assistive",
  "domain": "gamedev"
}

README badge

README badge for jwynia/agent-skills/godot-best-practices