History

  • How: Developed by Juan Linietsky and Ariel Manzur, first released publicly in 2014 as open-source.
  • Who: Maintained by the Godot Engine community and the Godot Foundation.
  • Why: To provide a fully free, open-source game engine with no royalties, no vendor lock-in, and a clean scene-based architecture.

Introduction

  • Godot is a feature-rich, cross-platform game engine for 2D and 3D games. It uses a unique scene/node system, supports GDScript (Python-like), C#, and C++ (via GDExtension), and exports to Windows, Linux, macOS, Android, iOS, and Web.

Advantages

  • Fully open-source and free — no royalties, no subscription.
  • Lightweight editor (~100MB), fast iteration.
  • Unified 2D and 3D workflows in one engine.
  • GDScript is beginner-friendly and tightly integrated.
  • Active community, frequent releases (Godot 4.x is a major leap).

Disadvantages

  • Smaller ecosystem than Unity/Unreal (fewer ready-made assets).
  • C# support in Godot 4 is still maturing (.NET 6+).
  • 3D rendering less mature than Unreal Engine for AAA-level visuals.

Editor & Project Setup

Editor Layout

  • Scene Panel (left) — tree of all nodes in the current scene.
  • Viewport (center) — visual editor for 2D/3D.
  • Inspector (right) — properties of the selected node.
  • FileSystem (bottom-left) — project files.
  • Output / Debugger (bottom) — logs, errors, profiler.

Key Shortcuts

F5          Run project
F6          Run current scene
Ctrl+S      Save scene
Ctrl+N      New scene
Ctrl+Z      Undo
Q / W / E   Select / Move / Rotate tool (3D)

Project Structure

res://              Root of project (all assets here)
res://scenes/       Scene files (.tscn)
res://scripts/      GDScript files (.gd)
res://assets/       Textures, audio, models
res://addons/       Plugins and extensions
project.godot       Project config file

Scene & Node System

Core Concept

  • Everything in Godot is a Node. A Scene is a tree of nodes saved as a file.
  • Scenes can be instanced inside other scenes — this is the primary composition pattern.
Node (root)
├── Sprite2D
├── CollisionShape2D
└── AudioStreamPlayer

Scene & Resource File Formats

.tscn   Text Scene   — human-readable, version-control friendly (default for scenes)
.scn    Binary Scene — compiled binary, faster to load, used in exported builds
.tres   Text Resource — human-readable resource (materials, custom data, etc.)
.res    Binary Resource — compiled binary version of .tres
.escn   Exported Scene — identical to .tscn but marks the file as externally exported
                         (e.g. from Blender); auto-compiled to .scn on import
  • When you export your game, Godot automatically converts .tscn.scn and .tres.res for performance.
  • Use .tscn / .tres during development (readable diffs in git), and let the exporter handle the rest.
# Loading works the same regardless of format:
var scene = preload("res://scenes/Player.tscn")   # text scene
var scene = preload("res://scenes/Player.scn")    # binary scene (exported)
var data  = preload("res://data/item.tres")       # text resource
var data  = preload("res://data/item.res")        # binary resource

