Zonas de Dano (Espinhos e Lava) no Godot 4

Aprenda a montar uma zona de dano godot com Area2D para espinhos e lava, dano por segundo, knockback, i-frames e respawn no ultimo checkpoint.
Quase todo jogo de plataforma tem aquele canto do mapa que machuca: o tapete de espinhos, a poca de lava, o feixe de laser. Montar uma zona de dano godot que funcione bem nao e so "tirar vida quando encosta". Voce precisa decidir se o dano e instantaneo ou continuo, aplicar empurrao para o jogador nao grudar no perigo, dar alguns quadros de invencibilidade e tratar o que acontece quando a vida chega a zero. Neste post a gente monta isso com Area2D, um Timer e uns poucos @export para reusar o mesmo script em vario tipos de armadilha.
Zonas de Dano (Espinhos e Lava) no Godot 4
A ideia central e separar duas responsabilidades. A armadilha sabe quanto de dano causa e de que tipo (toque unico ou repetido). O personagem sabe como receber dano, perder vida e reagir. Quando essas duas pecas conversam por sinais, voce troca um espinho por lava mudando so parametros no Inspector, sem reescrever logica.
Por que Area2D e nao um corpo solido
Para detectar sobreposicao sem bloquear o movimento do jogador, o no certo e o Area2D. Ele dispara sinais quando outro corpo entra ou sai da regiao, mas nao empurra ninguem nem participa da fisica de colisao. E exatamente o comportamento que voce quer em espinhos rasos ou numa poca de lava: o jogador atravessa visualmente e a area so registra o contato.
Se voce ainda esta na duvida entre detectar contato com area ou com corpo rigido, vale ler Area2D vs Body no Godot antes de seguir, porque a escolha muda o resto do design.
A estrutura de cena da armadilha fica assim:
Area2D(raiz, com o script)CollisionShape2D(o formato da zona perigosa)Sprite2DouAnimatedSprite2D(o visual de espinho ou lava)
No Inspector do Area2D, configure as camadas: a armadilha deve estar numa layer propria (por exemplo "hazard") e ter no mask a layer do jogador. Assim a area so reage ao player e ignora inimigos ou plataformas.
Espinho: dano instantaneo no contato
O espinho causa dano uma unica vez quando o corpo entra. Nao precisa de Timer para isso. Basta escutar o sinal body_entered e chamar o metodo de dano do jogador.
extends Area2D
@export var damage: int = 1
@export var knockback_force: float = 220.0
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.has_method("take_damage"):
var direction := (body.global_position - global_position).normalized()
body.take_damage(damage, direction * knockback_force)
Repare em dois detalhes. Primeiro, o has_method("take_damage") evita erro caso outra coisa caia na area por engano. Segundo, a direcao do knockback sai do centro da armadilha em direcao ao corpo, entao o empurrao sempre afasta o jogador do espinho, nao importa de que lado ele encostou.
Lava: dano por segundo com Timer
A lava e diferente. Enquanto o jogador estiver dentro, ele toma dano em intervalos regulares. Aqui entra um Timer. A logica e: quando o corpo entra, comeca a contar e aplica o primeiro dano; quando sai, para o timer.
Em vez de criar dois scripts, dá para unir tudo num so usando um @export que define o modo. Adicione um no Timer como filho do Area2D e deixe o one_shot desligado.
extends Area2D
enum DamageMode { INSTANT, PER_SECOND }
@export var mode: DamageMode = DamageMode.INSTANT
@export var damage: int = 1
@export var knockback_force: float = 220.0
@export var tick_interval: float = 0.75
@onready var tick_timer: Timer = $Timer
var _body_inside: Node2D = null
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
tick_timer.wait_time = tick_interval
tick_timer.timeout.connect(_on_tick)
func _on_body_entered(body: Node2D) -> void:
if not body.has_method("take_damage"):
return
_apply_damage(body)
if mode == DamageMode.PER_SECOND:
_body_inside = body
tick_timer.start()
func _on_body_exited(body: Node2D) -> void:
if body == _body_inside:
_body_inside = null
tick_timer.stop()
func _on_tick() -> void:
if _body_inside != null:
_apply_damage(_body_inside)
func _apply_damage(body: Node2D) -> void:
var direction := (body.global_position - global_position).normalized()
body.take_damage(damage, direction * knockback_force)
Com esse script, um espinho usa mode = INSTANT e um charco de lava usa mode = PER_SECOND com tick_interval de, digamos, 0.75 segundo. Mesmo arquivo, comportamentos diferentes, tudo decidido no Inspector. Se voce quiser uma lava mais cruel, baixa o intervalo; se quiser que ela apenas chamusque, aumenta.
Recebendo dano no personagem com i-frames
Do lado do jogador, o metodo take_damage faz quatro coisas: ignora o golpe se ainda estiver invencivel, reduz a vida, aplica o empurrao e liga os quadros de invencibilidade (os i-frames). Esses i-frames evitam que a lava drene a vida inteira em poucos quadros e dao ao jogador uma janela para escapar.
extends CharacterBody2D
@export var max_health: int = 6
@export var invincibility_time: float = 0.8
var health: int
var _invincible: bool = false
var _knockback: Vector2 = Vector2.ZERO
@onready var hit_timer: Timer = $HitTimer
signal health_changed(current: int, maximum: int)
signal died
func _ready() -> void:
health = max_health
hit_timer.one_shot = true
hit_timer.wait_time = invincibility_time
hit_timer.timeout.connect(func() -> void: _invincible = false)
health_changed.emit(health, max_health)
func take_damage(amount: int, knockback: Vector2 = Vector2.ZERO) -> void:
if _invincible:
return
health -= amount
health_changed.emit(health, max_health)
_knockback = knockback
_invincible = true
hit_timer.start()
if health <= 0:
_die()
func _die() -> void:
died.emit()
O _knockback guardado e aplicado dentro do _physics_process, junto com o movimento normal. Uma forma simples e somar o empurrao a velocity e deixar ele decair com o tempo:
func _physics_process(delta: float) -> void:
if _knockback.length() > 1.0:
velocity = _knockback
_knockback = _knockback.move_toward(Vector2.ZERO, 900.0 * delta)
else:
# aqui entra seu movimento normal de input
velocity.x = Input.get_axis("move_left", "move_right") * 180.0
velocity.y += 1200.0 * delta # gravidade
move_and_slide()
Enquanto o knockback for forte, ele domina a velocity e o jogador e arremessado para longe da armadilha. Quando enfraquece, o controle volta para o input. Se voce quer um tratamento mais completo dessa parte, com pisca-pisca visual durante a invencibilidade, o post sobre knockback e invencibilidade no Godot detalha o efeito de blink usando modulate.
Respawn no ultimo checkpoint quando a vida zera
Quando health chega a zero, o personagem emite o sinal died. Em vez de recarregar a cena inteira, o ideal e devolver o jogador ao ultimo checkpoint salvo. Um gerenciador simples (um autoload, por exemplo) guarda a posicao do ultimo ponto seguro e reposiciona o player.
extends Node
# Autoload chamado GameState
var last_checkpoint: Vector2 = Vector2.ZERO
var has_checkpoint: bool = false
func set_checkpoint(pos: Vector2) -> void:
last_checkpoint = pos
has_checkpoint = true
func respawn(player: CharacterBody2D) -> void:
if has_checkpoint:
player.global_position = last_checkpoint
player.velocity = Vector2.ZERO
player.reset_health()
E no jogador, conectando o died ao respawn:
func _ready() -> void:
# ... resto do _ready
died.connect(_on_died)
func _on_died() -> void:
GameState.respawn(self)
func reset_health() -> void:
health = max_health
_invincible = false
_knockback = Vector2.ZERO
health_changed.emit(health, max_health)
Cada checkpoint na fase chama GameState.set_checkpoint(global_position) quando o jogador passa por ele. Assim, cair na lava nao volta o jogo do comeco, apenas do ultimo ponto seguro. Para um sistema de checkpoint mais robusto, com bandeiras que ativam e salvam progresso, vale conferir o material sobre checkpoint e respawn no Godot.
Ajustando os valores por @export
A grande vantagem desse desenho e que cada armadilha vira uma instancia configuravel. Voce arrasta a cena para a fase e, no Inspector, decide:
mode: INSTANT para espinho, PER_SECOND para lava ou gas.damage: quanto de vida cada golpe tira.knockback_force: o tamanho do empurrao. Espinhos costumam empurrar mais; lava as vezes nem precisa.tick_interval: so importa no modo por segundo, define a frequencia do dano.
Para o lado da vida, manter max_health e invincibility_time como @export no jogador deixa fácil balancear a dificuldade do jogo inteiro num lugar so. Se voce ja tem um sistema de vida e dano no Godot montado, basta plugar essas zonas de dano nele, porque a interface (o metodo take_damage) e a mesma.
Testando antes de soltar na fase
Antes de espalhar armadilhas pelo nivel, faça um teste isolado. Crie uma cena com o jogador e uma unica zona de lava. Verifique tres coisas: o dano por segundo respeita o tick_interval, o knockback realmente afasta o jogador (e nao o joga para dentro do perigo) e os i-frames impedem perda dupla de vida no mesmo instante. Depois adicione um espinho instantaneo ao lado e confirme que o mesmo script atende os dois casos sem ajustes no codigo.
Quando isso estiver redondo, o resto e level design. Voce ja tem uma peca reutilizavel que cobre espinho, lava, gas venenoso e qualquer outra zona de dano godot que apareca: muda os parametros, posiciona o CollisionShape2D e segue construindo a fase.


