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.

Ferramenta:Godot

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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 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.