GDScript Cheat Sheet: Everything You Need in One Place
GDScript is Godot's built-in scripting language. It's designed specifically for game development - fast to write, easy to read, and tightly integrated with the engine. If you're coming from Python, it'll feel familiar. If you're coming from C# or JavaScript, you'll appreciate how little boilerplate it needs.
This is a reference, not a tutorial. Bookmark it and come back when you need to remember how something works.
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 = falseThis covers the most-used parts of GDScript. For the full API reference, the official Godot docs are comprehensive.
If you want to learn these patterns by building real game systems - not just reading syntax - check out the quest board. Every course teaches GDScript through hands-on projects: inventory systems, combat, AI, save/load, and more.