Appendix A: GDScript Quick Reference
This is a reference, not a tutorial. If you are learning GDScript from scratch, start with the official Godot docs and the Kidscancode YouTube channel. If you have written GDScript before, or you know Python and you are trying to orient yourself inside Godot 4, this appendix is for you. Flip to it when you forget how move_and_slide handles collision, or what the difference between _process and _physics_process is, or whether @onready runs before or after _ready.
Everything below is Godot 4.x idiomatic. If you are stuck on Godot 3, a lot still applies, but signals connect differently, yield is gone (replaced by await), and tool became @tool. Upgrade if you can.
The code snippets are minimal on purpose. Real examples with full context live in the chapters. Pointers to those are at the end.
Variables and Types
GDScript is dynamically typed by default but supports optional static typing. Use it. The editor will thank you, the compiler will catch errors, and your future self will read faster.
# Dynamic — works, but loses editor help
var score = 0
var name = "Rosa"
# Type-inferred (preferred)
var score := 0
var name := "Rosa"
# Explicit type (use when inference is ambiguous)
var score: int = 0
var speed: float = 200.0
var alive: bool = true
# Constants — cannot be reassigned
const MAX_HEALTH := 100
const GRAVITY := Vector2(0, 980)
# Type with no initial value
var target: Node2D
The := walrus operator does type inference. It is shorter than writing : int = 0 and gives you the same static guarantees. Use it as the default. Use explicit types only when inference guesses wrong or when you want the type annotation as documentation.
Static typing benefits:
- Editor autocomplete actually works
- Compile-time errors instead of runtime crashes
- ~2x speed improvement on typed code in Godot 4
- Code is self-documenting
The cost is a few extra characters. It is always worth it in a project larger than a prototype.
Basic Types
GDScript's primitive and built-in types cover 95% of what game code needs.
var i: int = 42
var f: float = 3.14
var b: bool = true
var s: String = "hello"
var a: Array = [1, 2, 3]
var typed_a: Array[int] = [1, 2, 3] # typed arrays — prefer these
var d: Dictionary = {"hp": 100, "name": "Goblin"}
var pos: Vector2 = Vector2(10, 20)
var pos3d: Vector3 = Vector3(1, 2, 3)
var color: Color = Color(1, 0, 0, 1) # RGBA, 0.0–1.0
var color_hex: Color = Color("#ff0000") # also valid
var rect: Rect2 = Rect2(Vector2.ZERO, Vector2(100, 100))
Typed arrays (Array[int], Array[Node]) are new in Godot 4 and you should prefer them. They are faster, catch errors at assignment time, and make editor tooling smarter.
Packed arrays (PackedByteArray, PackedFloat32Array, PackedVector2Array, PackedStringArray) are tightly packed native types. Use them when you have large homogeneous data — pixel buffers, vertex arrays, tile maps. They are dramatically faster than a generic Array for numeric work.
Control Flow
# if / elif / else
if health <= 0:
die()
elif health < 20:
flash_warning()
else:
clear_warning()
# match — pattern matching, more powerful than switch
match state:
"idle":
play_idle_animation()
"attacking":
resolve_attack()
_: # default case (underscore)
push_error("Unknown state: %s" % state)
# match with types / arrays
match value:
0:
print("zero")
1, 2, 3:
print("small")
[_, _]:
print("any 2-element array")
{"type": "weapon", ..}:
print("dictionary with type=weapon")
Match is more flexible than it looks. It can destructure arrays, check dictionary keys, and combine literal with wildcard. Read the Godot docs on match — it replaces a lot of if-chains.
# for loops over ranges
for i in range(10): # 0..9
print(i)
for i in range(5, 10): # 5..9
print(i)
for i in range(0, 10, 2): # 0,2,4,6,8
print(i)
# for over collections
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.queue_free()
for key in inventory:
print(key, " -> ", inventory[key])
# while
while ammo > 0 and has_target():
fire()
# break and continue work as expected
for item in items:
if item.broken:
continue
if item.cursed:
break
use(item)
Functions
# Basic function
func greet(name: String) -> void:
print("Hello, ", name)
# Return value
func add(a: int, b: int) -> int:
return a + b
# Default arguments
func spawn(position: Vector2, health: int = 100) -> void:
var enemy = ENEMY_SCENE.instantiate()
enemy.position = position
enemy.health = health
add_child(enemy)
# Static function — no instance required
class_name MathUtils
static func clamp_positive(x: float) -> float:
return max(0.0, x)
# Usage: MathUtils.clamp_positive(-3.0)
Always annotate parameter types and return types for anything beyond a throwaway prototype. The -> void is optional but explicit is better than implicit.
Inner scope: variables declared inside a function do not leak outside it. for i in range(10): — i is only in scope for the loop body. This is more forgiving than it used to be; in old GDScript, the loop counter leaked.
Variadic functions (...args) exist in GDScript but are rarely what you want. Usually a typed array is clearer.
Classes and Objects
GDScript is object-oriented. Scripts attach to nodes and extend their capabilities. Most scripts look like this:
class_name Player
extends CharacterBody2D
@export var speed: float = 200.0
@export var jump_power: float = 400.0
var health: int = 100
func _ready() -> void:
print("Player ready at position ", global_position)
func _process(delta: float) -> void:
# Called every frame. Use for animation, input reading, UI.
pass
func _physics_process(delta: float) -> void:
# Called at fixed physics rate (default 60Hz).
# Use for movement, collision, anything physics-related.
var direction := Input.get_vector("left", "right", "up", "down")
velocity = direction * speed
move_and_slide()
func _input(event: InputEvent) -> void:
# Fires once per input event — mouse, key, gamepad.
if event.is_action_pressed("pause"):
get_tree().paused = true
func _unhandled_input(event: InputEvent) -> void:
# Fires only if the event was not consumed by UI.
# Prefer this over _input for gameplay actions.
pass
Lifecycle order (from script load to first frame):
_init()— object created in memory. No scene tree access yet.@onreadyvariables resolve. Nodes referenced with$Pathbecome available._ready()— node has entered the scene tree. Safe to reach siblings, children, autoloads.- First frame:
_process(delta)and_physics_process(delta)start firing.
The @onready annotation defers a variable assignment until the node is in the tree. Without it, $Sprite fails because children are not yet ready when member initializers run.
# Good
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim: AnimationPlayer = $AnimationPlayer
# Bad — sprite will be null in _init, assignment crashes
var sprite: Sprite2D = $Sprite2D
class_name registers the class globally. Other scripts can then reference it by name (var p: Player = ...) without needing preload.
Signals
Signals are Godot's event system. A node emits a signal; other nodes connect listeners. Decoupled, observable, standard.
# Declare
signal health_changed(new_health: int)
signal died
# Emit
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health)
if health <= 0:
died.emit()
# Connect (in another script)
func _ready() -> void:
player.health_changed.connect(_on_player_health_changed)
player.died.connect(_on_player_died)
func _on_player_health_changed(new_health: int) -> void:
health_bar.value = new_health
func _on_player_died() -> void:
show_game_over_screen()
# Inline lambda connection — convenient for one-liners
player.health_changed.connect(func(h): health_bar.value = h)
Signal parameters are strongly typed if you declare them. Connection can pass extra bound arguments via .bind(extra_value) — useful when the same handler services multiple senders.
Disconnect with .disconnect(). Check with .is_connected(). Connections persist until disconnected or until the sender is freed. Leaked connections to freed nodes produce warnings in the output panel; pay attention.
Nodes and Scene Tree
Everything in a Godot scene is a node. Scripts attach to nodes. You reach sibling nodes by path.
# Dollar-sign shorthand (preferred when path is literal)
var sprite := $Sprite2D
var hud := $"../UI/HUD" # quoted when path has slashes
var label := $Sprite2D/Label # child-of-child
# get_node() — dynamic lookup
var node := get_node("Sprite2D")
var node_or_null := get_node_or_null("Sprite2D") # returns null instead of erroring
# Add and remove children at runtime
var enemy := ENEMY_SCENE.instantiate()
add_child(enemy)
enemy.global_position = spawn_point.global_position
# Remove a node from the tree and delete it
enemy.queue_free() # safe — deferred to end of frame
# enemy.free() # immediate — rarely what you want
# Groups — tag-based lookup across the scene
add_to_group("enemies")
for e in get_tree().get_nodes_in_group("enemies"):
e.queue_free()
Autoloads (Singletons). Globally accessible nodes, loaded before any scene. Register them in Project Settings → Autoload. Access by name anywhere:
# Assume GameManager.gd is registered as an autoload
GameManager.add_coins(5)
GameManager.current_level = 3
Use autoloads sparingly. They are global state. Good candidates: GameManager, AudioManager, SignalBus, SaveSystem. Bad candidates: anything specific to one scene.
Vectors and Math
Vector2 and Vector3 are your workhorses. Learn their methods.
var a := Vector2(3, 4)
var b := Vector2(1, 0)
a.length() # 5.0
a.length_squared() # 25.0 — prefer for comparisons, no sqrt
a.normalized() # Vector2(0.6, 0.8) — unit vector
a.distance_to(b) # 4.47...
a.direction_to(b) # unit vector from a toward b
a.dot(b) # 3.0 — scalar projection
a.cross(b) # -4.0 — signed area / z-component of 3D cross
a.angle() # angle from +X axis, radians
a.angle_to(b) # signed angle between vectors
a.rotated(PI / 2) # new vector rotated 90°
a.lerp(b, 0.5) # linear interpolation, t=0..1
# Scalar math
lerp(0.0, 100.0, 0.5) # 50.0
clamp(value, 0, 100)
smoothstep(0.0, 1.0, t) # smooth S-curve interpolation
move_toward(current, target, max_delta) # step toward target without overshoot
# Angles
deg_to_rad(180) # PI
rad_to_deg(PI) # 180.0
# Comparisons with tolerance
is_equal_approx(0.1 + 0.2, 0.3) # true
is_zero_approx(0.0000001) # true
move_toward is criminally underused. It is the cleanest way to ease a value toward a target without overshoot — perfect for decelerating player velocity, rotating turrets, fading health bars.
velocity.x = move_toward(velocity.x, 0, friction * delta)
Arrays and Dictionaries
var inv: Array[String] = ["sword", "shield", "potion"]
inv.append("torch") # or inv.push_back("torch")
inv.push_front("key")
inv.pop_back() # removes & returns last
inv.pop_front() # removes & returns first
inv.size() # length
inv.is_empty()
inv.has("sword") # membership
inv.find("shield") # index or -1
inv.erase("shield") # removes first occurrence
inv.clear()
# Functional style
var nums := [1, 2, 3, 4, 5]
var doubled := nums.map(func(n): return n * 2) # [2,4,6,8,10]
var evens := nums.filter(func(n): return n % 2 == 0) # [2,4]
var total := nums.reduce(func(accum, n): return accum + n, 0) # 15
# Sort
nums.sort() # ascending
nums.sort_custom(func(a, b): return a > b) # descending
# Iteration
for weapon in inv:
print(weapon)
Dictionaries:
var stats := {"hp": 100, "attack": 12, "defense": 8}
stats["hp"] = 80 # update
stats["crit"] = 0.05 # add
stats.has("defense") # membership
stats.erase("defense")
stats.keys() # array of keys
stats.values() # array of values
stats.size()
for key in stats:
print(key, " = ", stats[key])
# Nested data — great for configs
var enemies := {
"goblin": {"hp": 20, "atk": 5, "xp": 10},
"orc": {"hp": 50, "atk": 12, "xp": 30},
"dragon": {"hp": 500, "atk": 40, "xp": 500},
}
var orc_hp := enemies["orc"]["hp"]
Strings
var name := "Rosa"
var hp := 80
# Formatting with % — positional
var s1 := "Player %s has %d HP" % [name, hp]
# Format with named placeholders
var s2 := "Player {n} has {h} HP".format({"n": name, "h": hp})
# Concatenation (works but %s is cleaner)
var s3 := "Player " + name + " has " + str(hp) + " HP"
# Interpolation via str()
var s4 := str("Player ", name, " has ", hp, " HP")
# Common methods
name.length() # 4
name.to_lower()
name.to_upper()
name.begins_with("Ro") # true
name.ends_with("sa")
name.contains("os")
"a,b,c".split(",") # ["a","b","c"]
",".join(["a","b","c"]) # "a,b,c"
" hello ".strip_edges() # "hello"
# Regex — explicit RegEx object
var re := RegEx.new()
re.compile("\\d+")
var result := re.search("hp: 100")
if result:
print(result.get_string()) # "100"
PackedStringArray is what split() returns. It is an Array-like but of native strings. For most purposes it is interchangeable with Array[String], but be aware when passing between APIs.
Resources
Resources are Godot's serializable data objects. Textures, meshes, audio streams, fonts — all resources. You can also create your own.
# Loading resources
const PLAYER_SCENE := preload("res://scenes/Player.tscn") # compile-time, cached
var sword := load("res://items/sword.tres") as Weapon # runtime, re-fetches
# preload vs load:
# - preload: bundled at script load, constant, fastest. Use for frequently-accessed assets.
# - load: runtime, returns cached handle after first call. Use for dynamic paths.
Custom resources are just GDScript classes extending Resource:
# res://items/Weapon.gd
class_name Weapon
extends Resource
@export var name: String = "Sword"
@export var damage: int = 10
@export var icon: Texture2D
@export var sound: AudioStream
With this file saved, the editor's "Create New Resource" menu can now produce .tres (text resource) files of type Weapon. You edit them in the Inspector like any built-in resource. This is the single best pattern for data-driven design in Godot 4 — define your schema as a Resource class and author data as .tres files.
# Using a weapon resource
@export var starting_weapon: Weapon
func equip(weapon: Weapon) -> void:
damage = weapon.damage
$WeaponIcon.texture = weapon.icon
Scenes
A scene (.tscn) is a tree of nodes saved to disk. Scenes are instantiated, not inherited. Composition over inheritance is the Godot way.
const ENEMY_SCENE := preload("res://scenes/Enemy.tscn")
func spawn_enemy(pos: Vector2) -> void:
var enemy: Enemy = ENEMY_SCENE.instantiate()
add_child(enemy)
enemy.global_position = pos
PackedScene.instantiate() returns a deep copy of the scene root node, with all its children. The scene's script executes _init and (after add_child) _ready. Nothing in the new instance is shared with the original scene — modifying one does not affect others.
Scene inheritance (right-click a scene → "New Inherited Scene") exists but is rare. Prefer composition: put reusable behavior in a subscene and include it as a child of other scenes.
Physics
# CharacterBody2D — player-controlled, manual movement with collision response
extends CharacterBody2D
const SPEED := 300.0
const GRAVITY := 980.0
const JUMP := -600.0
func _physics_process(delta: float) -> void:
# Gravity
if not is_on_floor():
velocity.y += GRAVITY * delta
# Horizontal input
var dir := Input.get_axis("left", "right")
velocity.x = dir * SPEED
# Jump
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP
move_and_slide()
move_and_slide() is the CharacterBody2D workhorse. Set velocity, call the method, done. It handles collision, sliding along surfaces, floor/wall/ceiling detection. Check is_on_floor(), is_on_wall(), is_on_ceiling() after calling it.
RigidBody2D is for objects driven by physics (boxes, debris). You do not set velocity directly; you apply_impulse(), apply_force(), or let gravity take over.
Area2D is for trigger volumes — "did something enter this zone?" It does not collide, it only overlaps. Connect to body_entered, body_exited, area_entered.
Collision layers and masks are a 32-bit bitmask system. Layers define what this object is. Masks define what this object detects.
| Object | Layer | Mask |
|---|---|---|
| Player | 1 | 2, 3 (detects enemies, pickups) |
| Enemy | 2 | 1 (detects player) |
| Pickup | 3 | 1 (detects player) |
Name your layers in Project Settings → Layer Names. Do not leave them as "Layer 1, Layer 2." You will forget what they mean.
Input
# Define actions in Project Settings → Input Map.
# Actions abstract over specific keys/buttons — rebindable by design.
if Input.is_action_pressed("move_right"): # held this frame
pass
if Input.is_action_just_pressed("jump"): # pressed this frame only
pass
if Input.is_action_just_released("fire"):
pass
# Analog input
var x := Input.get_axis("left", "right") # -1.0..1.0
var dir := Input.get_vector("left", "right", "up", "down") # Vector2, deadzoned
# Event-driven (in _input or _unhandled_input)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
handle_click(event.position)
elif event.is_action_pressed("pause"):
pause()
Always use action names, never raw key codes. The moment a player asks for rebindable controls (and they will), your hardcoded KEY_SPACE is technical debt.
Common Patterns
State Machine (Enum)
enum State { IDLE, PATROL, CHASE, ATTACK, DEAD }
var state: State = State.IDLE
func _physics_process(delta: float) -> void:
match state:
State.IDLE: _state_idle(delta)
State.PATROL: _state_patrol(delta)
State.CHASE: _state_chase(delta)
State.ATTACK: _state_attack(delta)
State.DEAD: pass
func _state_chase(delta: float) -> void:
if player_in_attack_range():
state = State.ATTACK
return
move_toward_player(delta)
For large state machines, extract each state into its own script or node. See Chapter 27 for a full EnemyFSM.gd example.
Object Pool
# Reusable bullet pool — avoids allocation mid-combat
const BULLET_SCENE := preload("res://Bullet.tscn")
const POOL_SIZE := 50
var pool: Array[Bullet] = []
func _ready() -> void:
for i in range(POOL_SIZE):
var b := BULLET_SCENE.instantiate() as Bullet
b.visible = false
b.set_process(false)
add_child(b)
pool.append(b)
func get_bullet() -> Bullet:
for b in pool:
if not b.visible:
b.visible = true
b.set_process(true)
return b
return null # pool exhausted — consider growing
Signal Bus (Event Autoload)
# SignalBus.gd — registered as autoload
extends Node
signal enemy_killed(enemy: Node, xp_reward: int)
signal item_collected(item_name: String)
signal dialogue_started(npc_id: String)
# From anywhere in the game:
# SignalBus.enemy_killed.emit(self, 10)
# Listening from anywhere:
# SignalBus.enemy_killed.connect(_on_enemy_killed)
A signal bus lets decoupled systems talk without holding references. The UI does not need a pointer to every enemy — it just connects to SignalBus.enemy_killed and updates the kill count.
Save / Load with JSON
const SAVE_PATH := "user://save.json"
func save_game() -> void:
var data := {
"hp": player.health,
"level": current_level,
"inventory": player.inventory,
}
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(data))
func load_game() -> void:
if not FileAccess.file_exists(SAVE_PATH):
return
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
var json_text := file.get_as_text()
var data = JSON.parse_string(json_text)
if data == null:
push_error("Corrupt save")
return
player.health = data["hp"]
current_level = data["level"]
player.inventory = data["inventory"]
user:// resolves to a per-user writable directory. Do not write to res:// — it is read-only in exported builds.
Timers
Two ways to wait:
# Option A: Timer node (recurring, inspectable in editor)
# Add a Timer child, set wait_time, connect timeout signal
$AttackCooldown.start()
await $AttackCooldown.timeout
fire()
# Option B: SceneTreeTimer (one-shot, code-only)
await get_tree().create_timer(0.5).timeout
flash_damage_color()
await pauses the function without blocking the engine. The function resumes when the awaited signal fires. This is how you write coroutine-like code in GDScript 4.
Performance Tips
- Use
@onreadyfor node references. Resolving$Pathonce at ready time is vastly faster than repeatedly callingget_node()in_process. - Use
@exportfor designer-editable variables. It is faster than reading from a config file and makes tuning a direct UI operation. - Prefer typed arrays and typed variables. The engine can optimize typed code paths.
- Cache
StringNamefor frequently-used strings. Input actions, signal names, and group names are alreadyStringNameunder the hood — but if you build dynamic strings in a hot loop, intern them. length_squared()beatslength()when you only need to compare distances.- Avoid instantiating nodes mid-combat. Use object pools for bullets, particles, damage numbers.
_physics_processruns at a fixed rate (60Hz by default)._processruns every frame. Use physics process only for physics.- Profile before you optimize. Godot's built-in profiler (Debugger tab → Profiler) will tell you what is actually slow. Your guesses will be wrong 40% of the time.
Gotchas
Integer division. 5 / 2 is 2, not 2.5. You need at least one float: 5.0 / 2 or 5 / 2.0 or float(5) / 2. Catches everyone once.
Packed arrays vs regular arrays. PackedStringArray from split() does not have all the same methods as Array[String]. When in doubt, Array(packed_array) converts.
Signal connection leaks. If you connect a lambda to a signal on a node that outlives the listener, the listener can keep getting called after it has been freed. Use .connect(callable, CONNECT_ONE_SHOT) for fire-once, or disconnect explicitly in _exit_tree.
@tool mode. Scripts with @tool at the top run in the editor. Powerful (editor plugins, in-editor gizmos) but dangerous — your _ready runs at edit time too, which can corrupt scenes if not guarded with if Engine.is_editor_hint().
queue_free() vs free(). Use queue_free() always. free() is immediate and will crash if the node is mid-signal or mid-physics-step.
null vs is_instance_valid(). A freed node reference is not null until the GDScript runtime nulls it — which is usually next frame. If you must hold references across frames, check is_instance_valid(ref) before dereferencing.
Dictionaries are ordered. Godot 4 guarantees insertion order for dictionary iteration. This is often useful and occasionally surprising if you expected hash-map randomness.
match fall-through does not exist. Unlike C's switch, GDScript match does not fall through cases. Each case body is complete and execution jumps out after it.
Cross-Reference to Chapter Scripts
The quick reference above shows patterns in isolation. The chapters show them in context, with full multi-page scripts, design reasoning, and integration with the progressive project. If you want to see these pieces assembled into real systems:
Player.gd— 8-directional movement + attack. Introduced Ch. 3, extended in Ch. 5.GameManager.gd— autoload, progression state,has_item/add_item. Introduced Ch. 6, extended Ch. 7.BoundarySystem.gd/DoorLock.gd— rule enforcement, gating. Introduced Ch. 7.ScreenShake.gd/JuiceEffects.gd— feedback juice: shake, freeze-frame, particles. Introduced Ch. 8.FireSpread.gd— emergent systems (fire + grass interaction). Introduced Ch. 9.LootTable.gd— weighted random drops. Introduced Ch. 10.DifficultyManager.gd/AssistMode.gd— flow tuning, accessibility. Introduced Ch. 11.DialogueSystem.gd— branching dialogue with resource-driven nodes. Introduced Ch. 21.Inventory.gd/ShopNPC.gd— economy implementation. Introduced Ch. 24.CombatSystem.gd/BossFight.gd— hitboxes, i-frames, phase transitions. Introduced Ch. 26.EnemyFSM.gd— finite state machine for patrol/chase/attack/retreat. Introduced Ch. 27.AudioManager.gd— autoload for BGM, SFX, dynamic music layers. Introduced Ch. 30.
Use this appendix to remember the syntax. Use the chapters to remember why.