Comprehensive GDScript and Godot 4.x development knowledge covering node architecture, scene structure, signals, physics, input handling, resources, and best practices...
Comprehensive guide for Godot 4.x game development using GDScript.
Everything in Godot is a Node. Scenes are trees of nodes. Understand the hierarchy:
Node - Base class, provides basic tree structureNode2D - Adds 2D transform (position, rotation, scale)Node3D - Adds 3D transform# Inferred type
var health = 100
# Explicit type
var health: int = 100
# Export to Inspector (Godot 4.x syntax)
@export var speed: float = 300.0
@export_range(0, 100) var health: int = 100
@export_category("Combat")
@export var damage: int = 10
# Constants
const MAX_SPEED = 500.0
const GRAVITY = 980.0
Understanding the order matters for exports and setters:
@export var my_value: String = "initial":
set(value):
my_value = value + "!"
func _init():
# Runs AFTER initial value, triggers setter
my_value = "from_init" # Result: "from_init!"
# When node is in a scene file with Inspector value set to "inspector":
# Final result: "inspector!" (Inspector overrides _init)
Sequence:
= "initial") - setter NOT called_init() assignments - setter IS calledBest Practice: For exports, use null or invalid defaults, let Inspector set real values.
# @onready - Runs after _ready(), node tree is guaranteed to exist
@onready var sprite = $AnimatedSprite2D
@onready var timer = $Timer
@onready var collision = $CollisionShape2D
# Manual reference
var sprite = null
func _ready():
sprite = get_node("AnimatedSprite2D")
# or
sprite = $AnimatedSprite2D
func _init():
# Constructor - called when object is created
# Runs BEFORE _ready(), before node is in tree
# Use for initial property setup
pass
func _ready():
# Called once when node enters scene tree
# All child nodes have also called _ready() (bottom-up)
# Initialize variables, connect signals, setup state
pass
func _enter_tree():
# Called when node enters tree (before _ready)
# Called again if removed and re-added
# Use for setup that needs parent access
pass
func _exit_tree():
# Called when node exits tree
# Use for cleanup
pass
func _process(delta: float):
# Called every frame (frame-dependent)
# Use for non-physics updates, animations, timers, UI
# Check inputs here if you need frame-perfect response
pass
func _physics_process(delta: float):
# Called at fixed intervals (default 60 FPS, frame-independent)
# Use for physics, movement, collision detection
# Prefer this over _process for movement
pass
func _input(event: InputEvent):
# Receives ALL input events
# Use for specific key/mouse detection
if event.is_action_pressed("jump"):
jump()
func _unhandled_input(event: InputEvent):
# Only receives events not handled by UI or other nodes
pass
func _notification(what: int):
# Universal callback for engine notifications
# Handles NOTIFICATION_* constants
match what:
NOTIFICATION_PARENTED:
print("Node got a parent")
NOTIFICATION_UNPARENTED:
print("Node lost parent")
Use _process(delta):
Use _physics_process(delta):
Use *_input(event):
get_process_delta_time() if needed# Check action state (defined in Project Settings > Input Map)
if Input.is_action_pressed("move_right"): # Held down
velocity.x += SPEED
if Input.is_action_just_pressed("jump"): # Pressed this frame
jump()
if Input.is_action_just_released("shoot"): # Released this frame
stop_shooting()
# Get axis input (-1 to 1)
var direction = Input.get_axis("move_left", "move_right")
velocity.x = direction * SPEED
# Get vector input
var input_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
extends CharacterBody2D
const SPEED = 300.0
const GRAVITY = 980.0
const JUMP_FORCE = -400.0
func _physics_process(delta):
# Apply gravity
velocity.y += GRAVITY * delta
# Horizontal movement
var direction = Input.get_axis("ui_left", "ui_right")
velocity.x = direction * SPEED
# Jump
if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
velocity.y = JUMP_FORCE
# MUST call this to apply movement and handle collisions
move_and_slide()
extends Area2D
signal player_entered
signal player_exited
func _ready():
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D):
if body.is_in_group("Player"):
player_entered.emit()
print("Player entered area")
func _on_body_exited(body: Node2D):
if body.is_in_group("Player"):
player_exited.emit()
@onready var sprite = $AnimatedSprite2D
func _ready():
sprite.play("idle")
func update_animation(direction: float):
if direction != 0:
sprite.play("run")
sprite.flip_h = direction < 0 # Flip when moving left
else:
sprite.play("idle")
@onready var timer = $Timer
func _ready():
timer.wait_time = 2.0
timer.one_shot = true # Only fires once
timer.timeout.connect(_on_timer_timeout)
timer.start()
func _on_timer_timeout():
print("Timer finished!")
@onready var camera = $Camera2D
func _ready():
camera.enabled = true
camera.make_current() # Set as active camera
# Smooth following
camera.position_smoothing_enabled = true
camera.position_smoothing_speed = 5.0
# Define custom signal
signal health_changed(new_health)
signal player_died
# Emit signal
health_changed.emit(50)
player_died.emit()
# Godot 4.x syntax - connect in code
func _ready():
$Button.pressed.connect(_on_button_pressed)
$Timer.timeout.connect(_on_timer_timeout)
func _on_button_pressed():
print("Button clicked!")
# Add node to group (in editor or code)
add_to_group("enemies")
add_to_group("Player")
# Check if in group
if body.is_in_group("Player"):
take_damage()
# Get all nodes in group
var enemies = get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
enemy.take_damage(10)
# Get first node in group
var player = get_tree().get_first_node_in_group("Player")
# Change scene by path
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
# Change scene by PackedScene
var next_scene = preload("res://scenes/level_2.tscn")
get_tree().change_scene_to_packed(next_scene)
# Manual scene change with transition
var new_scene = load("res://scenes/level_2.tscn")
get_tree().root.add_child(new_scene.instantiate())
queue_free() # Remove current scene
# Preload (at compile time)
const BULLET = preload("res://scenes/bullet.tscn")
func shoot():
var bullet = BULLET.instantiate()
bullet.position = $Muzzle.global_position
get_tree().root.add_child(bullet)
# Load (at runtime)
var enemy_scene = load("res://scenes/enemy.tscn")
var enemy = enemy_scene.instantiate()
add_child(enemy)
# PRELOAD - Compile-time loading (FASTER)
# - Loads when script loads
# - Editor can autocomplete paths
# - Use for constants and frequently used resources
const BulletScene = preload("res://scenes/bullet.tscn")
const PlayerTexture = preload("res://sprites/player.png")
# LOAD - Runtime loading (FLEXIBLE)
# - Loads when line executes
# - Can use dynamic paths
# - Use for conditional/optional resources
var scene_path = "res://scenes/level_" + str(level_num) + ".tscn"
var level = load(scene_path)
# For exports, avoid preloading - let Inspector override
@export var custom_scene: PackedScene # Don't preload here
Use preload():
Use load():
# Create tween
var tween = create_tween()
# Animate property
tween.tween_property($Sprite, "position", Vector2(100, 100), 1.0)
tween.tween_property($Sprite, "modulate:a", 0.0, 0.5) # Fade out
# Chaining
tween.tween_property($Sprite, "scale", Vector2(2, 2), 0.5)
tween.tween_property($Sprite, "scale", Vector2(1, 1), 0.5)
# Easing
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_CUBIC)
# Wait for completion
await tween.finished
print("Tween completed!")
# Wait for signal
await $Timer.timeout
print("Timer finished")
# Wait one frame
await get_tree().process_frame
# Wait for animation
$AnimatedSprite.play("attack")
await $AnimatedSprite.animation_finished
print("Attack animation done")
# Useful in sequence
func do_sequence():
print("Starting")
await get_tree().create_timer(1.0).timeout
print("After 1 second")
await get_tree().create_timer(2.0).timeout
print("After 3 seconds total")
Configure in Project Settings → Autoload. Use sparingly - prefer scene-based architecture.
# Global.gd (autoload singleton)
extends Node
var player_health = 100
var current_level = 1
func save_game():
# Save logic
pass
# Access from any script
Global.player_health -= 10
Global.save_game()
Good uses:
Avoid autoloads for:
Alternative: Use static functions/variables in script classes:
# utils.gd
extends Node
class_name Utils
static var shared_data = {}
static func calculate_damage(base: int, modifier: float) -> int:
return int(base * modifier)
# Use anywhere without autoload
Utils.calculate_damage(10, 1.5)
get_node() in loops - Cache references in @onready or _ready()_physics_process() for physics - Don't do physics in _process()queue_free() instead of manual removalqueue_redraw() - Only redraw when state changesadd_child()MyScript.new() for game objectsArray: Fast iteration, slow insert/remove (except at end)
Dictionary: Fast insert/remove/get by key, slow find by value
Object/Resource: Provides structure but slower than both due to property lookup chain
Rule: Use simplest structure that meets needs. Array for lists, Dictionary for lookups, Object for complex data with behavior.
_ready() - Check that required nodes existis_instance_valid() - Before accessing nodes that might be freedhas_method() before calling on unknown types@export instead of export@onready instead of onready.connect() requires explicit method reference: signal.connect(method)move_and_slide() takes no parameters (velocity is property).emit() instead of emit_signal()move_and_slide() - CharacterBody2D won't move without itis_on_floor() - Before allowing jumps_physics_process(), not _process()_ready() - Use @onready or access in/after _ready()delta - Multiply movement/timers by delta for frame-rate independenceFor deep dives into specific topics: