Voltar para o Blog
Quest Log

Hitbox e Hurtbox no Godot com Area2D: guia prático de combate

Diagrama de hitbox e hurtbox com Area2D em um personagem no Godot 4

Aprenda o padrão hitbox e hurtbox no Godot 4 com Area2D: configure collision layers e masks, conecte area_entered, aplique dano e evite hits duplicados.

Combate é onde muito projeto de ação ou RPG empaca. O movimento até funciona, mas na hora de fazer o golpe machucar o inimigo, tudo vira um emaranhado de condições e bugs estranhos. A solução limpa para isso tem nome: o padrão hitbox e hurtbox no Godot com Area2D. Neste tutorial você vai montar esse padrão do zero no Godot 4, configurar as collision layers e masks para as áreas se detectarem do jeito certo, conectar o sinal area_entered, aplicar dano de verdade e ainda evitar aquele dano duplicado que aparece quando menos se espera.

O que são hitbox e hurtbox

A ideia é simples e vale para praticamente qualquer jogo de combate 2D. Você tem duas áreas com papéis opostos:

  • A hitbox é a área que causa dano. Ela nasce junto de um ataque: a lâmina de uma espada, o punho de um soco, o corpo de um projétil. Quando está ativa, quer encostar em alguém para machucar.
  • A hurtbox é a área que recebe dano. Ela fica presa ao corpo do personagem e representa a parte vulnerável dele. Fica sempre ligada, esperando ser atingida.

O truque é que uma nunca faz o trabalho da outra. A hitbox não sabe aplicar dano em si mesma, e a hurtbox não sabe atacar. Elas só conversam quando a hitbox de um lado sobrepõe a hurtbox do outro. Manter esses papéis separados evita aquele nó em que um mesmo Area2D tenta atacar, defender e ainda decidir de quem é o dano.

Por que Area2D e não o CharacterBody2D

No Godot 4, o corpo do personagem costuma ser um CharacterBody2D. Ele resolve movimento e colisão física com o cenário: bater na parede, subir rampa, não atravessar o chão. Só que a física de colisão empurra objetos, e isso não é o que a gente quer no combate. Você não quer que a espada empurre o inimigo pelo mapa, quer que ela detecte o toque e aplique dano.

É aí que entra a Area2D. Ela detecta sobreposições e dispara sinais, mas não resolve física de contato: nada é empurrado. Por isso a hitbox e a hurtbox são sempre Area2D filhas do personagem, vivendo ao lado do CharacterBody2D sem se misturar com ele. O movimento fica de um lado, o dano do outro, e você ajusta um sem quebrar o outro.

Uma montagem típica de um jogador fica assim na árvore de cena:

# Player (CharacterBody2D)
# ├── Sprite2D
# ├── CollisionShape2D      # colisão física com o cenário
# ├── Hurtbox (Area2D)      # recebe dano
# │   └── CollisionShape2D
# └── Hitbox (Area2D)       # causa dano (ativada durante o ataque)
#     └── CollisionShape2D

Configurando collision layers e masks

Essa é a parte que mais gera confusão, então vale entender direito. Toda Area2D (e todo corpo físico) tem duas propriedades:

  • collision_layer: em quais camadas esse objeto está. É como ele se anuncia para o mundo.
  • collision_mask: quais camadas esse objeto procura. É o que ele consegue enxergar e detectar.

Duas áreas só se detectam quando a layer de uma bate com a mask da outra. Ou seja: a hitbox do jogador precisa ter, na sua mask, a layer onde vive a hurtbox do inimigo. Se isso não bater, nada acontece, mesmo com as formas se sobrepondo na tela.

Vamos definir um esquema de camadas. No Godot você pode nomear as layers em Project Settings, Layer Names, 2D Physics, o que deixa tudo mais legível:

  • Layer 1: world (cenário)
  • Layer 2: player_hurtbox
  • Layer 3: player_hitbox
  • Layer 4: enemy_hurtbox
  • Layer 5: enemy_hitbox

Com isso, a configuração da hitbox do jogador fica: ela está na layer player_hitbox e procura a layer enemy_hurtbox. A hurtbox do jogador faz o inverso: ela está na layer player_hurtbox e procura enemy_hitbox. Assim a espada do jogador nunca vê a própria hurtbox dele, só as dos inimigos.