UID (Unique Identifier) System

  • In Godot 4, files and assets can be identified by a UID rather than a hardcoded project path (res://...).

How it Works

  • Every imported asset or resource is assigned a globally unique 64-bit integer ID.
  • This ID is saved inside a .uid file (for text resources) or within the .import files of your project directory.
  • The engine compiles all UIDs into a central database located at .godot/uid_cache.bin.
  • UIDs use the prefix uid:// followed by a base-36 representation of the unique integer (e.g., uid://d4e94a21924e).

Why Use UIDs?

  • Prevents Broken References: If you rename or move an asset (e.g., a texture, script, or sub-scene) within the Godot FileSystem dock, Godot automatically updates its cache, but other scene files referencing it do not break because they reference the stable UID instead of the path.
  • Better Team Collaboration: Decreases merge conflicts in team environments when multiple developers move or organize directories.

Example: Path vs. UID

  • Hardcoded Path reference (Fragile):
# If Player.tscn is moved to res://characters/player/Player.tscn, this fails:
var player_scene = preload("res://scenes/Player.tscn")
  • UID Reference (Resilient):
# This works even if the scene is moved or renamed anywhere in the project:
var player_scene = preload("uid://cc1ygqg530w8x")
  • You can copy a resource’s UID by right-clicking it in the Godot FileSystem dock and selecting Copy UID.

Best Practices & Caveats

  • Version Control: Always commit .uid files to Git. If a .uid file is deleted, Godot will regenerate it, which might assign a new ID and cause reference issues on other machines.
  • Text Files: When looking at a .tscn file, you will see resources linked via both UID and path, like ext_resource type="PackedScene" uid="uid://..." path="res://..." id="1". Godot uses the path as a fallback if the UID cache is rebuilding.

Common Node Types

Node2D/3D          		Base for all 2D/3D nodes (has position, rotation, scale)
Sprite2D/3D				Displays a texture in 2D/3D
AnimatedSprite2D/3D  	Sprite with frame animation
CollisionShape2D/3D		Defines collision area shape
Area2D/3D          		Detects overlaps (no physics response)
CharacterBody2D/3D  	Kinematic body for player/enemies
RigidBody2D/3D     		Physics-simulated body
StaticBody2D/3D    		Immovable physics body (walls, floors)
Camera2D/3D        		2D/3D camera with follow/zoom
Label           		UI text
Button          		UI button
CanvasLayer     		UI layer (always on top of game world)

3D Node Types

Node3D          Base for all 3D nodes
MeshInstance3D  Renders a 3D mesh
CollisionShape3D  3D collision shape
CharacterBody3D  3D kinematic body
RigidBody3D     3D physics body
Camera3D        3D camera
DirectionalLight3D  Sun-like light
OmniLight3D     Point light (all directions)
SpotLight3D     Cone-shaped spotlight
WorldEnvironment  Sky, ambient light, fog, tone mapping

Instancing Scenes

# In editor: drag .tscn into scene tree
# In code:
var bullet_scene = preload("res://scenes/Bullet.tscn")
 
func shoot():
    var bullet = bullet_scene.instantiate()
    bullet.position = $Muzzle.global_position
    get_parent().add_child(bullet)

GDScript

  • GDScript is Godot’s built-in scripting language — Python-like syntax, tightly integrated with the engine, supports static typing, signals, coroutines, lambdas, and full OOP.
  • For the complete in-depth GDScript language reference (variables, types, OOP, signals, annotations, coroutines, patterns, and more), see the dedicated GDScript note.
  • Quick reference:
    extends CharacterBody2D  # inherit a Godot class
    class_name Player        # register as global class
     
    @export var speed: float = 200.0   # editable in Inspector
    @onready var anim := $AnimationPlayer  # assigned at _ready
     
    signal health_changed(new_hp: int)
     
    var health: int = 100:
        set(v):
            health = clamp(v, 0, 100)
            health_changed.emit(health)
     
    func _physics_process(delta: float) -> void:
        var dir = Input.get_axis("move_left", "move_right")
        velocity.x = dir * speed
        move_and_slide()

Signals & Event-Driven Architecture

  • Signals are Godot’s implementation of the Observer Pattern. They allow nodes to send notifications to other nodes without needing direct references, enabling a decoupled, event-driven codebase.
  • The golden rule of Godot scene design: “Signals Up, Call Down”. Parents call methods on children directly; children emit signals to notify parents of events.

Defining & Emitting Signals

  • Signals are declared at the top of a script using the signal keyword. They can define typed arguments.
extends CharacterBody2D
 
# 1. Declaration (simple)
signal clicked
 
# 2. Declaration with typed arguments (highly recommended)
signal health_changed(old_value: int, new_value: int)
signal status_effect_applied(effect_name: StringName, duration: float)
 
var health: int = 100
 
func take_damage(amount: int) -> void:
    var old_health = health
    health = max(0, health - amount)
    
    # 3. Emitting the signal
    health_changed.emit(old_health, health)

Connecting Signals

1. Via the Editor UI

  • Select the node in the Scene tree.
  • Go to the Node tab (next to Inspector) → Signals.
  • Double-click a signal → Select the target node → Click Connect.
  • This creates a connection entry visible in the Editor and saved in the .tscn file.

2. Via Code (GDScript Callables)

  • In Godot 4, connections are made using the connect method on the signal object, passing a Callable (method reference).
# Connect signal to local method
func _ready() -> void:
    $Button.pressed.connect(_on_button_pressed)
    
    # Checking if connected
    if not health_changed.is_connected(_on_health_changed):
        health_changed.connect(_on_health_changed)
 
func _on_button_pressed() -> void:
    print("Button clicked!")
 
func _on_health_changed(old_val: int, new_val: int) -> void:
    print("Health went from ", old_val, " to ", new_val)

Advanced Signal Connections

1. Passing Extra Arguments (Binding)

  • You can bind extra arguments to a connection that weren’t defined in the original signal. This is useful for passing data like indices or node references.
# Multiple buttons call the same method, but pass different parameters
func _ready() -> void:
    $BuySwordButton.pressed.connect(_on_purchase_clicked.bind("sword", 150))
    $BuyShieldButton.pressed.connect(_on_purchase_clicked.bind("shield", 100))
 
func _on_purchase_clicked(item_type: String, cost: int) -> void:
    print("Purchasing: ", item_type, " for ", cost, " gold.")

2. Lambda (Anonymous) Connections

  • You can connect a signal directly to an inline anonymous function (lambda).
func _ready() -> void:
    # Basic inline lambda
    $Timer.timeout.connect(func(): print("Time is up!"))
    
    # Lambda with closure (captures local variables)
    var local_multiplier = 1.5
    $Enemy.died.connect(func(xp_reward: int):
        add_experience(xp_reward * local_multiplier)
    )

3. Connection Flags (Custom Behavior)

  • You can pass connection flags as an optional parameter to connect().
# Connect flags are passed via ConnectFlags enum
 
# CONNECT_DEFERRED: Defers the call to the end of the physics/idle frame.
# Vital when modifying physics states or scene hierarchy during collision signals.
body_entered.connect(_on_body_entered, CONNECT_DEFERRED)
 
# CONNECT_ONE_SHOT: Automatically disconnects the signal after it fires once.
level_loaded.connect(_on_level_init, CONNECT_ONE_SHOT)
 
# CONNECT_PERSISTED: Connection is saved when the scene is saved (used inside @tool scripts).
property_changed.connect(_on_prop_update, CONNECT_PERSISTED)

Signal Disconnection & Safety

  • Connections are automatically removed when either the emitter or receiver node is deleted (queue_free()), so memory leaks are rarely an issue for scene nodes.
  • However, manual disconnection is sometimes needed:
# Manual disconnect
if button.pressed.is_connected(_on_button_pressed):
    button.pressed.disconnect(_on_button_pressed)
  • To prevent calling code on an object that is in the middle of deletion or check if a Callable is valid:
# Check if target object exists and callable is valid
var my_callable = Callable(target_node, "receive_event")
if my_callable.is_valid():
    my_callable.call()

Component-Based Architecture

The Composition Pattern

  • In game development, deep inheritance trees (e.g. EntityActorCharacterPlayer) cause rigid codebases. If both a Player and an ExplosiveBarrel need health, putting health in a shared ancestor class forces it onto nodes that shouldn’t have it (like static walls).
  • Godot implements Composition using Nodes as Components. Instead of inheriting features, a parent node gets its capabilities by having small, specialized child nodes attached to it.

Composition vs. Inheritance Architecture

INHERITANCE PATHWAY (Rigid)
Node2D ── PhysicsBody2D ── CharacterBody2D ── LivingEntity ── Player
                                                 │
                                                 └── Enemy (forces health on static objects)

COMPOSITION PATHWAY (Modular)
CharacterBody2D (Player)
├── HealthComponent (Node) - Manages health variables and math
├── HitboxComponent (Area2D) - Detects incoming damage
├── InputComponent (Node) - Gathers inputs
└── VelocityComponent (Node) - Handles movement math

In-Depth Tutorial: Creating Components

  • Here is a complete component system for damage, health, and movement.

1. HealthComponent

  • A pure logical component (extends Node) that tracks health, handles heals/damage, and notifies the parent via signals.
# res://components/HealthComponent.gd
class_name HealthComponent
extends Node
 
signal health_changed(old_value: int, new_value: int)
signal health_depleted
 
@export var max_health: int = 100
@onready var current_health: int = max_health
 
func damage(amount: int) -> void:
    if amount <= 0: return
    var old_health = current_health
    current_health = max(0, current_health - amount)
    health_changed.emit(old_health, current_health)
    
    if current_health == 0:
        health_depleted.emit()
 
func heal(amount: int) -> void:
    if amount <= 0: return
    var old_health = current_health
    current_health = min(max_health, current_health + amount)
    health_changed.emit(old_health, current_health)

2. HitboxComponent

  • An Area2D component that represents the physical area where an object can receive damage. It delegates damage to a HealthComponent.
# res://components/HitboxComponent.gd
class_name HitboxComponent
extends Area2D
 
# Reference to the health component representing this object
@export var health_component: HealthComponent
 
# Allows specifying a damage multiplier (e.g., 2.0 for a headshot hitbox)
@export var damage_multiplier: float = 1.0
 
func receive_damage(base_damage: int) -> void:
    if health_component:
        var final_damage = int(base_damage * damage_multiplier)
        health_component.damage(final_damage)

3. HurtboxComponent

  • An Area2D component that represents the area that inflicts damage. It monitors overlaps with HitboxComponents.
# res://components/HurtboxComponent.gd
class_name HurtboxComponent
extends Area2D
 
@export var damage: int = 10
 
func _ready() -> void:
    # Connect collision signal
    area_entered.connect(_on_area_entered)
 
func _on_area_entered(area: Area2D) -> void:
    # Check if overlapping area is a Hitbox
    if area is HitboxComponent:
        area.receive_damage(damage)

4. VelocityComponent

  • Manages movement velocity calculations, easing friction, and acceleration, keeping movement math separate from input collecting.
# res://components/VelocityComponent.gd
class_name VelocityComponent
extends Node
 
@export var max_speed: float = 300.0
@export var acceleration: float = 1200.0
@export var friction: float = 800.0
 
var velocity: Vector2 = Vector2.ZERO
 
func accelerate_in_direction(direction: Vector2, delta: float) -> void:
    if direction != Vector2.ZERO:
        velocity = velocity.move_toward(direction * max_speed, acceleration * delta)
    else:
        velocity = velocity.move_toward(Vector2.ZERO, friction * delta)
 
func apply_to_body(body: CharacterBody2D) -> void:
    body.velocity = velocity
    body.move_and_slide()
    # Save resolved velocity back
    velocity = body.velocity

Connecting Components in a Scene

  • In your main Entity scene (e.g., Player or Enemy), you arrange these components as children and write a minimal orchestration script.

Scene Tree Setup

CharacterBody2D (Player) - (Script: player.gd)
├── Sprite2D (Visual)
├── CollisionShape2D (Collides with walls)
├── HealthComponent (Saves stats)
├── HitboxComponent (Child Area2D)
│   └── CollisionShape2D (Defines vulnerability zone)
└── VelocityComponent (Calculates velocity)
  • Important: In the editor, assign HealthComponent to the export property health_component on the HitboxComponent.

Orchestration Script (player.gd)

extends CharacterBody2D
 
@onready var health_comp: HealthComponent = $HealthComponent
@onready var velocity_comp: VelocityComponent = $VelocityComponent
 
func _ready() -> void:
    # Orchestrate responses to component signals
    health_comp.health_depleted.connect(_on_death)
    health_comp.health_changed.connect(_on_health_changed)
 
func _physics_process(delta: float) -> void:
    # Gather inputs and delegate calculations to components
    var move_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    
    # Calculate speed and apply movement physics
    velocity_comp.accelerate_in_direction(move_dir, delta)
    velocity_comp.apply_to_body(self)
 
func _on_health_changed(old_hp: int, new_hp: int) -> void:
    print("Ouch! Player health is now: ", new_hp)
    # e.g., trigger screen shake or hit flash shader
 
func _on_death() -> void:
    set_physics_process(false)
    # play death animation, then delete
    queue_free()

Core Lifecycle Methods

Built-in Callbacks

extends Node
 
func _init() -> void:
    # Called when object is created (before added to scene)
    pass
 
func _ready() -> void:
    # Called once when node enters the scene tree
    # All children are ready at this point
    pass
 
func _process(delta: float) -> void:
    # Called every frame (tied to FPS)
    # delta = time since last frame in seconds
    position.x += speed * delta
 
func _physics_process(delta: float) -> void:
    # Called every physics tick (default 60/s, fixed timestep)
    # Use for movement, physics, collision
    move_and_slide()
 
func _input(event: InputEvent) -> void:
    # Called for every input event
    if event is InputEventKey:
        print(event.keycode)
 
func _unhandled_input(event: InputEvent) -> void:
    # Called if no other node consumed the input
    pass
 
func _notification(what: int) -> void:
    # Low-level lifecycle notifications
    if what == NOTIFICATION_WM_CLOSE_REQUEST:
        get_tree().quit()
 
func _exit_tree() -> void:
    # Called when node is removed from scene tree
    pass

delta Time Pattern

# Always multiply movement by delta for frame-rate independence
func _process(delta: float) -> void:
    position += velocity * delta
    # Without delta: moves 200px per frame (FPS-dependent)
    # With delta:    moves 200px per second (FPS-independent)

Input System

Input Actions (Project Settings → Input Map)

# Check action state
func _process(delta: float) -> void:
    if Input.is_action_pressed("move_right"):
        velocity.x = speed
    elif Input.is_action_pressed("move_left"):
        velocity.x = -speed
    else:
        velocity.x = 0
 
    if Input.is_action_just_pressed("jump"):
        jump()
 
    if Input.is_action_just_released("attack"):
        end_attack()
 
# Get axis (returns -1, 0, or 1)
var h = Input.get_axis("move_left", "move_right")
var v = Input.get_axis("move_up", "move_down")
var dir = Vector2(h, v).normalized()

Mouse Input

func _input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
            print("Left click at: ", event.position)
 
    if event is InputEventMouseMotion:
        print("Mouse moved: ", event.relative)
 
# Get mouse position
var mouse_pos = get_global_mouse_position()

Gamepad Input

# Axis (joystick)
var h = Input.get_joy_axis(0, JOY_AXIS_LEFT_X)
var v = Input.get_joy_axis(0, JOY_AXIS_LEFT_Y)
 
# Button
if Input.is_joy_button_pressed(0, JOY_BUTTON_A):
    jump()

2D Game Development

CharacterBody2D — Player Movement

extends CharacterBody2D
 
const SPEED = 200.0
const JUMP_VELOCITY = -400.0
const GRAVITY = 980.0
 
func _physics_process(delta: float) -> void:
    # Apply gravity
    if not is_on_floor():
        velocity.y += GRAVITY * delta
 
    # Horizontal movement
    var direction = Input.get_axis("move_left", "move_right")
    velocity.x = direction * SPEED
 
    # Jump
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY
 
    move_and_slide()  # handles collision automatically

Area2D — Overlap Detection

extends Area2D
 
func _ready() -> void:
    body_entered.connect(_on_body_entered)
    area_entered.connect(_on_area_entered)
 
func _on_body_entered(body: Node2D) -> void:
    if body.is_in_group("player"):
        body.take_damage(10)
        queue_free()
 
func _on_area_entered(area: Area2D) -> void:
    print("Area overlap: ", area.name)

Sprite2D & AnimatedSprite2D

# Sprite2D — single texture
$Sprite2D.texture = preload("res://assets/player.png")
$Sprite2D.flip_h = true   # mirror horizontally
 
# AnimatedSprite2D — frame-based animation
# Set up SpriteFrames resource in editor, then:
$AnimatedSprite2D.play("run")
$AnimatedSprite2D.stop()
$AnimatedSprite2D.animation = "idle"
$AnimatedSprite2D.frame = 0
 
# Connect animation_finished signal
$AnimatedSprite2D.animation_finished.connect(_on_anim_done)

TileMap — Level Design

# TileMap node with TileSet resource
# In editor: paint tiles, set collision layers
 
# In code: read/write tiles
var tilemap = $TileMap
var cell = tilemap.get_cell_source_id(0, Vector2i(5, 3))
tilemap.set_cell(0, Vector2i(5, 3), 1, Vector2i(0, 0))
 
# Convert world position to tile coords
var tile_pos = tilemap.local_to_map(player.position)

3D Game Development

CharacterBody3D — 3D Player Movement

extends CharacterBody3D
 
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
 
func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y -= gravity * delta
 
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY
 
    var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
    var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
 
    if direction:
        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)
        velocity.z = move_toward(velocity.z, 0, SPEED)
 
    move_and_slide()

