Pick up coins and watch a counter climb. Introduces signals: one event, two different files reacting to it.
Loading the game engine. This takes a moment the first time.
game_events.gd creates it: signal coin_collected(value: int) At startup, hud.gd and this file each call .connect() one time, with a function name inside: .connect(_on_coin_collected) After that, every time coin.gd calls .emit(value), Godot runs the function each file named inside its own .connect(). One emit, two functions run. To trace any signal: highlight its name, press Ctrl + Shift + F.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
## How many coins are in this level. Change this if you add coins!
@export var total_coins: int = 3
var collected_coins: int = 0
func _ready() -> void:
# We call ".connect" on the signal "coin_collected", and we put the name
# "_on_coin_collected" inside it. That is the function Godot will run,
# written on line 35 below.
# hud.gd connects to this same signal, but names its own function.
GameEvents.coin_collected.connect(_on_coin_collected)
# The "_" in "_value" means we ignore it. hud.gd is the file that uses "value".
func _on_coin_collected(_value: int) -> void:
collected_coins += 1
# "print()" shows up in Godot's Output panel. The player never sees it.
if collected_coins >= total_coins:
print("All coins collected!")extends Area2D
## How many points this coin is worth.
@export var value: int = 1
func _ready() -> void:
# "body_entered" is a SIGNAL that Godot builds into every Area2D.
# The coin emits it whenever something touches the coin.
#
# "connect" is a function that every signal has. Piece by piece:
# body_entered the signal we are waiting for
# .connect run that signal's connect function
# (_on_body_entered) our function, written on line 32 below
# No "()" after "_on_body_entered": we hand Godot the NAME so it
# can run it later. Adding "()" would run it right now instead.
body_entered.connect(_on_body_entered)
# "body" is whatever touched the coin. Godot fills it in for us when
# it emits "body_entered".
func _on_body_entered(body: Node2D) -> void:
# "Player" is the class_name at the top of player.gd.
if body is Player:
# "coin_collected" is a signal we wrote ourselves, over in
# game_events.gd. ".emit(value)" sends it out, carrying "value".
# Two files connect to it: hud.gd (adds the score) and
# coin_pickup.gd (counts how many are left).
GameEvents.coin_collected.emit(value)
# "queue_free()" erases the coin. Emit first, erase second, or
# the coin is gone before it can send the signal.
queue_free()extends CanvasLayer
# This file owns "score". The coins never touch this number.
var score: int = 0
# "%ScoreLabel" means "the node named ScoreLabel in this scene."
@onready var score_label: Label = %ScoreLabel
func _ready() -> void:
# "coin_collected" is a signal we wrote ourselves in game_events.gd.
# coin.gd emits it. We call ".connect" on it here to wait for it: when
# it arrives, run our "_on_coin_collected" function on line 33.
GameEvents.coin_collected.connect(_on_coin_collected)
# "pressed" is a signal Godot builds into every Button. Same pattern:
# when HomeButton emits "pressed", run "_on_home_pressed" on line 29.
%HomeButton.pressed.connect(_on_home_pressed)
_update_label() # show "Coins: 0" before anything is picked up
# Loads the lesson list screen, which replaces this whole scene.
func _on_home_pressed() -> void:
get_tree().change_scene_to_file("res://shared/ui/lesson_select.tscn")
# "value" is the number coin.gd sent along with the signal. It is 1.
func _on_coin_collected(value: int) -> void:
score += value # "+=" means add this on to what we already had
_update_label()
# "_update_label()" is its own function because "_ready()" and
# "_on_coin_collected()" both need it.
func _update_label() -> void:
# "%d" is a blank spot the number drops into. Score of 2 = "Coins: 2"
score_label.text = "Coins: %d" % scoreclass_name Player
extends CharacterBody2D
## How fast the player moves. "@export" puts "speed" at the top of the
## Inspector panel, so you can change it there instead of in this file.
@export var speed: float = 200.0
# "_physics_process()" runs about 60 times a second. Godot calls it for you.
func _physics_process(_delta: float) -> void:
# "Input.get_vector(...)" checks all four keys at once and hands back
# an arrow pointing where to go. The four names in quotes are ours.
# See them under Project > Project Settings > Input Map.
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
# "direction" is only WHICH WAY (its length is always 1).
# "direction * speed" stretches it, so "velocity" is also HOW FAST.
velocity = direction * speed
# Nothing moves until "move_and_slide()" runs.
# "Slide" means we slide along walls instead of sticking to them.
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if direction.x != 0.0:
$Sprite2D.flip_h = direction.x < 0.0extends Player
## How hard the player jumps. NEGATIVE because in Godot up is the negative
## direction. The top of the screen is 0 and the numbers grow going down.
@export var jump_velocity: float = -620.0
## How hard gravity pulls down. A bigger number means you fall faster.
@export var gravity: float = 1800.0
# Player already has a "_physics_process". Writing our own here OVERRIDES it,
# so the top-down version never runs and we get gravity instead.
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# "speed" is not declared in this file. We inherited it from Player.
velocity.x = Input.get_axis("move_left", "move_right") * speed
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if velocity.x != 0.0:
$Sprite2D.flip_h = velocity.x < 0.0