Movement

2D Player Movement

Basic WASD/arrow key movement for CharacterBody2D

GDScript
extends CharacterBody2D

var speed = class="hl-number">300.0

func _physics_process(delta):
    var direction = Input.get_vector(class="hl-string">"left", class="hl-string">"right", class="hl-string">"up", class="hl-string">"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 := class="hl-number">200.0
@export var acceleration := class="hl-number">1200.0
@export var friction := class="hl-number">1000.0

func _physics_process(delta):
    var input_dir = Input.get_vector(class="hl-string">"left", class="hl-string">"right", class="hl-string">"up", class="hl-string">"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 := class="hl-number">5.0
@export var offset := Vector2(class="hl-number">0, -class="hl-number">40)
@export var look_ahead := class="hl-number">50.0

func _process(delta):
    if not target:
        return

    var target_pos = target.global_position + offset

    class="hl-comment"># 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 := class="hl-number">300.0
@export var jump_force := -class="hl-number">400.0
@export var coyote_time := class="hl-number">0.12
@export var jump_buffer_time := class="hl-number">0.1

var gravity = ProjectSettings.get_setting(class="hl-string">"physics/2d/default_gravity")
var coyote_timer := class="hl-number">0.0
var jump_buffer_timer := class="hl-number">0.0

func _physics_process(delta):
    class="hl-commentclass="hl-string">"># Gravity
    velocity.y += gravity * delta

    class="hl-commentclass="hl-string">"># Coyote time
    if is_on_floor():
        coyote_timer = coyote_time
    else:
        coyote_timer -= delta

    class="hl-commentclass="hl-string">"># Jump buffer
    if Input.is_action_just_pressed("jumpclass="hl-string">"):
        jump_buffer_timer = jump_buffer_time
    else:
        jump_buffer_timer -= delta

    class="hl-commentclass="hl-string">"># Execute jump
    if jump_buffer_timer > class="hl-number">0 and coyote_timer > class="hl-number">0:
        velocity.y = jump_force
        coyote_timer = class="hl-number">0
        jump_buffer_timer = class="hl-number">0

    class="hl-commentclass="hl-string">"># Variable jump height (release early = lower jump)
    if Input.is_action_just_released("jumpclass="hl-string">") and velocity.y < class="hl-number">0:
        velocity.y *= class="hl-number">0.5

    class="hl-commentclass="hl-string">"># Horizontal
    var dir = Input.get_axis("leftclass="hl-string">", "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():
    if health_component:
        health_component.health_changed.connect(_on_health_changed)
    max_value = class="hl-number">100
    value = class="hl-number">100

func _on_health_changed(new_health: float):
    var tween = create_tween()
    tween.set_ease(Tween.EASE_OUT)
    tween.set_trans(Tween.TRANS_CUBIC)
    tween.tween_property(self, class="hl-string">"value", new_health, class="hl-number">0.3)
UI

Damage Numbers Popup

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

GDScript
extends Node2D

class="hl-commentclass="hl-string">"># Call this to spawn a damage number
func show_damage(amount: int, pos: Vector2, is_crit := false):
    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_colorclass="hl-string">", Color.RED)
        label.add_theme_font_size_override("font_sizeclass="hl-string">", class="hl-number">28)
    else:
        label.add_theme_color_override("font_colorclass="hl-string">", Color.WHITE)
        label.add_theme_font_size_override("font_sizeclass="hl-string">", class="hl-number">20)

    add_child(label)

    var tween = create_tween().set_parallel(true)
    tween.tween_property(label, "position:yclass="hl-string">", pos.y - class="hl-number">60, class="hl-number">0.8)
    tween.tween_property(label, "modulate:a", class="hl-number">0.0, class="hl-number">0.8).set_delay(class="hl-number">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 := class="hl-number">0.0
var shake_decay := class="hl-number">5.0

func shake(intensity := class="hl-number">10.0, duration := class="hl-number">0.3):
    shake_intensity = intensity

    var tween = create_tween()
    tween.tween_property(self, class="hl-string">"shake_intensity", class="hl-number">0.0, duration)

func _process(delta):
    if shake_intensity > class="hl-number">0:
        offset = Vector2(
            randf_range(-shake_intensity, shake_intensity),
            randf_range(-shake_intensity, shake_intensity)
        )
    else:
        offset = Vector2.ZERO
UI

Menu Slide Transitions

Smooth panel transitions for navigating between menu screens

GDScript
extends Control

@onready var panels = [$MainMenu, $SettingsMenu, $CreditsMenu]
var current_panel: Control

func _ready():
    current_panel = $MainMenu
    for panel in panels:
        if panel != current_panel:
            panel.visible = false

func switch_to(panel: Control):
    if panel == current_panel:
        return

    var old = current_panel
    current_panel = panel
    panel.visible = true
    panel.modulate.a = class="hl-number">0.0
    panel.position.x = class="hl-number">40

    var tween = create_tween().set_parallel(true)
    class="hl-commentclass="hl-string">"># Fade out old
    tween.tween_property(old, "modulate:aclass="hl-string">", class="hl-number">0.0, class="hl-number">0.25)
    tween.tween_property(old, "position:xclass="hl-string">", -class="hl-number">40, class="hl-number">0.25)
    class="hl-commentclass="hl-string">"># Fade in new
    tween.tween_property(panel, "modulate:aclass="hl-string">", class="hl-number">1.0, class="hl-number">0.25)
    tween.tween_property(panel, "position:x", class="hl-number">0, class="hl-number">0.25)

    tween.chain().tween_callback(func(): old.visible = false)
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)

var health := class="hl-number">100

func take_damage(amount: int):
    var old_health = health
    health = max(health - amount, class="hl-number">0)
    health_changed.emit(old_health, health)

    if health <= class="hl-number">0:
        died.emit()

func collect_item(item_name: String, qty: int = class="hl-number">1):
    item_collected.emit(item_name, qty)
Signals

Connecting Signals in Code

Connect signals programmatically with lambdas and bound arguments

GDScript
extends Node

func _ready():
    class="hl-commentclass="hl-string">"># Basic connection
    $Player.health_changed.connect(_on_health_changed)
    $Player.died.connect(_on_player_died)

    class="hl-commentclass="hl-string">"># Lambda connection
    $Button.pressed.connect(func():
        print("Button pressed!class="hl-string">")
    )

    class="hl-commentclass="hl-string">"># One-shot connection (disconnects after first call)
    $Timer.timeout.connect(_on_first_timeout, CONNECT_ONE_SHOT)

    class="hl-commentclass="hl-string">"># Connect with bind (extra arguments)
    for enemy in get_tree().get_nodes_in_group("enemiesclass="hl-string">"):
        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.tscnclass="hl-string">")

func _on_first_timeout():
    print("This only runs once!class="hl-string">")

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
class="hl-commentclass="hl-string">"># EventBus.gd — Add as Autoload in Project Settings
extends Node

class="hl-commentclass="hl-string">"># Game state signals
signal game_started
signal game_paused(is_paused: bool)
signal game_over(score: int)

class="hl-commentclass="hl-string">"># Player signals
signal player_damaged(amount: int)
signal player_healed(amount: int)
signal score_changed(new_score: int)

class="hl-commentclass="hl-string">"># UI signals
signal show_dialog(text: String)
signal notification(message: String, type: String)

class="hl-commentclass="hl-string">"># ──────────────────────────────────────────
class="hl-commentclass="hl-string">"># Usage from any script:
class="hl-commentclass="hl-string">">#
class="hl-commentclass="hl-string">"># Emit:
class="hl-commentclass="hl-string">">#   EventBus.player_damaged.emit(class="hl-number">25)
class="hl-commentclass="hl-string">">#
class="hl-commentclass="hl-string">"># Listen:
class="hl-comment">#   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 = class="hl-string">"user://savegame.json"

func save_game(data: Dictionary) -> bool:
    var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    if not file:
        push_error(class="hl-string">"Cannot open save file: " + str(FileAccess.get_open_error()))
        return false

    file.store_string(JSON.stringify(data, class="hl-string">"  "))
    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(class="hl-string">"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
class="hl-commentclass="hl-string">"># save_data.gd
class_name SaveData
extends Resource

@export var player_name := "Heroclass="hl-string">"
@export var level := class="hl-number">1
@export var health := class="hl-number">100
@export var position := Vector2.ZERO
@export var inventory: Array[String] = []
@export var play_time := class="hl-number">0.0

class="hl-commentclass="hl-string">"># ─── 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 = class="hl-string">"user://settings.cfg"

func save_settings(settings: Dictionary):
    var config = ConfigFile.new()

    config.set_value(class="hl-string">"audio", class="hl-string">"master_volume", settings.get(class="hl-string">"master_volume", class="hl-number">1.0))
    config.set_value(class="hl-string">"audio", class="hl-string">"sfx_volume", settings.get(class="hl-string">"sfx_volume", class="hl-number">1.0))
    config.set_value(class="hl-string">"audio", class="hl-string">"music_volume", settings.get(class="hl-string">"music_volume", class="hl-number">0.8))
    config.set_value(class="hl-string">"video", class="hl-string">"fullscreen", settings.get(class="hl-string">"fullscreen", false))
    config.set_value(class="hl-string">"video", class="hl-string">"vsync", settings.get(class="hl-string">"vsync", true))

    config.save(CONFIG_PATH)

func load_settings() -> Dictionary:
    var config = ConfigFile.new()
    if config.load(CONFIG_PATH) != OK:
        return {}  class="hl-commentclass="hl-string">"># Return defaults

    return {
        "master_volumeclass="hl-string">": config.get_value("audioclass="hl-string">", "master_volumeclass="hl-string">", class="hl-number">1.0),
        "sfx_volumeclass="hl-string">": config.get_value("audioclass="hl-string">", "sfx_volumeclass="hl-string">", class="hl-number">1.0),
        "music_volumeclass="hl-string">": config.get_value("audioclass="hl-string">", "music_volumeclass="hl-string">", class="hl-number">0.8),
        "fullscreenclass="hl-string">": config.get_value("videoclass="hl-string">", "fullscreenclass="hl-string">", false),
        "vsyncclass="hl-string">": config.get_value("videoclass="hl-string">", "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,
        class="hl-number">1  class="hl-commentclass="hl-string">"># Collision mask
    )
    query.exclude = [self]

    var result = space.intersect_ray(query)

    if result.is_empty():
        return true  class="hl-commentclass="hl-string">"># Nothing blocking

    class="hl-comment"># 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 * class="hl-number">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(class="hl-string">"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
class="hl-commentclass="hl-string">"># bullet.gd
extends Area2D

var speed := class="hl-number">600.0
var direction := Vector2.RIGHT
var damage := class="hl-number">10

func _ready():
    class="hl-commentclass="hl-string">"># Auto-destroy after class="hl-number">3 seconds
    var timer = get_tree().create_timer(class="hl-number">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_damageclass="hl-string">"):
        body.take_damage(damage)
    queue_free()

class="hl-commentclass="hl-string">"># ─── In the player/weapon script: ────────
class="hl-commentclass="hl-string">"># var BulletScene = preload("res://bullet.tscnclass="hl-string">")
class="hl-commentclass="hl-string">">#
class="hl-commentclass="hl-string">"># func shoot():
class="hl-commentclass="hl-string">">#     var bullet = BulletScene.instantiate()
class="hl-commentclass="hl-string">">#     bullet.global_position = $Muzzle.global_position
class="hl-commentclass="hl-string">">#     bullet.direction = (get_global_mouse_position() - global_position).normalized()
class="hl-comment">#     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 = {}

class="hl-commentclass="hl-string">"># Preload your sounds
var sounds = {
    "jumpclass="hl-string">": preload("res://audio/sfx/jump.wavclass="hl-string">"),
    "hitclass="hl-string">": preload("res://audio/sfx/hit.wavclass="hl-string">"),
    "coinclass="hl-string">": preload("res://audio/sfx/coin.wavclass="hl-string">"),
}

func play_sfx(sound_name: String, pitch_variation := class="hl-number">0.1):
    if not sounds.has(sound_name):
        return

    var player = AudioStreamPlayer.new()
    player.stream = sounds[sound_name]
    player.pitch_scale = randf_range(class="hl-number">1.0 - pitch_variation, class="hl-number">1.0 + pitch_variation)
    player.bus = "SFXclass="hl-string">"
    add_child(player)
    player.play()

    class="hl-comment"># 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 := class="hl-number">1.0):
    var new_player = AudioStreamPlayer.new()
    new_player.stream = stream
    new_player.bus = class="hl-string">"Music"
    new_player.volume_db = -class="hl-number">80.0
    add_child(new_player)
    new_player.play()

    var tween = create_tween().set_parallel(true)

    class="hl-commentclass="hl-string">"># Fade in new track
    tween.tween_property(new_player, "volume_dbclass="hl-string">", class="hl-number">0.0, fade_duration)

    class="hl-commentclass="hl-string">"># Fade out old track
    if current_player:
        var old = current_player
        tween.tween_property(old, "volume_dbclass="hl-string">", -class="hl-number">80.0, fade_duration)
        tween.chain().tween_callback(old.queue_free)

    current_player = new_player

func stop_music(fade_duration := class="hl-number">1.0):
    if current_player:
        var tween = create_tween()
        var player = current_player
        tween.tween_property(player, "volume_db", -class="hl-number">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

enum State { IDLE, RUN, JUMP, FALL }
var current_state := State.IDLE

func _physics_process(delta):
    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 < class="hl-number">0 else State.FALL
    if abs(velocity.x) > class="hl-number">10:
        return State.RUN
    return State.IDLE

func play_animation():
    match current_state:
        State.IDLE:
            sprite.play(class="hl-string">"idle")
        State.RUN:
            sprite.play(class="hl-string">"run")
            sprite.flip_h = velocity.x < class="hl-number">0
        State.JUMP:
            sprite.play(class="hl-string">"jump")
        State.FALL:
            sprite.play(class="hl-string">"fall")
Animation

Tween UI Effects Collection

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

GDScript
extends Node

class="hl-commentclass="hl-string">"># Punch scale (e.g., coin pickup, button press)
static func punch_scale(node: Node, strength := class="hl-number">1.3, duration := class="hl-number">0.3):
    var tween = node.create_tween()
    tween.tween_property(node, "scaleclass="hl-string">", Vector2.ONE * strength, duration * class="hl-number">0.4)
    tween.tween_property(node, "scaleclass="hl-string">", Vector2.ONE, duration * class="hl-number">0.6).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_ELASTIC)

class="hl-commentclass="hl-string">"># Fade in
static func fade_in(node: CanvasItem, duration := class="hl-number">0.5):
    node.modulate.a = class="hl-number">0.0
    var tween = node.create_tween()
    tween.tween_property(node, "modulate:aclass="hl-string">", class="hl-number">1.0, duration)

class="hl-commentclass="hl-string">"># Slide in from direction
static func slide_in(node: Control, from_offset := Vector2(class="hl-number">0, class="hl-number">30), duration := class="hl-number">0.4):
    var target_pos = node.position
    node.position += from_offset
    node.modulate.a = class="hl-number">0.0
    var tween = node.create_tween().set_parallel(true)
    tween.tween_property(node, "positionclass="hl-string">", target_pos, duration).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
    tween.tween_property(node, "modulate:aclass="hl-string">", class="hl-number">1.0, duration * class="hl-number">0.6)

class="hl-commentclass="hl-string">"># Bounce (e.g., notification)
static func bounce(node: Node, height := class="hl-number">20.0, duration := class="hl-number">0.5):
    var tween = node.create_tween()
    tween.tween_property(node, "position:yclass="hl-string">", node.position.y - height, duration * class="hl-number">0.4).set_ease(Tween.EASE_OUT)
    tween.tween_property(node, "position:y", node.position.y, duration * class="hl-number">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

class="hl-commentclass="hl-string">"># Requires a ShaderMaterial on the Sprite2D with a "flash_amountclass="hl-string">" uniform
class="hl-commentclass="hl-string">"># Or use modulate for a simpler approach:

func flash_damage(duration := class="hl-number">0.15, flashes := class="hl-number">3):
    for i in flashes:
        sprite.modulate = Color.RED
        await get_tree().create_timer(duration / (flashes * class="hl-number">2)).timeout
        sprite.modulate = Color.WHITE
        await get_tree().create_timer(duration / (flashes * class="hl-number">2)).timeout

class="hl-commentclass="hl-string">"># Shader approach (more polished):
class="hl-commentclass="hl-string">"># func flash_white(duration := class="hl-number">0.1):
class="hl-commentclass="hl-string">">#     sprite.material.set_shader_parameter("flash_amountclass="hl-string">", class="hl-number">1.0)
class="hl-commentclass="hl-string">">#     await get_tree().create_timer(duration).timeout
class="hl-commentclass="hl-string">">#     sprite.material.set_shader_parameter("flash_amount", class="hl-number">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 := class="hl-number">100.0
@export var wait_time := class="hl-number">1.5

var current_point := class="hl-number">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()

    class="hl-comment"># Reached the point?
    if global_position.distance_to(target) < class="hl-number">5.0:
        waiting = true
        velocity = Vector2.ZERO
        await get_tree().create_timer(wait_time).timeout
        current_point = (current_point + class="hl-number">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 := class="hl-number">120.0
@export var detection_radius := class="hl-number">200.0
@export var stop_distance := class="hl-number">30.0

var player: Node2D = null

func _ready():
    class="hl-commentclass="hl-string">"># Find player (or use a group)
    player = get_tree().get_first_node_in_group("playerclass="hl-string">")

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

        class="hl-comment"># Face movement direction
        if direction.x != class="hl-number">0:
            $Sprite2D.flip_h = direction.x < class="hl-number">0
    else:
        velocity = velocity.move_toward(Vector2.ZERO, speed * delta * class="hl-number">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 := class="hl-number">80.0
@export var chase_speed := class="hl-number">150.0
@export var detect_range := class="hl-number">250.0
@export var attack_range := class="hl-number">40.0

func _physics_process(delta):
    player = get_tree().get_first_node_in_group(class="hl-string">"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:
            class="hl-commentclass="hl-string">"># Add patrol logic here
            if dist < detect_range:
                change_state(State.CHASE)

        State.CHASE:
            if dist > detect_range * class="hl-number">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 * class="hl-number">1.5:
                change_state(State.CHASE)

    move_and_slide()

func change_state(new_state: State):
    current_state = new_state
    class="hl-comment"># 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 := class="hl-number">150.0
@export var speed := class="hl-number">60.0
@export var idle_time_min := class="hl-number">1.0
@export var idle_time_max := class="hl-number">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) < class="hl-number">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(class="hl-number">40, wander_radius)
    target_position = home_position + Vector2(cos(angle), sin(angle)) * distance
    wandering = true