Camera3D — First Person Look

extends Node3D
 
@export var sensitivity: float = 0.003
@onready var camera = $Camera3D
 
func _ready() -> void:
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
 
func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        rotate_y(-event.relative.x * sensitivity)
        camera.rotate_x(-event.relative.y * sensitivity)
        camera.rotation.x = clamp(camera.rotation.x, -PI/2, PI/2)

MeshInstance3D & Materials

# Assign mesh in editor or code
var mesh_instance = $MeshInstance3D
 
# Create material in code
var mat = StandardMaterial3D.new()
mat.albedo_color = Color.RED
mat.metallic = 0.5
mat.roughness = 0.3
mesh_instance.material_override = mat
 
# Load texture
mat.albedo_texture = preload("res://assets/texture.png")

Raycasting 3D

func shoot_ray() -> void:
    var space_state = get_world_3d().direct_space_state
    var cam = $Camera3D
    var from = cam.global_position
    var to = from + (-cam.global_transform.basis.z * 100.0)
 
    var query = PhysicsRayQueryParameters3D.create(from, to)
    query.exclude = [self]
    var result = space_state.intersect_ray(query)
 
    if result:
        print("Hit: ", result.collider.name)
        print("Point: ", result.position)
        print("Normal: ", result.normal)

Physics System

