godot-gdscript
npx skills add https://github.com/erlendtregde/snufkin-game --skill godot-gdscript
Agent 安装分布
Skill 文档
Godot GDScript Development
Comprehensive guide for Godot 4.x game development using GDScript.
Core Principles
Node-Based Architecture
Everything in Godot is a Node. Scenes are trees of nodes. Understand the hierarchy:
Node– Base class, provides basic tree structureNode2D– Adds 2D transform (position, rotation, scale)Node3D– Adds 3D transform- Specialized nodes inherit from these
Scene Structure
- Scenes are reusable – A scene can be instanced multiple times
- Composition over inheritance – Build complex objects from simple scenes
- Each scene has a root node – The top-level node defines the scene type
Essential Syntax & Patterns
Variable Declaration
# Inferred type
var health = 100
# Explicit type
var health: int = 100
# Export to Inspector (Godot 4.x syntax)
@export var speed: float = 300.0
@export_range(0, 100) var health: int = 100
@export_category("Combat")
@export var damage: int = 10
# Constants
const MAX_SPEED = 500.0
const GRAVITY = 980.0
Property Initialization Sequence
Understanding the order matters for exports and setters:
@export var my_value: String = "initial":
set(value):
my_value = value + "!"
func _init():
# Runs AFTER initial value, triggers setter
my_value = "from_init" # Result: "from_init!"
# When node is in a scene file with Inspector value set to "inspector":
# Final result: "inspector!" (Inspector overrides _init)
Sequence:
- Initial value assignment (
= "initial") – setter NOT called _init()assignments – setter IS called- Inspector/export values – setter IS called (if node in scene)
Best Practice: For exports, use null or invalid defaults, let Inspector set real values.
Node References
# @onready - Runs after _ready(), node tree is guaranteed to exist
@onready var sprite = $AnimatedSprite2D
@onready var timer = $Timer
@onready var collision = $CollisionShape2D
# Manual reference
var sprite = null
func _ready():
sprite = get_node("AnimatedSprite2D")
# or
sprite = $AnimatedSprite2D
Lifecycle Methods
func _init():
# Constructor - called when object is created
# Runs BEFORE _ready(), before node is in tree
# Use for initial property setup
pass
func _ready():
# Called once when node enters scene tree
# All child nodes have also called _ready() (bottom-up)
# Initialize variables, connect signals, setup state
pass
func _enter_tree():
# Called when node enters tree (before _ready)
# Called again if removed and re-added
# Use for setup that needs parent access
pass
func _exit_tree():
# Called when node exits tree
# Use for cleanup
pass
func _process(delta: float):
# Called every frame (frame-dependent)
# Use for non-physics updates, animations, timers, UI
# Check inputs here if you need frame-perfect response
pass
func _physics_process(delta: float):
# Called at fixed intervals (default 60 FPS, frame-independent)
# Use for physics, movement, collision detection
# Prefer this over _process for movement
pass
func _input(event: InputEvent):
# Receives ALL input events
# Use for specific key/mouse detection
if event.is_action_pressed("jump"):
jump()
func _unhandled_input(event: InputEvent):
# Only receives events not handled by UI or other nodes
pass
func _notification(what: int):
# Universal callback for engine notifications
# Handles NOTIFICATION_* constants
match what:
NOTIFICATION_PARENTED:
print("Node got a parent")
NOTIFICATION_UNPARENTED:
print("Node lost parent")
Process vs Physics Process vs Input
Use _process(delta):
- Frame-dependent updates
- UI updates
- Non-physics animations
- Timers that don’t need precision
- For recurring checks without every-frame need, use Timer instead
Use _physics_process(delta):
- All physics and movement
- Consistent updates regardless of framerate
- Collision detection
- Kinematic operations
Use *_input(event):
- Reacts only when input occurs (more efficient)
- For input checks, prefer this over polling in _process
- Check delta time with
get_process_delta_time()if needed
Input Handling
# Check action state (defined in Project Settings > Input Map)
if Input.is_action_pressed("move_right"): # Held down
velocity.x += SPEED
if Input.is_action_just_pressed("jump"): # Pressed this frame
jump()
if Input.is_action_just_released("shoot"): # Released this frame
stop_shooting()
# Get axis input (-1 to 1)
var direction = Input.get_axis("move_left", "move_right")
velocity.x = direction * SPEED
# Get vector input
var input_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
Common Node Types & Usage
CharacterBody2D (for player/NPCs)
extends CharacterBody2D
const SPEED = 300.0
const GRAVITY = 980.0
const JUMP_FORCE = -400.0
func _physics_process(delta):
# Apply gravity
velocity.y += GRAVITY * delta
# Horizontal movement
var direction = Input.get_axis("ui_left", "ui_right")
velocity.x = direction * SPEED
# Jump
if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
velocity.y = JUMP_FORCE
# MUST call this to apply movement and handle collisions
move_and_slide()
Area2D (for triggers/detection)
extends Area2D
signal player_entered
signal player_exited
func _ready():
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D):
if body.is_in_group("Player"):
player_entered.emit()
print("Player entered area")
func _on_body_exited(body: Node2D):
if body.is_in_group("Player"):
player_exited.emit()
AnimatedSprite2D
@onready var sprite = $AnimatedSprite2D
func _ready():
sprite.play("idle")
func update_animation(direction: float):
if direction != 0:
sprite.play("run")
sprite.flip_h = direction < 0 # Flip when moving left
else:
sprite.play("idle")
Timer
@onready var timer = $Timer
func _ready():
timer.wait_time = 2.0
timer.one_shot = true # Only fires once
timer.timeout.connect(_on_timer_timeout)
timer.start()
func _on_timer_timeout():
print("Timer finished!")
Camera2D
@onready var camera = $Camera2D
func _ready():
camera.enabled = true
camera.make_current() # Set as active camera
# Smooth following
camera.position_smoothing_enabled = true
camera.position_smoothing_speed = 5.0
Signals
Defining & Emitting Signals
# Define custom signal
signal health_changed(new_health)
signal player_died
# Emit signal
health_changed.emit(50)
player_died.emit()
Connecting Signals
# Godot 4.x syntax - connect in code
func _ready():
$Button.pressed.connect(_on_button_pressed)
$Timer.timeout.connect(_on_timer_timeout)
func _on_button_pressed():
print("Button clicked!")
Groups & Node Queries
# Add node to group (in editor or code)
add_to_group("enemies")
add_to_group("Player")
# Check if in group
if body.is_in_group("Player"):
take_damage()
# Get all nodes in group
var enemies = get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
enemy.take_damage(10)
# Get first node in group
var player = get_tree().get_first_node_in_group("Player")
Scene Management
Changing Scenes
# Change scene by path
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
# Change scene by PackedScene
var next_scene = preload("res://scenes/level_2.tscn")
get_tree().change_scene_to_packed(next_scene)
# Manual scene change with transition
var new_scene = load("res://scenes/level_2.tscn")
get_tree().root.add_child(new_scene.instantiate())
queue_free() # Remove current scene
Instancing Scenes
# Preload (at compile time)
const BULLET = preload("res://scenes/bullet.tscn")
func shoot():
var bullet = BULLET.instantiate()
bullet.position = $Muzzle.global_position
get_tree().root.add_child(bullet)
# Load (at runtime)
var enemy_scene = load("res://scenes/enemy.tscn")
var enemy = enemy_scene.instantiate()
add_child(enemy)
Resources & Preloading
Preload vs Load
# PRELOAD - Compile-time loading (FASTER)
# - Loads when script loads
# - Editor can autocomplete paths
# - Use for constants and frequently used resources
const BulletScene = preload("res://scenes/bullet.tscn")
const PlayerTexture = preload("res://sprites/player.png")
# LOAD - Runtime loading (FLEXIBLE)
# - Loads when line executes
# - Can use dynamic paths
# - Use for conditional/optional resources
var scene_path = "res://scenes/level_" + str(level_num) + ".tscn"
var level = load(scene_path)
# For exports, avoid preloading - let Inspector override
@export var custom_scene: PackedScene # Don't preload here
When to Use Each
Use preload():
- Script/scene dependencies that won’t change
- Frequently spawned objects (bullets, particles)
- Required resources that must exist
Use load():
- Dynamic path construction
- Optional/conditional resources
- Resources that may be unloaded later
- Large resources that shouldn’t stay in memory
Tweens (Animations)
# Create tween
var tween = create_tween()
# Animate property
tween.tween_property($Sprite, "position", Vector2(100, 100), 1.0)
tween.tween_property($Sprite, "modulate:a", 0.0, 0.5) # Fade out
# Chaining
tween.tween_property($Sprite, "scale", Vector2(2, 2), 0.5)
tween.tween_property($Sprite, "scale", Vector2(1, 1), 0.5)
# Easing
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_CUBIC)
# Wait for completion
await tween.finished
print("Tween completed!")
Async Operations (await)
# Wait for signal
await $Timer.timeout
print("Timer finished")
# Wait one frame
await get_tree().process_frame
# Wait for animation
$AnimatedSprite.play("attack")
await $AnimatedSprite.animation_finished
print("Attack animation done")
# Useful in sequence
func do_sequence():
print("Starting")
await get_tree().create_timer(1.0).timeout
print("After 1 second")
await get_tree().create_timer(2.0).timeout
print("After 3 seconds total")
Autoload (Singletons)
Configure in Project Settings â Autoload. Use sparingly – prefer scene-based architecture.
# Global.gd (autoload singleton)
extends Node
var player_health = 100
var current_level = 1
func save_game():
# Save logic
pass
# Access from any script
Global.player_health -= 10
Global.save_game()
When to Use Autoloads
Good uses:
- Truly global systems (save/load, settings, audio manager)
- Systems managing their own data without interfering with scenes
- Shared utilities that don’t need scene context
Avoid autoloads for:
- Scene-specific functionality (use scene nodes instead)
- Things that could be passed via signals or references
- Manager classes that tightly couple your code
Alternative: Use static functions/variables in script classes:
# utils.gd
extends Node
class_name Utils
static var shared_data = {}
static func calculate_damage(base: int, modifier: float) -> int:
return int(base * modifier)
# Use anywhere without autoload
Utils.calculate_damage(10, 1.5)
Best Practices
Code Organization
- One script per node – Each scene’s root node gets its own script
- Use @onready for node references – Ensures nodes exist when accessed
- Group related functionality – Use separate scenes for reusable components
- Prefer composition – Combine simple scenes into complex ones
- snake_case for files/folders – Avoids case-sensitivity issues on export
- PascalCase for node names – Matches built-in node convention
Performance
- Avoid
get_node()in loops – Cache references in@onreadyor_ready() - Use
_physics_process()for physics – Don’t do physics in_process() - Queue free properly – Use
queue_free()instead of manual removal - Limit
queue_redraw()– Only redraw when state changes - Set properties before adding to tree – Property setters can be slow; batch changes before
add_child() - PackedScene faster than script instantiation – Prefer scenes over
MyScript.new()for game objects
Data Structure Choice
Array: Fast iteration, slow insert/remove (except at end)
Dictionary: Fast insert/remove/get by key, slow find by value
Object/Resource: Provides structure but slower than both due to property lookup chain
Rule: Use simplest structure that meets needs. Array for lists, Dictionary for lookups, Object for complex data with behavior.
Common Patterns
- State machines for complex behavior – Track current state, handle transitions
- Use signals for loose coupling – Don’t directly reference other nodes when possible
- Validate in
_ready()– Check that required nodes exist - Use
is_instance_valid()– Before accessing nodes that might be freed - Duck-typed access – Check
has_method()before calling on unknown types
Godot 4.x Changes
@exportinstead ofexport@onreadyinstead ofonready.connect()requires explicit method reference:signal.connect(method)move_and_slide()takes no parameters (velocity is property)- Signals emit with
.emit()instead ofemit_signal()
Common Gotchas
- Forgetting
move_and_slide()– CharacterBody2D won’t move without it - Not checking
is_on_floor()– Before allowing jumps - Using wrong process function – Physics in
_physics_process(), not_process() - Accessing nodes before
_ready()– Use@onreadyor access in/after_ready() - Forgetting
delta– Multiply movement/timers by delta for frame-rate independence - Not setting collision layers – Objects won’t collide without proper layer setup
Further Reading
For deep dives into specific topics:
- Node Types Reference – Detailed node type documentation
- Physics & Collision – Advanced physics patterns
- Scene Architecture – Scene composition strategies
- Best Practices – Official Godot best practices (dependency injection, autoload guidelines, data structures, performance patterns)