Dá para configurar isso pelo Inspector, mas também dá para deixar explícito em código, o que ajuda a lembrar da intenção:

extends Area2D

# Hitbox do jogador
func _ready() -> void:
    # Está na camada 3 (player_hitbox)
    collision_layer = 1 << 2
    # Procura a camada 4 (enemy_hurtbox)
    collision_mask = 1 << 3

O 1 << 2 é só a forma de ligar o bit da terceira camada. Se preferir clareza no Inspector, marque as caixas na mão: o importante é que layer e mask do par batam. Um erro clássico é deixar a hitbox procurando a própria layer do jogador, o que faz o personagem se machucar sozinho no primeiro golpe.

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

Conectando o sinal area_entered e aplicando dano

O Area2D do Godot 4 emite o sinal real area_entered(area: Area2D) quando outra área entra na sua região, e body_entered(body: Node2D) quando um corpo físico entra. Como nosso alvo (a hurtbox) é uma Area2D, vamos usar area_entered.

A lógica limpa é esta: a hitbox escuta o próprio area_entered. Quando alguma hurtbox entra, ela pergunta a essa hurtbox para aplicar o dano. A hurtbox, por sua vez, sabe quem é o dono dela e repassa o dano para o sistema de vida.

Primeiro, a hurtbox. Ela é enxuta: só recebe o dano e avisa o dono.

extends Area2D
class_name Hurtbox

@export var owner_body: Node = null

func take_damage(amount: int) -> void:
    if owner_body != null and owner_body.has_method("apply_damage"):
        owner_body.apply_damage(amount)

Agora a hitbox. Ela guarda um valor de dano e, ao detectar uma hurtbox, chama o take_damage dela:

extends Area2D
class_name Hitbox

@export var dano: int = 10

func _ready() -> void:
    area_entered.connect(_on_area_entered)

func _on_area_entered(area: Area2D) -> void:
    if area is Hurtbox:
        var alvo: Hurtbox = area as Hurtbox
        alvo.take_damage(dano)

Repare no area is Hurtbox. Mesmo com as layers e masks corretas, checar o tipo é uma rede de segurança barata, e ainda deixa o código legível: só uma hurtbox de verdade toma dano. O apply_damage no corpo do inimigo é onde você desconta pontos de vida, que é assunto do seu sistema de vida e dano no Godot, então a hitbox nem precisa saber como a vida funciona por dentro.

Um exemplo de corpo de inimigo recebendo esse dano:

extends CharacterBody2D

var vida: int = 30

func apply_damage(amount: int) -> void:
    vida -= amount
    if vida <= 0:
        morrer()

func morrer() -> void:
    queue_free()

Evitando dano duplicado

Aqui mora o bug mais comum do padrão. O area_entered dispara uma vez por entrada, mas na prática você acaba tomando dano múltiplo por dois motivos: um golpe que fica com a hitbox ativa por vários frames enquanto o inimigo passa por ela, ou várias áreas de dano se sobrepondo no mesmo instante. O resultado é um inimigo que perde a vida inteira num piscar de olhos.

A forma mais robusta é a hitbox lembrar quem ela já atingiu no ataque atual. Você guarda os alvos numa lista e ignora repetição:

extends Area2D
class_name Hitbox

@export var dano: int = 10

var _ja_atingidos: Array[Hurtbox] = []

func _ready() -> void:
    area_entered.connect(_on_area_entered)

func _on_area_entered(area: Area2D) -> void:
    if area is Hurtbox:
        var alvo: Hurtbox = area as Hurtbox
        if alvo in _ja_atingidos:
            return
        _ja_atingidos.append(alvo)
        alvo.take_damage(dano)

# Chame isso no início de cada novo ataque
func iniciar_ataque() -> void:
    _ja_atingidos.clear()
    monitoring = true

func encerrar_ataque() -> void:
    monitoring = false

O monitoring é uma propriedade real da Area2D: quando false, a área para de detectar sobreposições. Ligar a hitbox só durante os frames do golpe e limpar a lista de atingidos a cada novo ataque resolve o problema por completo. Um inimigo entrou na área, tomou dano, e não toma de novo até o próximo golpe, mesmo que continue encostado na lâmina.

