Lesson 06

Hazards and respawning

Stakes make exploration matter. We add a hazard zone that resets the player to a safe spawn point when touched. Same Area3D pattern as fragments, different consequence.

▸ By the end of this lesson: A hazard that respawns the player at a checkpoint on contact

6.1Mark a spawn point

  1. Open main.tscn. Add a child Marker3D to Main, rename it SpawnPoint. Position it somewhere safe on the floor (y ≈ 1).

Marker3D

A Marker3D is an invisible position holder — no mesh, no collision, just a transform. Ideal for spawn points, patrol targets, and camera anchors. It shows as a small cross in the editor and nothing in-game.

6.2Build a hazard zone

  1. New Scene → "Other Node" → Area3D root, rename Hazard. Save as hazard.tscn.
  2. Add a child MeshInstance3D → New BoxMesh, scale it into a flat slab (a glowing red 'corruption pool'). Give it an emissive red material.
  3. Add a child CollisionShape3D → New BoxShape3D matching the slab.

Attach hazard.gd:

hazard.gdgdscript
extends Area3D

# Emitted when the player touches the hazard.
signal player_hit(body: Node3D)

func _ready() -> void:
	body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node3D) -> void:
	if body.is_in_group("player"):
		player_hit.emit(body)

Signal up, don't act down

The hazard doesn't move the player itself — it announces a hit and lets Main decide what happens (respawn now, later maybe lose a life). Emitting signals instead of reaching across the scene keeps each piece independent and easy to change.

6.3Respawn from Main

  1. Instance hazard.tscn under Main and place it in the player's path.
  2. Group hazards: select it, Node tab → Groups → add hazards.

Extend main.gd to wire hazards and respawn the player at the marker:

main.gd (replace _ready, add handlers)gdscript
@onready var spawn_point: Marker3D = $SpawnPoint
@onready var player: CharacterBody3D = $Player

func _ready() -> void:
	var frags: Array = get_tree().get_nodes_in_group("fragments")
	total_fragments = frags.size()
	for frag in frags:
		frag.collected.connect(_on_fragment_collected)

	for hz in get_tree().get_nodes_in_group("hazards"):
		hz.player_hit.connect(_on_player_hit)

	_respawn_player()   # start the player at the spawn point

func _on_player_hit(_body: Node3D) -> void:
	_respawn_player()

func _respawn_player() -> void:
	player.velocity = Vector3.ZERO
	player.global_position = spawn_point.global_position

Zero the velocity on respawn

If you teleport the player without resetting velocity, any downward speed from falling carries over and they may immediately clip or shoot off. Always zero velocity when repositioning a physics body.

global_position vs position

Use global_position to place the player at the marker's world location. Plain position is relative to the parent — if Main is at origin they happen to match, but global_position is the correct, robust choice.

  1. Run with F6. Walk into the hazard — you snap back to the spawn point instantly.
  2. The player also starts at the spawn point on launch.

Checkpoint — expected state