GDScript is Godot's built-in language, and it has exactly one job: making games in Godot. If you're coming from Python it'll feel familiar within an hour. Coming from C# or JavaScript, the first thing you'll notice is how much boilerplate just isn't there.
This is a reference, not a tutorial. Everything below is Godot 4.x syntax. Bookmark it and Ctrl+F your way around when something slips your mind.
Variables and Types
# Basic typesvar health: int = 100var speed: float = 5.5var name: String = "Player"var alive: bool = truevar direction: Vector2 = Vector2(1, 0)var position: Vector3 = Vector3.ZERO# Type inferencevar score := 0 # intvar damage := 10.5 # floatvar label := "Hello" # String# Constantsconst MAX_SPEED: float = 10.0const GRAVITY: float = 9.8# Enumsenum State { IDLE, RUN, JUMP, ATTACK }var current_state: State = State.IDLE# Exports (editable in Inspector)@export var max_health: int = 100@export var walk_speed: float = 3.0@export var player_name: String = "Hero"@export var item_scene: PackedScene@export_range(0, 100, 1) var volume: int = 80@export_enum("Sword", "Axe", "Bow") var weapon: String@export_group("Movement")@export var run_speed: float = 6.0@export var jump_force: float = 8.0Functions
# Basic functionfunc take_damage(amount: int) -> void: health -= amount# Return valuefunc get_speed() -> float: return speed * speed_multiplier# Default parametersfunc heal(amount: int = 10) -> void: health = mini(health + amount, max_health)# Optional return typefunc is_alive() -> bool: return health > 0# Static function (callable without instance)static func calculate_damage(attack: int, defense: int) -> int: return maxi(attack - defense, 1)# Lambda / anonymous functionvar double := func(x: int) -> int: return x * 2Control Flow
# If / elif / elseif health <= 0: die()elif health < 20: show_warning()else: regenerate()# Match (like switch)match current_state: State.IDLE: play_idle() State.RUN: play_run() State.JUMP, State.ATTACK: # Multiple values play_action() _: # Default pass# Ternaryvar label := "Dead" if health <= 0 else "Alive"# For loopsfor i in range(10): # 0 to 9 print(i)for item in inventory: # Iterate array print(item.name)for key in stats: # Iterate dictionary keys print(key, stats[key])# While loopwhile not is_on_floor(): velocity.y -= gravity * deltaCollections
# Arraysvar items: Array[String] = ["Sword", "Shield", "Potion"]items.append("Bow")items.remove_at(0)items.has("Shield") # trueitems.size() # 3items.find("Potion") # index or -1items.sort()items.shuffle()items.filter(func(i): return i != "Bow")items.map(func(i): return i.to_upper())# Typed arraysvar enemies: Array[Enemy] = []var numbers: Array[int] = [1, 2, 3]# Dictionaryvar stats: Dictionary = { "health": 100, "strength": 15, "defense": 10,}stats["health"] # 100stats.get("mana", 0) # 0 (default)stats.has("strength") # truestats.keys() # Array of keysstats.values() # Array of valuesstats.erase("defense")stats.merge({"speed": 5})Strings
var name := "Adventurer"# String operationsname.length() # 10name.to_upper() # "ADVENTURER"name.to_lower() # "adventurer"name.begins_with("Adv") # truename.contains("vent") # truename.replace("er", "or") # "Adventuror"name.split("e") # ["Adv", "ntur", "r"]# String formattingvar msg := "HP: %d / %d" % [health, max_health]var pos := "Position: %.2f, %.2f" % [x, y]# String interpolation (Godot 4.x - no built-in f-strings, use % or +)var greeting := "Hello, " + name + "!"Node Lifecycle
extends CharacterBody3D# Called when node enters the scene treefunc _ready() -> void: pass# Called every frame (variable delta)func _process(delta: float) -> void: pass# Called at fixed rate (default 60/sec) - use for physicsfunc _physics_process(delta: float) -> void: pass# Called for unhandled input eventsfunc _unhandled_input(event: InputEvent) -> void: pass# Called for all input eventsfunc _input(event: InputEvent) -> void: pass# Called when node exits the scene treefunc _exit_tree() -> void: passInput
# Polling (check state right now)if Input.is_action_pressed("move_forward"): # Held down move()if Input.is_action_just_pressed("jump"): # Just pressed this frame jump()if Input.is_action_just_released("attack"): # Just released this frame stop_charging()# Movement vector (combines 4 directional inputs into Vector2)var input := Input.get_vector("left", "right", "forward", "back")# MouseInput.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)# Event-based (in _unhandled_input or _input)func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion: rotate_y(-event.relative.x * sensitivity) if event is InputEventKey: if event.pressed and event.keycode == KEY_ESCAPE: get_tree().quit()Signals
# Define a signalsignal health_changed(new_health: int)signal died# Emit a signalfunc take_damage(amount: int) -> void: health -= amount health_changed.emit(health) if health <= 0: died.emit()# Connect a signal (in code)func _ready() -> void: $Enemy.died.connect(_on_enemy_died)func _on_enemy_died() -> void: score += 100# Connect with lambda$Button.pressed.connect(func(): print("clicked"))# One-shot connection (auto-disconnects after firing once)$Timer.timeout.connect(_on_timeout, CONNECT_ONE_SHOT)# Disconnect$Enemy.died.disconnect(_on_enemy_died)# Check if connected$Enemy.died.is_connected(_on_enemy_died)Node References
# Get child node (same scene)@onready var sprite: Sprite2D = $Sprite2D@onready var health_bar: ProgressBar = $UI/HealthBar@onready var anim: AnimationPlayer = $AnimationPlayer# Export (set in Inspector - survives restructuring)@export var target: Node3D@export var weapon_scene: PackedScene# Get node by pathvar player := get_node("/root/Main/Player")# Get parentvar parent := get_parent()# Get childrenfor child in get_children(): if child is Enemy: child.take_damage(10)# Groupsadd_to_group("enemies")if is_in_group("enemies"): passvar all_enemies := get_tree().get_nodes_in_group("enemies")# Find nodesvar camera := get_viewport().get_camera_3d()Scenes and Instantiation
# Load a scenevar enemy_scene: PackedScene = preload("res://scenes/enemy.tscn")# Instantiatevar enemy := enemy_scene.instantiate()add_child(enemy)enemy.global_position = Vector3(10, 0, 5)# Remove from sceneenemy.queue_free()# Change sceneget_tree().change_scene_to_file("res://scenes/game_over.tscn")get_tree().change_scene_to_packed(preload("res://scenes/menu.tscn"))# Reload current sceneget_tree().reload_current_scene()Physics and Movement (CharacterBody3D)
extends CharacterBody3D@export var speed: float = 5.0@export var jump_force: float = 8.0@export var gravity: float = 20.0func _physics_process(delta: float) -> void: # Gravity if not is_on_floor(): velocity.y -= gravity * delta # Jump if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = jump_force # Movement var input := Input.get_vector("left", "right", "forward", "back") velocity.x = input.x * speed velocity.z = input.y * speed move_and_slide()# Useful checks after move_and_slide()is_on_floor()is_on_wall()is_on_ceiling()get_last_slide_collision()Timers
# Create timer in codevar timer := Timer.new()timer.wait_time = 2.0timer.one_shot = truetimer.timeout.connect(_on_timer_done)add_child(timer)timer.start()# Quick one-shot timerawait get_tree().create_timer(1.5).timeoutprint("1.5 seconds later")# Scene tree timer (no node needed)get_tree().create_timer(0.5).timeout.connect(func(): print("done"))Tweens
# Create tweenvar tween := create_tween()# Move to position over 0.5 secondstween.tween_property(self, "position", Vector3(10, 0, 0), 0.5)# Chain animationstween.tween_property(self, "modulate:a", 0.0, 0.3) # Fade outtween.tween_callback(queue_free) # Then remove# Parallel tweenstween.set_parallel(true)tween.tween_property(self, "position:y", 5.0, 0.3)tween.tween_property(self, "rotation:y", PI, 0.3)# Easingtween.tween_property(self, "position", target, 0.5)\ .set_ease(Tween.EASE_OUT)\ .set_trans(Tween.TRANS_CUBIC)# Looptween.set_loops(3) # Repeat 3 timestween.set_loops(0) # Loop foreverResources
# Define a custom Resourceclass_name ItemDataextends Resource@export var id: String@export var name: String@export var icon: Texture2D@export var value: int# Load a resourcevar sword: ItemData = load("res://data/items/sword.tres")var sword2: ItemData = preload("res://data/items/sword.tres") # Compile-time# Create at runtimevar item := ItemData.new()item.name = "Potion"item.value = 50# Save a resourceResourceSaver.save(item, "user://custom_item.tres")File I/O
# Write JSONfunc save_data(data: Dictionary) -> void: var file := FileAccess.open("user://save.json", FileAccess.WRITE) file.store_string(JSON.stringify(data, "\t"))# Read JSONfunc load_data() -> Dictionary: if not FileAccess.file_exists("user://save.json"): return {} var file := FileAccess.open("user://save.json", FileAccess.READ) var json := JSON.new() if json.parse(file.get_as_text()) == OK: return json.data return {}# ConfigFile (INI-style)var config := ConfigFile.new()config.set_value("audio", "volume", 0.8)config.save("user://settings.cfg")config.load("user://settings.cfg")var vol: float = config.get_value("audio", "volume", 1.0)# Check pathsFileAccess.file_exists("user://save.json")DirAccess.dir_exists_absolute("user://saves/")Math Helpers
# Common mathabs(-5) # 5sign(-3.2) # -1.0clamp(value, 0, 100) # Constrain to rangeclampf(0.5, 0.0, 1.0) # Float versionclampi(50, 0, 100) # Int versionmini(a, b) # Smaller intmaxi(a, b) # Larger intminf(a, b) # Smaller floatmaxf(a, b) # Larger float# Interpolationlerp(0.0, 100.0, 0.5) # 50.0lerpf(a, b, t) # Float lerplerp_angle(a, b, t) # Angle lerp (handles wrapping)# Randomrandf() # 0.0 to 1.0randi() # Random intrandf_range(1.0, 10.0) # Float in rangerandi_range(1, 6) # Int in range (inclusive)# Vectorsvar v := Vector3(1, 2, 3)v.normalized() # Unit vectorv.length() # Magnitudev.distance_to(other) # Distance between two vectorsv.direction_to(other) # Normalized directionv.lerp(other, 0.5) # Halfway betweenv.dot(other) # Dot productv.cross(other) # Cross product (3D)# Anglesdeg_to_rad(90.0) # 1.5708rad_to_deg(PI) # 180.0atan2(y, x) # Angle from componentsAutoloads (Singletons)
# Set up: Project → Project Settings → Autoload → Add script# GameManager.gd (autoloaded as "GameManager")extends Nodevar score: int = 0var current_level: int = 1signal score_changed(new_score: int)func add_score(amount: int) -> void: score += amount score_changed.emit(score)# Use from anywhere:GameManager.add_score(100)GameManager.score_changed.connect(_on_score_changed)Coroutines (Await)
# Wait for a signalawait $AnimationPlayer.animation_finished# Wait for a timerawait get_tree().create_timer(2.0).timeout# Wait for a tweenvar tween := create_tween()tween.tween_property(self, "position:y", 10.0, 1.0)await tween.finished# Async functionfunc fade_out() -> void: var tween := create_tween() tween.tween_property(self, "modulate:a", 0.0, 0.5) await tween.finished queue_free()Class Inheritance
# Base class# weapon.gdclass_name Weaponextends Node3D@export var damage: int = 10@export var attack_speed: float = 1.0func attack() -> void: print("Base attack")# Derived class# sword.gdclass_name Swordextends Weaponfunc _ready() -> void: damage = 25func attack() -> void: super.attack() # Call parent method print("Sword slash!")# Type checkingif weapon is Sword: weapon.special_ability()Common Patterns
# Null-safe accessvar health_bar := get_node_or_null("UI/HealthBar")if health_bar: health_bar.value = health# Deferred calls (safe for physics/tree changes)call_deferred("add_child", new_node)set_deferred("monitoring", true)queue_free() # Already deferred# Process mode (pause behavior)process_mode = Node.PROCESS_MODE_ALWAYS # Runs during pauseprocess_mode = Node.PROCESS_MODE_PAUSABLE # Stops during pause (default)process_mode = Node.PROCESS_MODE_DISABLED # Never runs# Pause the gameget_tree().paused = trueget_tree().paused = falseThat's the GDScript I reach for daily. The official Godot docs cover the rest of the API when you need it.
Syntax only sticks when you build with it, though. The inventory system quest is free and puts most of this sheet to work, and these five beginner mistakes are worth dodging while your habits are still forming.


