Lesson 09

Health and Damage

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.

Perspective

The level scene has no code at all. Everything happens in these three files, and they talk using signals plus one direct function call.

HOW A HIT TRAVELS

  1. The player walks into a hazard.
  2. hazard.gd calls body.take_damage(1) right on the player. It can do that because body_entered already handed it the player.
  3. This file lowers "health", then emits health_changed.
  4. hud.gd connects to health_changed and redraws the number.

New code in this lesson

None of the earlier lessons use these. Each one shows the line where it appears here.

func():

A function with no name, written right where it is used, often inside .connect().

hazard.gd
timer.timeout.connect(func() -> void: _can_hurt = true)

The scenes

■ UI node · ■ 2D node · % unique name · .tscn instanced scene

game.tscn
  • Game Node2D
  • Player CharacterBody2D player.tscn
  • Hazard1 Area2D hazard.tscn
  • Hazard2 Area2D hazard.tscn
  • Hazard3 Area2D hazard.tscn
  • Hazard4 Area2D hazard.tscn
  • HUD CanvasLayer hud.tscn
hazard.tscn
  • Hazard Area2D hazard.gd
  • Polygon2D
  • CollisionShape2D
hud.tscn
  • HUD CanvasLayer hud.gd
  • MarginContainer
  • HealthLabel Label %
player.tscn
  • Player CharacterBody2D group: Player player.gd
  • Sprite2D
  • CollisionShape2D

FileSystem

  • game.tscn
  • hazard.tscn
  • hazard.gd
  • hud.tscn
  • hud.gd
  • player.tscn
  • player.gd

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.

The code

Perspective
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)