The words the lessons assume you already have. Start at the top: a game is nodes, nodes are arranged into scenes, scripts tell a node what to do, and signals are how nodes talk to each other.
Every word comes with the real thing next to it: the line of GDScript it shows up in, and for 22 of them the actual scene tree from a lesson, with the node lit up so you can see where it sits.
The pieces every Godot game is built out of. Learn these first, because every lesson assumes them.
One piece of your game: a player, a wall, a coin, a label. Every node is one kind of thing, and that kind decides what it can do. Nodes are the smallest thing Godot works with.
Three nodes. Each row is one, and its type (the grey word) is what decides what it can do.
A group of nodes saved together in one .tscn file. A scene can be a whole level, or one small thing like a coin that you use over and over.
Those same three nodes saved together in one file. Drop player.tscn into any level and you get all three at once.
The list of nodes in the Scene dock, drawn as a family tree. Nodes indented under another node are its children, and children move with their parent.
Indent is parent and child. Move Borders and all four of its shapes move with it, because they are underneath it.
The node at the very top of a scene. Its type is what the whole scene counts as, so a scene whose root is a CharacterBody2D can be dropped into a level and it moves.
The top row, the one nothing is indented under. The whole scene counts as a CharacterBody2D because that is what its root is.
The plain 2D node. It has a position and a rotation and draws nothing on its own, so it is what you reach for when you just need something to hold other nodes.
It draws nothing at all. It is a position for other nodes to hang off, which is what almost every level root is. (Borders really has four shapes; two are shown.)
extends Node2D
# draws nothing, but it has a position
position = Vector2(100, 50)A body you drive yourself with code. It is the node for anything that walks, runs, or jumps: the player, and usually the enemies. You set velocity, call move_and_slide(), and Godot stops it at walls.
The thing you drive. It is the only node in this scene that moves, and the only one with a script.
extends CharacterBody2D
func _physics_process(delta):
velocity = Vector2(200, 0)
move_and_slide()A solid thing that never moves: walls, floors, platforms. It blocks bodies that run into it and needs no code at all.
A wall never moves and has no script. It only has to be there, with a collision shape under it, and the player stops. (The real maze has four walls; two are shown.)
A shape that notices a touch but does not block it. Coins, spikes, checkpoints, bullets. It tells you something arrived by sending the body_entered signal.
Reach for an Area2D when you want to NOTICE a touch without blocking it: coins, checkpoints, hazards, goal zones. A StaticBody2D would stop the player; this lets them walk right through and sends body_entered as they do.
extends Area2D
func _on_body_entered(body):
queue_free() # noticed the touch, did not block itThe invisible shape that says which part of a node counts as solid. A body or an area with no CollisionShape2D child touches nothing, which is the most common reason a coin does nothing when you walk over it.
It goes UNDER the node that needs to be touchable, never on its own. Leave it off and the coin is invisible to collisions: the player walks through and nothing happens.
The node that draws a picture. Drag an image into its Texture slot and it shows up. The picture is only how the thing looks, never how it collides.
A child of the player, so it moves with it. The Sprite2D is what you SEE; the CollisionShape2D next to it is what you HIT. They are two different nodes on purpose.
$Sprite2D.texture = load("res://art/coin.png")
$Sprite2D.flip_h = trueA node that counts down and sends timeout when it reaches zero. Use it for a shot cooldown, a countdown clock, or a spawner that fires every two seconds.
A Timer sits in the scene like any other node. It counts down and sends timeout, over and over if you let it.
$Timer.wait_time = 2.0
$Timer.start() # sends timeout in 2 secondsDecides which part of the world is on screen. Make it a child of the player and the view follows the player for free.
A CHILD of the player, and that is the whole trick: children move with their parent, so the view follows without a single line of code.
A layer that stays put on screen even when the camera moves. Your score, your hearts, and your menus go in here so they do not slide away with the world.
Everything underneath it is drawn on the SCREEN instead of in the world, so the score stays in its corner while the camera moves.
Draws text on screen. Set its text from code to show a score, a countdown, or the words GAME OVER.
Green rows are UI nodes. The % is Access as Unique Name, which is what lets the script say %ScoreLabel instead of spelling out the path through MarginContainer.
%ScoreLabel.text = "Score: " + str(score)The node that plays a sound. Put the sound file in its Stream slot, then call .play() when the coin is picked up.
One node per sound, sitting in the scene with everything else. After that, $PickupSound.play() is the entire thing. (The real scene also has a PickupArea the player walks into.)
$PickupSound.play()Changes a value over time and can loop it: a coin that spins, a door that slides open, a sprite that flashes red when it is hit.
A sibling of the thing it animates, not a parent of it. It holds the named animations; the script only ever asks for one by name.
$AnimationPlayer.play("spin")
$AnimationPlayer.play("hit_flash")An invisible node used only to mark a spot. Ours sits at the tip of the gun, so bullets start there instead of in the middle of the player.
An empty point that moves with the gun. Spawn the bullet at $Muzzle.global_position and it leaves the barrel every time, whichever way the player is facing.
var bullet = bullet_scene.instantiate()
bullet.global_position = $Muzzle.global_positionA saved scene file sitting ready to be copied, like a cookie cutter. It is not in the game yet.
This scene is not in the level anywhere. It is the cookie cutter the spawner loads and copies while the game runs.
@export var enemy_scene: PackedScene
# the cookie cutter, not a cookie yetOne real copy of a scene, alive in the game. instantiate() makes the copy and add_child() puts it in, which is how one bullet scene becomes fifty bullets.
Three copies of coin.tscn, plus one of player.tscn and one of hud.tscn. The teal badge is the file each copy came from: edit the coin scene once and all three change.
var enemy = enemy_scene.instantiate() # one real copy
add_child(enemy) # now it is in the gameWhat the code attached to a node is made of, and the handful of lines that show up in almost every lesson.
A .gd file attached to a node that says what that node does. The node is the thing; the script is its behaviour.
The badge on the right is the script file attached to that node. One script per node at most, and most nodes have none.
# player.gd, attached to the Player node
extends CharacterBody2D
var speed = 200The first line of every script. extends CharacterBody2D means this script is attached to a CharacterBody2D and already knows everything that node can do.
extends CharacterBody2D # always the first lineStarts a function: a named block of lines that runs when something calls it. Everything indented under it belongs to it.
func take_damage(amount):
health -= amount # indented, so it belongs to the func
if health <= 0:
queue_free()Makes a variable, a named box holding a value the script can read and change later, like var speed = 200.
var speed = 200
var health = 3
speed = 400 # a variable can change laterPuts a variable in Godot’s Inspector so you can change it without editing code. Speed, jump height, and health all belong here, because no number is right on the first try.
@export var speed = 200
@export var jump_height = 400
# both now show up in the InspectorGrabs a node AFTER the scene is built instead of before. Without it, the script goes looking for a node that does not exist yet and you get a null error.
@onready var label = %ScoreLabel
# waits until the scene exists, so label is never nullThe shortcut for a node marked "Access as Unique Name". %ScoreLabel finds that node anywhere in the scene, so moving it in the tree does not break your code. $Sprite2D is the other way, spelling out the exact path.
The % is switched on per node in the Scene dock. It is what makes %ScoreLabel work from anywhere in this scene, even though the label is buried under a container.
%ScoreLabel.text = "0" # found anywhere in the scene
$Sprite2D.flip_h = true # the exact path, from this nodeA function Godot runs ONCE, the moment the node joins the game. One-time setup lives here: connect your signals, grab your nodes, set the starting score.
func _ready():
score = 0 # runs once, when the node joins the game
$Timer.start()Runs 60 times a second, in step with the physics engine. Anything about moving a body or bumping into things goes here.
func _physics_process(delta):
velocity.x = 200 # 60 times a second
move_and_slide()Runs every drawn frame, which is as fast as the computer can manage. Use it for looks only: camera smoothing, spinning a coin, updating a label.
func _process(delta):
rotation += 2 * delta # looks only, every drawn frameHow many seconds went by since the last frame, usually about 0.016. Multiplying by it turns "a bit every frame" into "the same amount every second", so your game plays the same on a fast computer and a slow one.
position.x += speed * delta
# same distance per SECOND on a fast or slow computerTwo numbers that work like an arrow: x and y together. Positions, directions, and speeds are all Vector2. On screen, y gets bigger going DOWN.
var dir = Vector2(1, 0) # right
var up = Vector2(0, -1) # up, because y grows downward
position += dir * speedA CharacterBody2D’s built-in Vector2 for how fast it is moving and which way. Setting it does nothing on its own until you call move_and_slide().
velocity = Vector2(200, 0) # sets it, moves nothing yet
move_and_slide() # now it movesTakes the velocity you set and actually moves the body, sliding it along walls instead of sticking. One line, and collisions are handled for you.
velocity = Vector2(200, 0)
move_and_slide() # moves, and slides along wallsHow you ask what the player is doing right now. Input.is_action_pressed("move_right") is true while that key is held. The action names come from Project Settings, so changing the key never touches your code.
if Input.is_action_pressed("move_right"):
velocity.x = speed
# "move_right" is named in Project Settings, not hereDeletes a node, safely, at the end of the current frame. This is how a coin disappears when you touch it and how a bullet cleans itself up.
func _on_body_entered(body):
queue_free() # the coin removes itselfGives a script a name other scripts can check for. Write class_name Player in player.gd and any script can ask if body is Player, which is how a coin knows the player touched it and not an enemy.
# in player.gd
class_name Player
# in coin.gd
if body is Player:
queue_free()How one node tells the rest of the game that something happened, without the two files knowing about each other.
A message a node sends out when something happens. A signal is not a function: it does not do anything by itself, it just announces. Whoever cares is listening.
signal coin_collected(value)
# declared, but nothing has happened yetTo send the signal. coin_collected.emit(value) shouts "a coin was collected" to the whole game. If nobody is listening, nothing happens, and that is fine.
coin_collected.emit(1) # shouts it to whoever is listeningTo start listening. body_entered.connect(_on_body_entered) says "when that signal arrives, run this function of mine". Hand over the function NAME with no (), or you run it right now by accident.
body_entered.connect(_on_body_entered)
# the function NAME, with no () after itThe function that runs when the signal arrives. By habit its name starts with _on_, as in _on_body_entered, so you can tell at a glance that you never call it yourself.
func _on_body_entered(body):
queue_free()
# the _on_ prefix means you never call it yourselfA signal every Area2D already has. It fires when a body moves into the area and hands you the body that did it, so the coin can check whether it was the player.
func _on_body_entered(body):
if body is Player:
queue_free() # a coin the player touchedThe signal a Timer sends when it hits zero. Connect it to spawn the next enemy, end the round, or let the player shoot again.
$Timer.timeout.connect(_on_timer_timeout)
func _on_timer_timeout():
spawn_enemy()The signal a Button sends when it is clicked. Same pattern as every other signal, which is the point: once you know one, you know them all.
%PlayButton.pressed.connect(_on_play_pressed)
func _on_play_pressed():
start_game()One shared script holding the signals the whole game cares about, ours is game_events.gd. The coin emits into it and the HUD listens to it, so neither file has to know the other exists.
# in game_events.gd
signal coin_collected(value)
# the coin shouts, the HUD listens, neither knows the other
GameEvents.coin_collected.emit(1)A label you stick on a node with add_to_group("enemies"). Other scripts can then ask is_in_group("enemies") without knowing which file that node came from.
add_to_group("enemies")
if body.is_in_group("enemies"):
take_damage(1)