Collision Layers & Masks

Layer:  What this object IS (what layer it occupies)
Mask:   What this object DETECTS (what layers it scans)

Example:
  Player  → Layer 1, Mask 2 (detects enemies)
  Enemy   → Layer 2, Mask 1 (detects player)
  Bullet  → Layer 3, Mask 2 (detects enemies only)
# Set in editor (Physics → Layers) or in code:
collision_layer = 1   # bit 1
collision_mask  = 2   # bit 2
 
# Check group membership
if body.is_in_group("enemy"):
    body.take_damage(10)

RigidBody2D / RigidBody3D

extends RigidBody2D
 
func _ready() -> void:
    # Apply impulse (instant force)
    apply_impulse(Vector2(0, -500))
 
    # Apply continuous force
    apply_force(Vector2(100, 0))
 
    # Set linear velocity directly
    linear_velocity = Vector2(200, 0)
 
    # Connect body_entered signal
    body_entered.connect(_on_collision)
 
func _on_collision(body: Node) -> void:
    print("Collided with: ", body.name)

Groups

# Add to group in editor (Node → Groups) or code:
add_to_group("enemies")
add_to_group("collectibles")
 
# Check
if is_in_group("enemies"):
    take_damage(10)
 
# Call method on all in group
get_tree().call_group("enemies", "freeze")
 
# Get all nodes in group
var enemies = get_tree().get_nodes_in_group("enemies")

Animation System

AnimationPlayer

@onready var anim = $AnimationPlayer
 
# Play animation
anim.play("walk")
anim.play_backwards("walk")
anim.stop()
anim.pause()
 
# Blend / transition
anim.play("run", -1, 1.0, false)  # (name, blend_time, speed, from_end)
 
# Check state
print(anim.current_animation)
print(anim.is_playing())
 
# Connect signals
anim.animation_finished.connect(_on_anim_finished)
 
func _on_anim_finished(anim_name: StringName) -> void:
    if anim_name == "attack":
        anim.play("idle")

AnimationTree (State Machine)

@onready var anim_tree = $AnimationTree
@onready var state_machine = anim_tree.get("parameters/playback")
 
func _ready() -> void:
    anim_tree.active = true
 
func _physics_process(delta: float) -> void:
    # Set blend parameters
    anim_tree.set("parameters/blend_position", velocity.normalized())
 
    # Travel to state
    if is_on_floor():
        state_machine.travel("idle")
    else:
        state_machine.travel("jump")

Tween — Procedural Animation

