GDScript looks like Python but plays by different rules. Signals replace callbacks. Nodes replace objects. @export replaces config files. And every frame, _physics_process runs whether you remember the syntax or not.
SyntaxCache covers the GDScript you actually write in Godot 4: node references, signal connections, scene instancing, vector math. The exercises build toward an Arena Survivor game, one pattern at a time.
Popular topics include GDScript Foundations Practice, GDScript Movement Practice, GDScript Timers & Signals Practice.
Built for GDScript Learners
- Indie devs building games in Godot 4.
- Unity refugees learning the Godot way.
- Hobbyists who know programming but not GDScript.
- Game jam participants who need fast recall.
- Connect signals and handle collisions without checking the docs.
- Instantiate scenes, spawn enemies, and manage the scene tree from memory.
- Write vector math for movement, aiming, and projectiles on instinct.
- Use @export, tweens, and timers without guessing at syntax.
Practice by Topic
Pick a concept and train recall with small, focused reps.
GDScript Foundations Practice
Nail the first 10 lines of every script: extends, typed vars, @export, @onready, _ready.
Show 3 gotchasHide gotchas
@onreadyruns at_ready()time, not parse time. Using it outside the tree crashes._init()runs before the node enters the tree. No$paths work yet.- Forgetting
extendsdefaults to RefCounted, not Node.
GDScript Movement Practice
Stop copy-pasting CharacterBody2D code. Know velocity, move_and_slide(), and delta.
Show 3 gotchasHide gotchas
move_and_slide()takes no arguments in Godot 4. Setvelocityfirst.- Use
_physics_process(delta)for movement, not_process. Otherwise speed varies with FPS. Input.get_vector()returns a normalized vector. No need to normalize again.
GDScript Timers & Signals Practice
Connect signals, bind args, and use timers without double-fire bugs.
Show 3 gotchasHide gotchas
create_timer()has no cancel API. Use a Timer node if you need to stop it.- Connecting the same signal twice causes duplicate calls. Guard with
is_connected(). - Store bound callables (
func.bind(arg)) if you plan todisconnect()later.
GDScript Scene Instancing Practice
Spawn scenes correctly: preload, instantiate, add_child, set position.
Show 3 gotchasHide gotchas
- Set
global_positionafteradd_child(), not before. No parent transform yet. instance()was renamed toinstantiate()in Godot 4.- Use
add_child.call_deferred()when spawning during callbacks or_ready().
GDScript Exports Practice
Tune gameplay from the Inspector with @export, ranges, enums, and groups.
Show 3 gotchasHide gotchas
@exportreplacedexport()from Godot 3. Parentheses are gone.@export_range(0.0, 1.0, 0.01)adds an Inspector slider. Third arg is step.@export_group("Label")organizes exports visually. Does not affect code behavior.
GDScript Vector Math Practice
Direction, distance, dot, cross, bounce. The Vector2 methods you use every frame.
Show 3 gotchasHide gotchas
distance_squared_to()skips the sqrt. Use it when comparing distances.Vector2.ZERO.normalized()returns(0,0), not NaN. Manualv / v.length()does crash.bounce()is for physics ricochet.reflect()is the mathematical mirror (rarely what you want).
GDScript Tweens & Polish Practice
Code-driven animations: hit flashes, pop scales, smooth fades, chain sequences.
Show 3 gotchasHide gotchas
Tween.new()is invalid. Always usecreate_tween()from a node.- Each
create_tween()auto-kills the previous tween on that node by default. set_parallel()runs all subsequent steps at once. Without it, steps are sequential.
GDScript Collisions Practice
Fix "body_entered not firing" and understand layers vs masks.
Show 3 gotchasHide gotchas
- Layer = what I am. Mask = what I detect. A detects B if A's mask includes B's layer.
- Area2D needs
monitoring = true. RigidBody2D needscontact_monitor = true+max_contacts_reported > 0. - Missing CollisionShape2D is the most common reason signals never fire.
GDScript Resources Practice
Custom data assets, shared-state bugs, and when to duplicate().
Show 3 gotchasHide gotchas
- Resources are shared by default. Mutating one instance changes all references.
duplicate(true)does NOT deep-copy Resources inside Arrays. Manual copy needed.- Use
preload()for compile-time paths,load()for runtime/dynamic paths.
GDScript Randomness Practice
randf_range, randi_range, seeded RNG, weighted picks, shuffle bags.
Show 3 gotchasHide gotchas
randf_range(0.0, 1.0)is inclusive on both ends.randi_range(0, 5)includes 5.- For reproducible results, use
RandomNumberGeneratorwith a fixedseed. array.pick_random()is uniform. For weighted drops, build a cumulative threshold array.
GDScript Groups & Pickups Practice
Tag nodes with groups and query/broadcast without hard-coded paths.
Show 3 gotchasHide gotchas
is_in_group()takes a StringName. Use&"group_name"for performance.call_group()calls a method on every node in the group. No return values.get_nodes_in_group()returns an Array. It may include nodes being freed this frame.
GDScript Arrays & Loops Practice
Iterate, filter, sort, and safely remove elements from GDScript arrays.
Show 3 gotchasHide gotchas
- Removing items while iterating forward skips elements. Iterate backward or filter.
array.sort_custom(func)sorts in-place and returns void.- Typed arrays (
Array[int]) catch type mismatches at assignment time.
GDScript Projectiles Practice
Spawn bullets with direction, lifetime, and collision filtering.
Show 3 gotchasHide gotchas
- Set collision layers so bullets don't hit the shooter. Different layers for player vs enemy projectiles.
- Every
instantiate()needs a matchingqueue_free()or your scene tree grows forever. - Fast bullets can tunnel through thin walls. Use RayCast2D or ShapeCast2D for hit detection.
GDScript UI & HUD Practice
CanvasLayer, anchors, containers, and signal-driven HUD updates.
Show 3 gotchasHide gotchas
- Without CanvasLayer, your HUD moves with the camera.
- Set root Control to Full Rect anchor preset so it fills the screen.
- Use bind/unbind pattern for player respawn. Signals can fire after
queue_free()until end-of-frame.
Procedural Generation
Four dungeon algorithms: drunkard's walk, cellular automata, BSP, and noise terrain.
Show 3 gotchasHide gotchas
- Cellular automata needs double-buffering. Reading and writing the same grid corrupts neighbor counts.
- Drunkard's walk and BSP guarantee connectivity; cellular automata and noise terrain do NOT. Run flood fill.
- Use seeded
RandomNumberGenerator, not globalrandf(), so maps are reproducible for debugging.
TileMapLayer
set_cell, erase_cell, map_to_local, and the TileMapLayer migration from Godot 4.3+.
Show 3 gotchasHide gotchas
get_cell_source_id()returns -1 for empty cells, NOT 0. Source ID 0 is a valid tileset.map_to_local()returns the tile CENTER, not the top-left corner.- The old
TileMapnode is deprecated in Godot 4.3. Use individualTileMapLayernodes instead.
GDScript Cheat Sheets
Copy-ready syntax references for quick lookup
What You'll Practice
Real GDScript patterns you'll type from memory
var direction := Input.get_vector("left", "right", "up", "down")
velocity = direction * speed
move_and_slide()timer.timeout.connect(_on_timeout)
button.pressed.connect(_on_pressed)const BulletScene: PackedScene = preload("res://scenes/bullet.tscn")
var bullet := BulletScene.instantiate() as Area2D
add_child(bullet)
bullet.global_position = muzzle.global_position@export var speed: float = 200.0
@export_range(0.0, 1.0, 0.01) var friction: float = 0.8var tween := create_tween()
tween.tween_property(self, "modulate", Color.RED, 0.05)
tween.tween_property(self, "modulate", Color.WHITE, 0.1)func _on_body_entered(body: Node2D) -> void:
if body.is_in_group(&"enemy"):
take_damage(body.damage)var dir := global_position.direction_to(target.global_position)
velocity = dir * chase_speedawait get_tree().create_timer(0.25).timeoutSample Exercises
Apply movement to a CharacterBody2D using direction and speed. Set velocity, then move. (Assume direction and speed exist.)
velocity = direction * speed
move_and_slide()In 3 lines: instantiate enemy_scene into enemy, set enemy.global_position to spawn_point, add to parent.
var enemy = enemy_scene.instantiate()
enemy.global_position = spawn_point
get_parent().add_child(enemy)Why this works
GDScript fades fast if you only touch Godot on weekends. The algorithm brings back each pattern right before you'd blank on it. After a few rounds, the syntax is there when you need it.
What to expect
Week 1: You'll see exactly what trips you up. Maybe it's signal.connect() syntax, maybe it's forgetting global_position after add_child(). The algorithm finds the gaps.
Week 2-4: Intervals stretch. Patterns you got right move to 2-day, 4-day, then weekly review. You stop reaching for the docs on basics like @export and move_and_slide().
Month 2+: Core GDScript is automatic. Reviews take 10 minutes. You still practice new concepts (resources, UI, randomness), but the foundations don't need refreshing anymore.
FAQ
Godot 4 only. All exercises use modern syntax: @export, move_and_slide() with no args, signal.connect(callable).
No. Everything runs in the browser. The exercises teach syntax recall, not editor workflow.
The syntax is similar but the concepts differ. Signals, nodes, and the scene tree are Godot-specific and need their own drilling.
For most solo and small-team Godot projects, yes. GDScript has the tightest integration with the editor, the best documentation coverage, and the quickest edit-run loop.
Both. GDScript is gradually typed: you can write quick dynamic code, then add type hints where you want safety, editor autocomplete, and fewer runtime surprises.
The biggest day-to-day differences are callables/signals (less stringly-typed), @export/@onready annotations, and updated physics APIs like CharacterBody2D using a velocity property with move_and_slide() taking no arguments.
Node2D is the base for 2D objects with position/rotation/scale. CharacterBody2D extends it with physics collision and move_and_slide() for kinematic movement. Use Node2D for simple objects; use CharacterBody2D when you need collision response.
You're probably using _process instead of _physics_process, or forgetting to multiply by delta. _physics_process runs at a fixed rate (60 Hz by default) regardless of FPS.
No, but you should. Type hints catch errors at parse time, enable better editor autocomplete, and make code self-documenting. Use `: Type` for variables and `-> Type` for return values.