Lose health by touching a hazard. Mixes a signal with a direct function call, and shows when to use each.
Loading the game engine. This takes a moment the first time.
The level scene has no code at all. Everything happens in these three files, and they talk using signals plus one direct function call.
hazard.gd calls body.take_damage(1) right on the player. It can do that because body_entered already handed it the player.health_changed.hud.gd connects to health_changed and redraws the number.None of the earlier lessons use these. Each one shows the line where it appears here.
A function with no name, written right where it is used, often inside .connect().
timer.timeout.connect(func() -> void: _can_hurt = true)■ UI node · ■ 2D node · % unique name · .tscn instanced scene
game_events.gd
Switch it on once, under Project > Project Settings, on the Globals tab. Not made yet? Press Create Script to make it right there. Already made it? Press Select Script/Scene, pick it, and press Add. Its Name column should say GameEvents, which Godot fills in for you. That's the name your code uses.
class_name Player
extends CharacterBody2D
## How fast the player moves.
@export var speed: float = 200.0
## How much health the player starts with, and the most they can hold.
@export var max_health: int = 5
# The health right now. "_ready()" fills it in when the level starts.
var health: int
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
# Begin the level at full health.
health = max_health
func _physics_process(_delta: float) -> void:
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
velocity = direction * speed
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if direction.x != 0.0:
sprite.flip_h = direction.x < 0.0
## Called BY hazard.gd, not by anything in this file. Any script holding
## this player can call "take_damage(1)" to hurt it.
func take_damage(amount: int) -> void:
health -= amount
# Never let a negative number reach the screen.
if health <= 0:
health = 0
# hud.gd connects to "player_died" and shows the death message.
GameEvents.player_died.emit()
# Emit this one LAST, so the HUD draws the corrected 0 instead of -1.
GameEvents.health_changed.emit(health)extends Area2D
## How much health the player loses on a touch.
@export var damage: int = 1
## How many seconds before this hazard is allowed to hurt again.
@export var cooldown: float = 1.0
# True when this hazard is ready to hurt. False during the cooldown.
var _can_hurt: bool = true
func _ready() -> void:
body_entered.connect(_on_body_entered)
# "body" is whatever touched the hazard.
func _on_body_entered(body: Node2D) -> void:
# BOTH must be true: it is the player, AND we are not cooling down.
# "Player" is a group ticked on the player's root node (Node dock > Groups).
if body.is_in_group("Player") and _can_hurt:
# "body_entered" already handed us the player as "body", so we just
# call a function on it. No signal needed going back. This is how
# damage works in most games.
body.take_damage(damage)
# Start the cooldown so we cannot hurt again straight away.
_can_hurt = false
# "create_timer()" makes a one-time timer without adding a node.
var timer: SceneTreeTimer = get_tree().create_timer(cooldown)
# The "func() -> void:" bit is a lambda, a tiny function with no name.
# When the timer runs out it flips "_can_hurt" back to true.
timer.timeout.connect(func() -> void: _can_hurt = true)extends CanvasLayer
# "%HealthLabel" means "the node named HealthLabel in this scene."
@onready var health_label: Label = %HealthLabel
@onready var home_button: Button = %HomeButton
func _ready() -> void:
# player.gd emits BOTH of these signals, inside its "take_damage()".
GameEvents.health_changed.connect(_on_health_changed)
GameEvents.player_died.connect(_on_player_died)
home_button.pressed.connect(_on_home_pressed)
# 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")
# "new_health" is the number player.gd sent along with the signal.
func _on_health_changed(new_health: int) -> void:
# "%d" is a blank spot that the number drops into.
health_label.text = "Health: %d" % new_health
func _on_player_died() -> void:
health_label.text = "You died!"extends CharacterBody2D
## How fast the player runs left and right.
@export var speed: float = 200.0
## How hard the player jumps. Negative is up.
@export var jump_velocity: float = -620.0
## How hard gravity pulls down.
@export var gravity: float = 1800.0
## How much health the player starts with, and the most they can hold.
@export var max_health: int = 5
# The health right now. "_ready()" fills it in when the level starts.
var health: int
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
# Begin the level at full health.
health = max_health
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
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:
sprite.flip_h = velocity.x < 0.0
## Called BY hazard.gd, not by anything in this file. Any script holding
## this player can call "take_damage(1)" to hurt it.
func take_damage(amount: int) -> void:
health -= amount
# Never let a negative number reach the screen.
if health <= 0:
health = 0
# hud.gd connects to "player_died" and shows the death message.
GameEvents.player_died.emit()
# Emit this one LAST, so the HUD draws the corrected 0 instead of -1.
GameEvents.health_changed.emit(health)extends Node
# All of the game's signals are declared below, grouped by the lesson
# that uses them. A signal with brackets, like coin_collected(value: int),
# passes that value to every function connected to it.
# This shows only the signals this lesson uses. The full file declares
# every lesson's signals.
# Health and Damage lesson
signal health_changed(new_health: int)
signal player_died