# Animate any property smoothly
func animate_in() -> void:
    var tween = create_tween()
    tween.tween_property(self, "position", Vector2(200, 100), 0.5)
    tween.tween_property(self, "modulate:a", 1.0, 0.3)
 
# Chain tweens
var tween = create_tween().set_loops()
tween.tween_property($Sprite, "scale", Vector2(1.2, 1.2), 0.5)
tween.tween_property($Sprite, "scale", Vector2(1.0, 1.0), 0.5)
 
# Easing
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_BOUNCE)

UI System (Control Nodes)

Common UI Nodes

Control         Base UI node
Label           Display text
Button          Clickable button
TextEdit        Multi-line text input
LineEdit        Single-line text input
ProgressBar     Health/loading bar
Slider          Value slider
CheckBox        Toggle checkbox
OptionButton    Dropdown menu
Panel           Background panel
VBoxContainer   Vertical layout
HBoxContainer   Horizontal layout
GridContainer   Grid layout
ScrollContainer Scrollable area

UI Scripting

extends CanvasLayer
 
@onready var health_bar = $HealthBar
@onready var score_label = $ScoreLabel
@onready var menu_button = $MenuButton
 
func _ready() -> void:
    menu_button.pressed.connect(_on_menu_pressed)
 
func update_health(value: int) -> void:
    health_bar.value = value
 
func update_score(score: int) -> void:
    score_label.text = "Score: " + str(score)
 
func _on_menu_pressed() -> void:
    get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")

Anchors & Layout

Anchor presets (Inspector → Layout):
  Top Left / Top Right / Bottom Left / Bottom Right
  Center / Full Rect (stretches to fill parent)

# In code:
$Label.anchor_left = 0.0
$Label.anchor_right = 1.0   # stretch full width
$Label.offset_left = 10
$Label.offset_right = -10

Audio System

AudioStreamPlayer

# AudioStreamPlayer     — non-positional (music, UI sounds)
# AudioStreamPlayer2D   — positional 2D audio
# AudioStreamPlayer3D   — positional 3D audio
 
@onready var music = $AudioStreamPlayer
@onready var sfx = $SFX
 
func _ready() -> void:
    music.stream = preload("res://audio/theme.ogg")
    music.play()
 
func play_jump_sound() -> void:
    sfx.stream = preload("res://audio/jump.wav")
    sfx.play()
 
# Volume (in dB)
music.volume_db = -10.0
 
# Pitch
sfx.pitch_scale = randf_range(0.9, 1.1)  # random pitch variation
 
# Stop
music.stop()
print(music.playing)  # false

Audio Buses

Audio buses (Project → Audio):
  Master → Music → SFX → Ambient

# Assign bus in code:
$AudioStreamPlayer.bus = "Music"

# Control bus volume:
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Music"), -5.0)
AudioServer.set_bus_mute(AudioServer.get_bus_index("SFX"), true)

Resource System & Data Management

Resources

# Resources are data containers saved as .tres or .res files
# Examples: Texture2D, AudioStream, Material, TileSet, Font
 
# Load resource
var texture = load("res://assets/player.png")
var texture = preload("res://assets/player.png")  # compile-time load
 
# Custom Resource
class_name ItemData
extends Resource
 
@export var item_name: String = ""
@export var damage: int = 0
@export var icon: Texture2D
 
# Use it:
var sword = ItemData.new()
sword.item_name = "Iron Sword"
sword.damage = 25
ResourceSaver.save(sword, "res://data/iron_sword.tres")
 
# Load back:
var loaded = load("res://data/iron_sword.tres") as ItemData

@export — Inspector Variables

extends Node
 
@export var speed: float = 200.0
@export var health: int = 100
@export var sprite: Texture2D
@export var enemy_scene: PackedScene
@export_range(0, 100) var volume: float = 50.0
@export_enum("Idle", "Run", "Attack") var state: int = 0
@export_color_no_alpha var tint: Color = Color.WHITE

Saving & Loading Game Data

# Save with ConfigFile (simple key-value)
func save_game() -> void:
    var config = ConfigFile.new()
    config.set_value("player", "health", health)
    config.set_value("player", "position", position)
    config.save("user://save.cfg")
 
func load_game() -> void:
    var config = ConfigFile.new()
    if config.load("user://save.cfg") == OK:
        health = config.get_value("player", "health", 100)
        position = config.get_value("player", "position", Vector2.ZERO)
 
# Save with JSON
func save_json() -> void:
    var data = {"health": health, "score": score}
    var file = FileAccess.open("user://save.json", FileAccess.WRITE)
    file.store_string(JSON.stringify(data))
 
func load_json() -> void:
    var file = FileAccess.open("user://save.json", FileAccess.READ)
    if file:
        var data = JSON.parse_string(file.get_as_text())
        health = data["health"]

Scene Management

Changing Scenes

# Change scene (unloads current, loads new)
get_tree().change_scene_to_file("res://scenes/Level2.tscn")
 
# Change scene from PackedScene
var next_level = preload("res://scenes/Level2.tscn")
get_tree().change_scene_to_packed(next_level)
 
# Reload current scene
get_tree().reload_current_scene()
 
# Quit game
get_tree().quit()

Autoloads (Singletons)

Project Settings → Autoload → add script/scene
Name: GameManager, Path: res://scripts/GameManager.gd
# GameManager.gd (autoload singleton)
extends Node
 
var score: int = 0
var lives: int = 3
 
signal score_changed(new_score: int)
 
func add_score(points: int) -> void:
    score += points
    score_changed.emit(score)
 
# Access from any script:
GameManager.add_score(100)
print(GameManager.score)

SceneTree & Pausing

# Pause the game
get_tree().paused = true
 
# Node process mode (Inspector → Process Mode):
# Inherit / Pausable / When Paused / Always / Disabled
 
# In code:
process_mode = Node.PROCESS_MODE_ALWAYS  # runs even when paused (UI)
process_mode = Node.PROCESS_MODE_PAUSABLE # stops when paused (game objects)

Shaders & Visual Effects

Shader Basics (Godot Shading Language)

// Spatial shader (3D)
shader_type spatial;
 
uniform vec4 albedo_color : source_color = vec4(1.0);
uniform sampler2D texture_albedo : source_color;
 
void fragment() {
    ALBEDO = texture(texture_albedo, UV).rgb * albedo_color.rgb;
    ROUGHNESS = 0.5;
    METALLIC = 0.0;
}
 
