Voltar para o Blog
Quest Log

Zonas de Dano (Espinhos e Lava) no Godot 4

Personagem 2D pulando sobre espinhos e poca de lava em um nivel de plataforma

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)
  • Sprite2D ou AnimatedSprite2D (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.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

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.