Reference Quest

GDScript Recipes

Copy-ready snippets for common mechanics, with enough context to adapt them safely.

10-20 min Practice first Godot 4.6
Movement

2D Player Movement

Basic WASD/arrow key movement for CharacterBody2D

GDScript
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()
Movement

Top-Down 8-Direction Movement

Normalized 8-direction movement with acceleration and friction

GDScript
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()
Movement

Smooth Camera Follow

Camera2D that smoothly tracks a target node with offset and dead zone

GDScript
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)
Movement

Platformer Jump with Gravity

Variable-height jump with coyote time and jump buffering

GDScript
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()
UI

Health Bar with Tween

Animated health bar that smoothly transitions when health changes

GDScript
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)
UI

Damage Numbers Popup

Floating damage numbers that rise and fade out when enemies take hits

GDScript
extends Node2D

# Call this to spawn a damage number
func show_damage(amount: int, pos: Vector2, is_crit: bool = false) -> void:
    var label := Label.new()
    label.text = str(amount)
    label.position = pos
    label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER

    if is_crit:
        label.add_theme_color_override("font_color", Color.RED)
        label.add_theme_font_size_override("font_size", 28)
    else:
        label.add_theme_color_override("font_color", Color.WHITE)
        label.add_theme_font_size_override("font_size", 20)

    add_child(label)

    var tween := create_tween().set_parallel(true)
    tween.tween_property(label, "position:y", pos.y - 60, 0.8)
    tween.tween_property(label, "modulate:a", 0.0, 0.8).set_delay(0.3)
    tween.chain().tween_callback(label.queue_free)
UI

Screen Shake Effect

Camera shake effect triggered by events like explosions or damage

GDScript
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
Signals

Custom Signal with Data

Define and emit custom signals that carry typed payload data

GDScript
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)
Signals

Connecting Signals in Code

Connect signals programmatically with lambdas and bound arguments

GDScript
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!")
Signals

Signal Bus (Autoload)

Global event bus pattern using an autoload singleton for decoupled communication

GDScript
# 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)
Save/Load

JSON Save System

Save and load game data as JSON files with error handling

GDScript
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/Load

Resource-Based Save

Type-safe save system using custom Resource classes

GDScript
# 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()
Save/Load

Config File Settings

Store user preferences (volume, resolution, keybinds) with ConfigFile

GDScript
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),
    }
Physics

Raycasting from Code

Cast rays to detect walls, ground, or line-of-sight between objects

GDScript
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
Physics

Area2D Detection Zone

Detect when bodies enter/exit an area for pickups, triggers, or aggro zones

GDScript
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
Physics

Projectile Spawning

Spawn and fire projectiles with speed, direction, and lifetime

GDScript
# 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)
Audio

Play Sound Effects

Play one-shot SFX with pitch variation using AudioStreamPlayer

GDScript
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)
Audio

Background Music with Crossfade

Crossfade between background music tracks with volume tweening

GDScript
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
Animation

Sprite Animation State Machine

Simple state-based sprite animation using AnimatedSprite2D

GDScript
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")
Animation

Tween UI Effects Collection

Reusable tween effects for punch scale, fade, slide-in, and bounce

GDScript
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)
Animation

Sprite Flash on Hit

Flash a sprite white briefly when the character takes damage

GDScript
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)
AI

Simple Patrol AI

Enemy that walks between patrol points and waits at each one

GDScript
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
AI

Follow/Chase Player

Enemy that detects and chases the player within a detection radius

GDScript
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()
AI

State Machine NPC

Finite state machine for NPC behavior: idle, patrol, chase, attack

GDScript
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.
AI

Random Wander AI

NPC that picks random nearby positions and wanders between them

GDScript
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.