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.
New to the language? Start with the GDScript tutorial for beginners for the "why" behind each form, read the signals guide when events come up, then write it for real in the free Code From Zero quest. Keep this sheet open in another tab. For the full learning order, follow the project-based Godot roadmap; to browse guides by topic, see the Godot tutorials library.
On this page
- Variables and types
- Functions
- Control flow
- Collections
- Strings
- Node lifecycle
- Input
- Signals
- Node references
- Scenes and instantiation
- Physics and movement
- Timers
- Tweens
- Resources
- File I/O
- Math helpers
- Autoloads (singletons)
- Coroutines (await)
- Class inheritance
- Common patterns
- Common Godot 4 syntax mistakes
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 = falseCommon Godot 4 Syntax Mistakes
The syntax errors that bite Godot 4 beginners most often, and the fix for each:
# Integer division truncates. 5 / 2 is 2, not 2.5.var half := 5 / 2 # 2 (both operands are ints)var half := 5.0 / 2 # 2.5 (make one a float)# Frame-rate-dependent movement: forgetting delta.position.x += 100 # moves faster on a fast PCposition.x += 100 * delta # same speed everywhere# Comparing floats with ==. Rounding makes this fail.if speed == 10.0: # fragileif is_equal_approx(speed, 10.0): # correct# Typed arrays need the element type on BOTH sides.var enemies: Array[Node] = [] # okvar enemies := [] # untyped; loses autocomplete + checks# Type check: use `is`, not `==`, on classes.if node is CharacterBody2D: # correctA few more that are not syntax errors but behave surprisingly: connecting the same signal twice (guard with is_connected()), reading a $Node that is not in the tree yet (use @onready), and get_node("Wrong/Path") crashing instead of returning null (use get_node_or_null()). The five most common GDScript mistakes covers the habits worth forming early.
That'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. If you are still learning the language, the GDScript tutorial for beginners walks through these concepts in order, the signals guide goes deep on Godot's event system, and the free Code From Zero quest puts most of this sheet to work in your browser.