// Canvas item shader (2D)
shader_type canvas_item;
 
void fragment() {
    vec4 col = texture(TEXTURE, UV);
    COLOR = col * vec4(1.0, 0.5, 0.5, 1.0);  // red tint
}

Shader Parameters from GDScript

var mat = $MeshInstance3D.material_override as ShaderMaterial
mat.set_shader_parameter("albedo_color", Color.RED)
mat.set_shader_parameter("speed", 2.0)

GPUParticles2D / GPUParticles3D

@onready var particles = $GPUParticles2D
 
# Trigger burst
func explode() -> void:
    particles.emitting = true
    particles.one_shot = true
    particles.restart()
 
# Continuous
particles.emitting = true
particles.amount = 100
particles.lifetime = 1.5

Environment & Post-Processing

# WorldEnvironment node → Environment resource
# Settings: Sky, Ambient Light, Fog, Glow, SSAO, SSR, Tone Mapping
 
var env = $WorldEnvironment.environment
env.fog_enabled = true
env.fog_density = 0.01
env.glow_enabled = true
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC

Networking & Multiplayer

High-Level Multiplayer API

# Server setup (ENet — UDP-based, low latency)
var peer = ENetMultiplayerPeer.new()
peer.create_server(PORT, MAX_CLIENTS)   # PORT e.g. 9999
multiplayer.multiplayer_peer = peer
 
# Client setup
var peer = ENetMultiplayerPeer.new()
peer.create_client("127.0.0.1", PORT)
multiplayer.multiplayer_peer = peer
 
# Signals
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.connected_to_server.connect(_on_connected)
multiplayer.connection_failed.connect(_on_failed)
 
func _on_peer_connected(id: int) -> void:
    print("Peer joined: ", id)

RPC — Remote Procedure Calls

# Annotate functions with @rpc to call them on remote peers
 
@rpc("any_peer")                    # any peer can call this
func take_damage(amount: int) -> void:
    health -= amount
 
@rpc("authority", "call_local")     # only server calls, runs locally too
func sync_position(pos: Vector2) -> void:
    position = pos
 
@rpc("any_peer", "reliable")        # guaranteed delivery (like TCP)
func chat_message(msg: String) -> void:
    print(msg)
 
# Call on specific peer
take_damage.rpc_id(peer_id, 25)
 
# Call on all peers
sync_position.rpc(global_position)

MultiplayerSpawner & MultiplayerSynchronizer

# MultiplayerSpawner — auto-spawns nodes on all clients
# Add MultiplayerSpawner node, set spawn_path and auto_spawn_list in editor
 
# MultiplayerSynchronizer — syncs properties automatically
# Add MultiplayerSynchronizer node, configure replication in editor
# Or in code:
var sync = $MultiplayerSynchronizer
sync.root_path = NodePath("..")
# Then add properties to replicate via editor or:
# ReplicationConfig resource → add properties like "position", "health"

WebSocket (for Web exports)

# Server
var ws_server = WebSocketMultiplayerPeer.new()
ws_server.create_server(PORT)
multiplayer.multiplayer_peer = ws_server
 
# Client (works in browser)
var ws_client = WebSocketMultiplayerPeer.new()
ws_client.create_client("ws://localhost:9999")
multiplayer.multiplayer_peer = ws_client

Debugging & Profiling

print("value: ", health)           # basic print
print_rich("[color=red]Error![/color]")  # colored output
push_warning("Low health!")        # shows in debugger as warning
push_error("Critical failure!")    # shows as error, doesn't stop execution
assert(health > 0, "Health must be positive")  # crashes in debug builds
 
# Print only in debug builds
if OS.is_debug_build():
    print("Debug info: ", position)

Debugger Panel

Bottom panel → Debugger tab:
  Errors     — runtime errors and warnings
  Stack Trace — call stack when paused/crashed
  Inspector  — live node property inspection while running
  Profiler   — CPU time per function (enable before running)
  Visual Profiler — GPU frame time breakdown
  Network    — RPC and sync traffic monitor
  Monitors   — FPS, memory, physics objects, draw calls

Breakpoints

Click the line number gutter in the script editor to set a breakpoint.
Run the game → execution pauses at the breakpoint.
Use Step Over (F10), Step Into (F11), Continue (F12) to navigate.
Inspect local variables in the Debugger → Stack Locals panel.

Performance Tips

# Use _physics_process for physics, _process for visuals only
# Cache node references with @onready instead of $Node in loops
@onready var player = $Player   # cached once
 
# Avoid get_node() in hot loops
# Use object pooling for bullets/particles instead of queue_free + instantiate
 
# Check draw calls: Debug → Visible Collision Shapes / Navigation
# Use VisibleOnScreenNotifier2D/3D to disable off-screen processing
 
# Profile with:
var time = Time.get_ticks_usec()
# ... code ...
print("Took: ", Time.get_ticks_usec() - time, " µs")

Exporting Your Game

Export Setup

1. Editor → Export → Add preset (Windows, Linux, macOS, Android, iOS, Web)
2. Download export templates: Editor → Manage Export Templates
3. Configure per-platform settings (icon, name, permissions)
4. Click Export Project → choose output folder

Export Presets

Windows Desktop  → .exe + .pck  (or embedded single .exe)
Linux/X11        → ELF binary + .pck
macOS            → .app bundle
Android          → .apk or .aab (needs Android SDK + JDK)
iOS              → Xcode project (needs macOS + Xcode)
Web (HTML5)      → .html + .js + .wasm + .pck

PCK Files

# .pck = packed resource file containing all game assets
# Can be distributed separately from the executable
# Useful for DLC or updates
 
# Load external .pck at runtime:
ProjectSettings.load_resource_pack("res://dlc_pack.pck")

Android Export

Requirements:
  - Android SDK (API 28+)
  - JDK 17+
  - Godot Android export templates

Editor → Export → Android:
  - Set package name (e.g. com.yourname.yourgame)
  - Set keystore for release builds
  - Enable permissions (INTERNET, VIBRATE, etc.)
  - Min SDK: 21 (Android 5.0)

C# in Godot 4

Setup