Se o seu ataque é um golpe rápido e único, dá para simplificar ainda mais: desative a hitbox no primeiro hit. Mas para golpes que devem acertar vários inimigos de uma vez (um giro de espada, por exemplo), a lista é o caminho, porque cada inimigo entra como uma hurtbox diferente.

Ligando ao ataque do personagem

Para fechar o ciclo, o CharacterBody2D do jogador controla quando a hitbox liga e desliga, geralmente sincronizado com a animação do golpe. Uma forma direta usando @onready:

extends CharacterBody2D

@onready var hitbox: Hitbox = $Hitbox

func atacar() -> void:
    hitbox.iniciar_ataque()
    await get_tree().create_timer(0.2).timeout
    hitbox.encerrar_ataque()

Na prática, o ideal é disparar iniciar_ataque e encerrar_ataque por meio de Animation Player, usando call method tracks nos frames exatos em que a espada está no ar. Isso amarra o dano ao visual e evita golpes que acertam antes de a animação começar. Esse encaixe entre animação, hitbox e input é justamente o que aprofundamos no tutorial de sistema de combate corpo a corpo.

Do lado do inimigo, o esquema é idêntico, só que espelhado nas layers: a hitbox dele procura a player_hurtbox e a hurtbox dele fica na enemy_hurtbox. Quando você acopla isso ao comportamento de perseguir e atacar da sua IA de inimigos no Godot, o combate ganha vida.

Resumo do padrão

Recapitulando o que faz esse sistema funcionar sem dor de cabeça:

  1. Hitbox e hurtbox são Area2D separadas, filhas do personagem, ao lado do CharacterBody2D. Movimento e dano não se misturam.
  2. Collision layers e masks fazem o casamento: a hitbox procura a layer da hurtbox inimiga, e nunca a própria. Layer é onde você está, mask é o que você enxerga.
  3. O sinal area_entered aplica o dano através de um método na hurtbox, que repassa ao sistema de vida do dono.
  4. Uma lista de já atingidos mais o controle de monitoring eliminam o dano duplicado.

Com esse esqueleto no lugar, expandir fica fácil: adicione knockback no apply_damage ou frames de invencibilidade desligando a hurtbox por um instante após o hit. A base continua a mesma, e é ela que separa um combate confiável de um sistema cheio de bugs. No CursoGame.Dev a gente sempre volta a esse padrão porque ele escala do primeiro protótipo até um jogo de ação completo.

Perguntas frequentes

Qual a diferença entre hitbox e hurtbox?

A hitbox é a área que causa dano, normalmente ligada a um ataque ou projétil. A hurtbox é a área que recebe dano, geralmente presa ao corpo do personagem. Separar as duas deixa o combate mais claro e fácil de ajustar.

Por que usar Area2D em vez do próprio CharacterBody2D?

O CharacterBody2D cuida do movimento e da colisão física com o cenário. A Area2D serve para detectar sobreposições sem empurrar nada. Usar Area2D separada mantém dano e movimento independentes, então você muda o alcance de um ataque sem mexer na física do personagem.

Como faço a hitbox atingir a hurtbox e não o próprio dono?

Use collision layers e masks. Coloque a hitbox e a hurtbox em layers diferentes e configure a mask de cada uma para enxergar só a layer do alvo. Assim a hitbox do jogador olha para a layer dos inimigos, e nunca para a dele mesmo.

Por que meu inimigo toma dano várias vezes com um só golpe?

O sinal area_entered pode disparar em frames seguidos enquanto as áreas ficam sobrepostas ou por causa de várias áreas. Guarde quem já foi atingido em uma lista, ou desative a hitbox logo após o primeiro hit, para aplicar o dano uma única vez.

Devo usar area_entered ou body_entered?

Depende de como você modelou o alvo. Se a parte que recebe dano é uma Area2D (a hurtbox), use area_entered. Se você quer detectar diretamente um CharacterBody2D ou StaticBody2D, use body_entered. Manter tudo como Area2D costuma ser mais previsível.