Reference quest GDScript Recipes. Copy-ready snippets for common mechanics, with enough context to adapt them safely.
10-20 min · Godot 4.7
All 26 Movement 4 UI 4 Signals 3 Save/Load 3 Physics 3 Audio 2 Animation 3 AI 4
extends CharacterBody2D
@export var speed: float = 300.0
func _physics_process (delta: float) -> void :
var direction := Input .get_vector ("left" , "right" , "up" , "down" )
velocity = direction * speed
move_and_slide () extends CharacterBody2D
@export var max_speed: float = 200.0
@export var acceleration: float = 1200.0
@export var friction: float = 1000.0
func _physics_process (delta: float) -> void :
var input_dir := Input .get_vector ("left" , "right" , "up" , "down" )
if input_dir != Vector2 .ZERO :
velocity = velocity.move_toward (input_dir * max_speed, acceleration * delta)
else :
velocity = velocity.move_toward (Vector2 .ZERO , friction * delta)
move_and_slide () extends Camera2D
@export var target: Node2D
@export var smoothing: float = 5.0
@export var offset: Vector2 = Vector2 (0 , -40 )
@export var look_ahead: float = 50.0
func _process (delta: float) -> void :
if not target:
return
var target_pos := target.global_position + offset
# Add look-ahead based on target velocity
if target is CharacterBody2D :
target_pos += target.velocity.normalized () * look_ahead
global_position = global_position.lerp (target_pos, smoothing * delta) extends CharacterBody2D
@export var speed: float = 300.0
@export var jump_force: float = -400.0
@export var coyote_time: float = 0.12
@export var jump_buffer_time: float = 0.1
var gravity: float = ProjectSettings .get_setting ("physics/2d/default_gravity" )
var coyote_timer := 0.0
var jump_buffer_timer := 0.0
func _physics_process (delta: float) -> void :
# Gravity
velocity.y += gravity * delta
# Coyote time
if is_on_floor ():
coyote_timer = coyote_time
else :
coyote_timer -= delta
# Jump buffer
if Input .is_action_just_pressed ("jump" ):
jump_buffer_timer = jump_buffer_time
else :
jump_buffer_timer -= delta
# Execute jump
if jump_buffer_timer > 0 and coyote_timer > 0 :
velocity.y = jump_force
coyote_timer = 0
jump_buffer_timer = 0
# Variable jump height (release early = lower jump)
if Input .is_action_just_released ("jump" ) and velocity.y < 0 :
velocity.y *= 0.5
# Horizontal
var dir := Input .get_axis ("left" , "right" )
velocity.x = dir * speed
move_and_slide () extends ProgressBar
@export var health_component: Node
func _ready () -> void :
if health_component:
health_component.health_changed.connect (_on_health_changed)
max_value = 100
value = 100
func _on_health_changed (new_health: float) -> void :
var tween := create_tween ()
tween.set_ease (Tween .EASE_OUT )
tween.set_trans (Tween .TRANS_CUBIC )
tween.tween_property (self , "value" , new_health, 0.3 ) extends Camera2D
var shake_intensity := 0.0
var shake_decay: float = 5.0
func shake (intensity: float = 10.0 , duration: float = 0.3 ) -> void :
shake_intensity = intensity
var tween := create_tween ()
tween.tween_property (self , "shake_intensity" , 0.0 , duration)
func _process (delta: float) -> void :
if shake_intensity > 0 :
offset = Vector2 (
randf_range (-shake_intensity, shake_intensity),
randf_range (-shake_intensity, shake_intensity)
)
else :
offset = Vector2 .ZERO extends CharacterBody2D
signal health_changed (old_value: int, new_value: int)
signal died
signal item_collected (item_name: String , quantity: int)
@export var max_health: int = 100
var health: int = max_health
func take_damage (amount: int) -> void :
var old_health := health
health = max (health - amount, 0 )
health_changed.emit (old_health, health)
if health <= 0 :
died.emit ()
func collect_item (item_name: String , qty: int = 1 ) -> void :
item_collected.emit (item_name, qty) extends Node
func _ready ():
# Basic connection
$Player .health_changed.connect (_on_health_changed)
$Player .died.connect (_on_player_died)
# Lambda connection
$Button .pressed.connect (func ():
print ("Button pressed!" )
)
# One-shot connection (disconnects after first call)
$Timer .timeout.connect (_on_first_timeout, CONNECT_ONE_SHOT )
# Connect with bind (extra arguments)
for enemy in get_tree ().get_nodes_in_group ("enemies" ):
enemy.died.connect (_on_enemy_died.bind (enemy.name))
func _on_health_changed (old_val, new_val):
$HealthBar .value = new_val
func _on_player_died ():
get_tree ().change_scene_to_file ("res://game_over.tscn" )
func _on_first_timeout ():
print ("This only runs once!" )
func _on_enemy_died (enemy_name: String ):
print (enemy_name + " was defeated!" ) # EventBus.gd — Add as Autoload in Project Settings
extends Node
# Game state signals
signal game_started
signal game_paused (is_paused: bool)
signal game_over (score: int)
# Player signals
signal player_damaged (amount: int)
signal player_healed (amount: int)
signal score_changed (new_score: int)
# UI signals
signal show_dialog (text: String )
signal notification (message: String , type: String )
# ──────────────────────────────────────────
# Usage from any script:
#
# Emit:
# EventBus.player_damaged.emit(25)
#
# Listen:
# EventBus.player_damaged.connect(_on_player_damaged) extends Node
const SAVE_PATH = "user://savegame.json"
func save_game (data: Dictionary ) -> bool:
var file = FileAccess .open (SAVE_PATH , FileAccess .WRITE )
if not file:
push_error ("Cannot open save file: " + str (FileAccess .get_open_error ()))
return false
file.store_string (JSON .stringify (data, " " ))
file.close ()
return true
func load_game () -> Dictionary :
if not FileAccess .file_exists (SAVE_PATH ):
return {}
var file = FileAccess .open (SAVE_PATH , FileAccess .READ )
var json = JSON .new ()
var error = json.parse (file.get_as_text ())
file.close ()
if error != OK :
push_error ("JSON parse error: " + json.get_error_message ())
return {}
return json.data # save_data.gd
class_name SaveData
extends Resource
@export var player_name := "Hero"
@export var level := 1
@export var health := 100
@export var position := Vector2 .ZERO
@export var inventory: Array [String ] = []
@export var play_time := 0.0
# ─── save_manager.gd (Autoload) ────────────
extends Node
const SAVE_PATH = "user://save.tres"
func save (data: SaveData ) -> void :
ResourceSaver .save (data, SAVE_PATH )
func load_save () -> SaveData :
if ResourceLoader .exists (SAVE_PATH ):
return ResourceLoader .load (SAVE_PATH ) as SaveData
return SaveData .new () extends Node
const CONFIG_PATH = "user://settings.cfg"
func save_settings (settings: Dictionary ):
var config = ConfigFile .new ()
config.set_value ("audio" , "master_volume" , settings.get ("master_volume" , 1.0 ))
config.set_value ("audio" , "sfx_volume" , settings.get ("sfx_volume" , 1.0 ))
config.set_value ("audio" , "music_volume" , settings.get ("music_volume" , 0.8 ))
config.set_value ("video" , "fullscreen" , settings.get ("fullscreen" , false ))
config.set_value ("video" , "vsync" , settings.get ("vsync" , true ))
config.save (CONFIG_PATH )
func load_settings () -> Dictionary :
var config = ConfigFile .new ()
if config.load (CONFIG_PATH ) != OK :
return {} # Return defaults
return {
"master_volume" : config.get_value ("audio" , "master_volume" , 1.0 ),
"sfx_volume" : config.get_value ("audio" , "sfx_volume" , 1.0 ),
"music_volume" : config.get_value ("audio" , "music_volume" , 0.8 ),
"fullscreen" : config.get_value ("video" , "fullscreen" , false ),
"vsync" : config.get_value ("video" , "vsync" , true ),
} extends CharacterBody2D
func check_line_of_sight (target: Node2D ) -> bool:
var space = get_world_2d ().direct_space_state
var query = PhysicsRayQueryParameters2D .create (
global_position,
target.global_position,
1 # Collision mask
)
query.exclude = [self ]
var result = space.intersect_ray (query)
if result.is_empty ():
return true # Nothing blocking
# Check if the hit object is the target
return result.collider == target
func get_ground_normal () -> Vector2 :
var space = get_world_2d ().direct_space_state
var query = PhysicsRayQueryParameters2D .create (
global_position,
global_position + Vector2 .DOWN * 100
)
var result = space.intersect_ray (query)
if not result.is_empty ():
return result.normal
return Vector2 .UP extends Area2D
signal body_detected (body: Node2D )
signal body_lost (body: Node2D )
var bodies_in_range: Array [Node2D ] = []
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" ):
bodies_in_range.append (body)
body_detected.emit (body)
func _on_body_exited (body: Node2D ):
if body in bodies_in_range:
bodies_in_range.erase (body)
body_lost.emit (body)
func get_nearest () -> Node2D :
var nearest: Node2D = null
var min_dist := INF
for body in bodies_in_range:
var dist = global_position.distance_to (body.global_position)
if dist < min_dist:
min_dist = dist
nearest = body
return nearest # bullet.gd
extends Area2D
var speed := 600.0
var direction := Vector2 .RIGHT
var damage := 10
func _ready ():
# Auto-destroy after 3 seconds
var timer = get_tree ().create_timer (3.0 )
timer.timeout.connect (queue_free)
func _physics_process (delta):
position += direction * speed * delta
func _on_body_entered (body):
if body.has_method ("take_damage" ):
body.take_damage (damage)
queue_free ()
# ─── In the player/weapon script: ────────
# var BulletScene = preload("res://bullet.tscn")
#
# func shoot():
# var bullet = BulletScene.instantiate()
# bullet.global_position = $Muzzle.global_position
# bullet.direction = (get_global_mouse_position() - global_position).normalized()
# get_parent().add_child(bullet) extends Node
@export var sfx_library: Dictionary = {}
# Preload your sounds
var sounds = {
"jump" : preload ("res://audio/sfx/jump.wav" ),
"hit" : preload ("res://audio/sfx/hit.wav" ),
"coin" : preload ("res://audio/sfx/coin.wav" ),
}
func play_sfx (sound_name: String , pitch_variation := 0.1 ):
if not sounds.has (sound_name):
return
var player = AudioStreamPlayer .new ()
player.stream = sounds[sound_name]
player.pitch_scale = randf_range (1.0 - pitch_variation, 1.0 + pitch_variation)
player.bus = "SFX"
add_child (player)
player.play ()
# Clean up when done
player.finished.connect (player.queue_free) extends Node
var current_player: AudioStreamPlayer = null
func play_music (stream: AudioStream , fade_duration := 1.0 ):
var new_player = AudioStreamPlayer .new ()
new_player.stream = stream
new_player.bus = "Music"
new_player.volume_db = -80.0
add_child (new_player)
new_player.play ()
var tween = create_tween ().set_parallel (true )
# Fade in new track
tween.tween_property (new_player, "volume_db" , 0.0 , fade_duration)
# Fade out old track
if current_player:
var old = current_player
tween.tween_property (old, "volume_db" , -80.0 , fade_duration)
tween.chain ().tween_callback (old.queue_free)
current_player = new_player
func stop_music (fade_duration := 1.0 ):
if current_player:
var tween = create_tween ()
var player = current_player
tween.tween_property (player, "volume_db" , -80.0 , fade_duration)
tween.tween_callback (player.queue_free)
current_player = null extends CharacterBody2D
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
enum State { IDLE , RUN , JUMP , FALL }
var current_state := State .IDLE
func _physics_process (delta: float) -> void :
var new_state := determine_state ()
if new_state != current_state:
current_state = new_state
play_animation ()
func determine_state () -> State :
if not is_on_floor ():
return State .JUMP if velocity.y < 0 else State .FALL
if abs (velocity.x) > 10 :
return State .RUN
return State .IDLE
func play_animation () -> void :
match current_state:
State .IDLE :
sprite.play ("idle" )
State .RUN :
sprite.play ("run" )
sprite.flip_h = velocity.x < 0
State .JUMP :
sprite.play ("jump" )
State .FALL :
sprite.play ("fall" ) extends Node
# Punch scale (e.g., coin pickup, button press)
static func punch_scale (node: Node , strength := 1.3 , duration := 0.3 ):
var tween = node.create_tween ()
tween.tween_property (node, "scale" , Vector2 .ONE * strength, duration * 0.4 )
tween.tween_property (node, "scale" , Vector2 .ONE , duration * 0.6 ).set_ease (Tween .EASE_OUT ).set_trans (Tween .TRANS_ELASTIC )
# Fade in
static func fade_in (node: CanvasItem , duration := 0.5 ):
node.modulate.a = 0.0
var tween = node.create_tween ()
tween.tween_property (node, "modulate:a" , 1.0 , duration)
# Slide in from direction
static func slide_in (node: Control , from_offset := Vector2 (0 , 30 ), duration := 0.4 ):
var target_pos = node.position
node.position += from_offset
node.modulate.a = 0.0
var tween = node.create_tween ().set_parallel (true )
tween.tween_property (node, "position" , target_pos, duration).set_ease (Tween .EASE_OUT ).set_trans (Tween .TRANS_CUBIC )
tween.tween_property (node, "modulate:a" , 1.0 , duration * 0.6 )
# Bounce (e.g., notification)
static func bounce (node: Node , height := 20.0 , duration := 0.5 ):
var tween = node.create_tween ()
tween.tween_property (node, "position:y" , node.position.y - height, duration * 0.4 ).set_ease (Tween .EASE_OUT )
tween.tween_property (node, "position:y" , node.position.y, duration * 0.6 ).set_ease (Tween .EASE_IN ).set_trans (Tween .TRANS_BOUNCE ) extends CharacterBody2D
@onready var sprite: Sprite2D = $Sprite2D
# Requires a ShaderMaterial on the Sprite2D with a "flash_amount" uniform
# Or use modulate for a simpler approach:
func flash_damage (duration: float = 0.15 , flashes: int = 3 ) -> void :
for i in flashes:
sprite.modulate = Color .RED
await get_tree ().create_timer (duration / (flashes * 2 )).timeout
sprite.modulate = Color .WHITE
await get_tree ().create_timer (duration / (flashes * 2 )).timeout
# Shader approach (more polished):
# func flash_white(duration := 0.1):
# sprite.material.set_shader_parameter("flash_amount", 1.0)
# await get_tree().create_timer(duration).timeout
# sprite.material.set_shader_parameter("flash_amount", 0.0) extends CharacterBody2D
@export var patrol_points: Array [Marker2D ] = []
@export var speed := 100.0
@export var wait_time := 1.5
var current_point := 0
var waiting := false
func _physics_process (delta):
if patrol_points.is_empty () or waiting:
return
var target = patrol_points[current_point].global_position
var direction = (target - global_position).normalized ()
velocity = direction * speed
move_and_slide ()
# Reached the point?
if global_position.distance_to (target) < 5.0 :
waiting = true
velocity = Vector2 .ZERO
await get_tree ().create_timer (wait_time).timeout
current_point = (current_point + 1 ) % patrol_points.size ()
waiting = false extends CharacterBody2D
@export var speed := 120.0
@export var detection_radius := 200.0
@export var stop_distance := 30.0
var player: Node2D = null
func _ready ():
# Find player (or use a group)
player = get_tree ().get_first_node_in_group ("player" )
func _physics_process (delta):
if not player:
return
var distance = global_position.distance_to (player.global_position)
if distance <= detection_radius and distance > stop_distance:
var direction = (player.global_position - global_position).normalized ()
velocity = direction * speed
# Face movement direction
if direction.x != 0 :
$Sprite2D .flip_h = direction.x < 0
else :
velocity = velocity.move_toward (Vector2 .ZERO , speed * delta * 5 )
move_and_slide () extends CharacterBody2D
enum State { IDLE , PATROL , CHASE , ATTACK }
var current_state := State .IDLE
var player: Node2D = null
@export var patrol_speed := 80.0
@export var chase_speed := 150.0
@export var detect_range := 250.0
@export var attack_range := 40.0
func _physics_process (delta):
player = get_tree ().get_first_node_in_group ("player" )
var dist = global_position.distance_to (player.global_position) if player else INF
match current_state:
State .IDLE :
velocity = Vector2 .ZERO
if dist < detect_range:
change_state (State .CHASE )
State .PATROL :
# Add patrol logic here
if dist < detect_range:
change_state (State .CHASE )
State .CHASE :
if dist > detect_range * 1.5 :
change_state (State .IDLE )
elif dist < attack_range:
change_state (State .ATTACK )
else :
var dir = (player.global_position - global_position).normalized ()
velocity = dir * chase_speed
State .ATTACK :
velocity = Vector2 .ZERO
if dist > attack_range * 1.5 :
change_state (State .CHASE )
move_and_slide ()
func change_state (new_state: State ):
current_state = new_state
# Trigger animations, sounds, etc. extends CharacterBody2D
@export var wander_radius := 150.0
@export var speed := 60.0
@export var idle_time_min := 1.0
@export var idle_time_max := 3.0
var home_position := Vector2 .ZERO
var target_position := Vector2 .ZERO
var wandering := false
func _ready ():
home_position = global_position
pick_new_target ()
func _physics_process (delta):
if not wandering:
return
var direction = (target_position - global_position).normalized ()
velocity = direction * speed
move_and_slide ()
if global_position.distance_to (target_position) < 8.0 :
wandering = false
velocity = Vector2 .ZERO
var wait = randf_range (idle_time_min, idle_time_max)
await get_tree ().create_timer (wait).timeout
pick_new_target ()
func pick_new_target ():
var angle = randf () * TAU
var distance = randf_range (40 , wander_radius)
target_position = home_position + Vector2 (cos (angle), sin (angle)) * distance
wandering = true Tutor Checkpoint Lock the pattern in Before jumping to the next page, turn the idea into one tiny scene or script. That is where the Godot habit sticks.
Unity habit Treat snippets like components you still need to own and tune.
Unreal habit Use recipes to replace common graph fragments with readable code.
Godot habit Paste small, test immediately, then extract reusable scripts only when repetition appears.
Try this Pick one recipe and write the scene tree it expects before copying code.