Use the Godot .NET version (not the standard build).
Download: godotengine.org → .NET version
Requires: .NET SDK 6.0 or later
IDE: VS Code (with C# extension) or JetBrains Rider

C# Script Basics

using Godot;
 
public partial class Player : CharacterBody2D
{
    [Export] public float Speed = 200.0f;
    [Export] public int Health = 100;
 
    public override void _Ready()
    {
        GD.Print("Player ready!");
    }
 
    public override void _PhysicsProcess(double delta)
    {
        var velocity = Velocity;
 
        var direction = Input.GetAxis("move_left", "move_right");
        velocity.X = direction * Speed;
 
        Velocity = velocity;
        MoveAndSlide();
    }
 
    public void TakeDamage(int amount)
    {
        Health -= amount;
        if (Health <= 0) QueueFree();
    }
}

Signals in C#

// Define signal
[Signal] public delegate void HealthChangedEventHandler(int newHealth);
 
// Emit
EmitSignal(SignalName.HealthChanged, Health);
 
// Connect
someNode.HealthChanged += OnHealthChanged;
 
private void OnHealthChanged(int newHealth)
{
    GD.Print("Health: ", newHealth);
}

GDScript vs C# — When to Use

GDScript:
  + Faster iteration, no compile step
  + Tightly integrated with Godot API
  + Best for game logic, UI, scripting
  - Slower than C# for heavy computation

C#:
  + Faster execution (JIT compiled)
  + Strong typing, better IDE support
  + Familiar for Unity developers
  - Requires .NET build, slower hot-reload
  - Some Godot 4 features lag behind GDScript support

GDExtension (C++ / Rust / Other Languages)

What is GDExtension

GDExtension lets you write native code (C++, Rust, Swift, etc.)
that integrates with Godot as if it were built-in.
Use it for: performance-critical systems, existing C++ libraries,
custom physics, or platform-specific features.

Replaces GDNative from Godot 3.
Uses godot-cpp bindings (official C++ library).

Minimal C++ GDExtension

// my_node.h
#include <godot_cpp/classes/node.hpp>
using namespace godot;
 
class MyNode : public Node {
    GDCLASS(MyNode, Node)
 
static void _bind_methods();
public:
    void hello();
};
 
// my_node.cpp
#include "my_node.h"
#include <godot_cpp/core/class_db.hpp>
 
void MyNode::_bind_methods() {
    ClassDB::bind_method(D_METHOD("hello"), &MyNode::hello);
}
 
void MyNode::hello() {
    UtilityFunctions::print("Hello from C++!");
}
  • Build with SCons, output a .gdextension file pointing to your .dll/.so.

Advanced Godot Design Patterns & Tips

State Machine Pattern

  • For complex logic trees (like a player character who can move, jump, slide, wall-climb, and attack), basic if/else checks rapidly descend into “spaghetti code”. A Finite State Machine (FSM) ensures a character is in exactly one state at a time and defines clear rules for changing states.

1. Simple Enum State Machine (Best for simple AI / simple actors)

enum State { IDLE, RUN, JUMP, FALL }
var current_state: State = State.IDLE
 
func _physics_process(delta: float) -> void:
    # State action execution
    match current_state:
        State.IDLE: _state_idle(delta)
        State.RUN:  _state_run(delta)
        State.JUMP: _state_jump(delta)
        State.FALL: _state_fall(delta)
 
    # State transition logic
    _check_transitions()
 
func _check_transitions() -> void:
    var on_floor = is_on_floor()
    var move_input = Input.get_axis("move_left", "move_right") != 0
    
    match current_state:
        State.IDLE:
            if move_input: current_state = State.RUN
            elif not on_floor: current_state = State.FALL
        State.RUN:
            if not move_input: current_state = State.IDLE
            elif not on_floor: current_state = State.FALL

2. Node-Based State Machine (Best for complex actors/bosses)

  • Each state is represented by its own node/script. This keeps code modular, files short, and makes adding states easy.

The State Base Class

# res://scripts/fsm/State.gd
class_name State
extends Node
 
# Reference to the FSM manager
var fsm: StateMachine
# Reference to the physical entity (e.g. CharacterBody2D)
var actor: CharacterBody2D
 
# Virtual methods to be overridden by child states:
func enter() -> void: pass
func exit() -> void: pass
func handle_input(event: InputEvent) -> void: pass
func update(delta: float) -> void: pass
func physics_update(delta: float) -> void: pass

The StateMachine Manager Class

# res://scripts/fsm/StateMachine.gd
class_name StateMachine
extends Node
 
@export var initial_state: State
var current_state: State
 
# Assign physical actor to pass down to states
@onready var actor: CharacterBody2D = get_parent()
 
func _ready() -> void:
    # Setup all children states
    for child in get_children():
        if child is State:
            child.fsm = self
            child.actor = actor
            
    if initial_state:
        transition_to(initial_state.name)
 
func _unhandled_input(event: InputEvent) -> void:
    if current_state:
        current_state.handle_input(event)
 
func _process(delta: float) -> void:
    if current_state:
        current_state.update(delta)
 
func _physics_process(delta: float) -> void:
    if current_state:
        current_state.physics_update(delta)
 
func transition_to(target_state_name: String) -> void:
    var target_state = get_node(target_state_name) as State
    if not target_state: 
        push_error("State machine: State not found: " + target_state_name)
        return
        
    if current_state:
        current_state.exit()
        
    current_state = target_state
    current_state.enter()

Example Concrete State: PlayerIdleState

# res://scripts/fsm/PlayerIdleState.gd
class_name PlayerIdleState
extends State
 
func enter() -> void:
    actor.get_node("AnimationPlayer").play("idle")
    actor.velocity.x = 0
 
func physics_update(delta: float) -> void:
    # Apply gravity
    if not actor.is_on_floor():
        fsm.transition_to("PlayerFallState")
        return
        
    # Transition to run if input detected
    var input = Input.get_axis("move_left", "move_right")
    if input != 0:
        fsm.transition_to("PlayerRunState")
        return
        
    # Transition to jump if requested
    if Input.is_action_just_pressed("jump"):
        fsm.transition_to("PlayerJumpState")

Scene Tree Setup

Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── AnimationPlayer
└── StateMachine (Node) - [initial_state points to PlayerIdleState]
    ├── PlayerIdleState (Node)
    ├── PlayerRunState (Node)
    ├── PlayerJumpState (Node)
    └── PlayerFallState (Node)

Event Bus (Signal Bus) Pattern

  • The Event Bus is a central communications hub that allows disconnected nodes to broadcast and listen to game-wide events (e.g., player scoring, UI popups, game state shifts) without referencing each other or navigating the scene tree.

How to Setup the Event Bus

    1. Create a script called EventBus.gd.
    1. Go to Project Settings → Autoload, select EventBus.gd, and add it as an Autoload named EventBus (making it a global singleton).

Event Bus Implementation

# res://scripts/EventBus.gd
extends Node
 
# Define all global events here
signal player_health_changed(current: int, max_health: int)
signal enemy_defeated(score_value: int)
signal item_collected(item_name: String, quantity: int)
signal game_over(reason: String)
signal show_notification(message: String)

Publisher (Triggering Events)

# Inside an Enemy script when it dies
func die() -> void:
    # Broadcast the event globally to any listeners
    EventBus.enemy_defeated.emit(100)
    EventBus.show_notification.emit("Enemy Slain! +100 PTS")
    queue_free()

Subscriber (Listening to Events)

# Inside a UI Score Label script
extends Label
 
var current_score: int = 0
 
func _ready() -> void:
    # Subscribe to the global Event Bus
    EventBus.enemy_defeated.connect(_on_enemy_defeated)
    
func _on_enemy_defeated(points: int) -> void:
    current_score += points
    text = "Score: " + str(current_score)

Performance & Cleaning Up

  • Safety: Since EventBus is a singleton, if a listening node is freed (queue_free()), Godot automatically disconnects its methods. However, if you hook up non-node Callables (like transient Resources or custom classes), you MUST disconnect them manually on deletion to prevent calling invalid memory.

UI Separation Pattern (Model-View-Controller / Presenter)

  • In complex game interfaces (like menus, inventories, or skill trees), mixing data logic (e.g. inventory contents, gold coins) with UI rendering logic (e.g. updating labels, scaling progress bars) creates unmaintainable script structures. Godot developers solve this with a modified MVP/MVC pattern.

Structure breakdown

  • Model (Data): A custom Resource containing game data. Has no visual logic. (e.g. CharacterStats.tres).
  • View (Presentation): Control nodes that handle layout, button triggers, and graphics. They have no business logic and do not perform calculations.
  • Controller / Presenter (Glue): A controller script that connects the Model to the View. It listens to data changes on the model and pushes them to the view.

Script Implementation Example

1. The Model (Stats Resource)

# res://resources/PlayerStats.gd
class_name PlayerStats
extends Resource
 
signal health_changed(new_hp: int)
signal gold_changed(new_gold: int)
 
@export var max_health: int = 100
var health: int = 100 :
    set(value):
        health = clamp(value, 0, max_health)
        health_changed.emit(health)
 
var gold: int = 0 :
    set(value):
        gold = max(0, value)
        gold_changed.emit(gold)

2. The View (UI Layout Controller)

# res://ui/PlayerHUDView.gd
class_name PlayerHUDView
extends CanvasLayer
 
@onready var health_bar = $Margin/VBox/HealthBar
@onready var gold_label = $Margin/VBox/GoldLabel
 
# Pure setter functions, no business calculations
func update_health_display(health: int, max_health: int) -> void:
    health_bar.max_value = max_health
    health_bar.value = health
 
func update_gold_display(amount: int) -> void:
    gold_label.text = "Gold: " + str(amount)

3. The Controller / Presenter (Scene Orchestrator)

# res://scenes/GameHUDController.gd
extends Node
 
@export var player_stats: PlayerStats
@onready var hud_view: PlayerHUDView = $PlayerHUDView
 
func _ready() -> void:
    if player_stats:
        # Connect Model signals to updater methods in View
        player_stats.health_changed.connect(
            func(new_hp): hud_view.update_health_display(new_hp, player_stats.max_health)
        )
        player_stats.gold_changed.connect(
            func(new_gold): hud_view.update_gold_display(new_gold)
        )
        
        # Initialize view starting state
        hud_view.update_health_display(player_stats.health, player_stats.max_health)
        hud_view.update_gold_display(player_stats.gold)

Object Pooling

# Reuse nodes instead of instantiate/queue_free every frame
var pool: Array[Node] = []
 
func get_from_pool() -> Node:
    for node in pool:
        if not node.visible:
            node.visible = true
            return node
    var new_node = bullet_scene.instantiate()
    add_child(new_node)
    pool.append(new_node)
    return new_node
 
func return_to_pool(node: Node) -> void:
    node.visible = false

@tool Scripts (Editor Scripts)

@tool
extends Node2D
 
@export var tile_count: int = 10 : set = _set_tile_count
 
func _set_tile_count(value: int) -> void:
    tile_count = value
    _rebuild()  # runs in editor when you change the value
 
func _rebuild() -> void:
    # Generate tiles procedurally in the editor
    for child in get_children():
        child.queue_free()
    for i in tile_count:
        var tile = preload("res://scenes/Tile.tscn").instantiate()
        tile.position.x = i * 64
        add_child(tile)

Coroutines with await

# await pauses execution until a signal fires or a time passes
 
func show_message(text: String) -> void:
    $Label.text = text
    $Label.visible = true
    await get_tree().create_timer(2.0).timeout
    $Label.visible = false
 
# await animation finish
$AnimationPlayer.play("attack")
await $AnimationPlayer.animation_finished
$AnimationPlayer.play("idle")
 
# await signal
await player_died
get_tree().reload_current_scene()
# 2D: NavigationRegion2D + NavigationAgent2D
extends CharacterBody2D
 
@onready var nav_agent = $NavigationAgent2D
 
func move_to(target_pos: Vector2) -> void:
    nav_agent.target_position = target_pos
 
func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return
    var next = nav_agent.get_next_path_position()
    var dir = (next - global_position).normalized()
    velocity = dir * speed
    move_and_slide()
 
# 3D: NavigationRegion3D + NavigationAgent3D (same pattern)

More Learn

  • Godot Docs - For Latest Docs and Features
  • Game Systems — Implement inventory, save/load, quests & procedural generation in Godot
  • Advanced Graphics — Vulkan-based low-level rendering via Godot’s RenderingDevice