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

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. Esse sistema de camadas merece um artigo próprio: o guia de collision layers e masks no Godot destrincha a matriz de colisão com exemplos além do combate.
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.
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:
- Hitbox e hurtbox são Area2D separadas, filhas do personagem, ao lado do
CharacterBody2D. Movimento e dano não se misturam. - 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.
- O sinal area_entered aplica o dano através de um método na hurtbox, que repassa ao sistema de vida do dono.
- 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, mostre números de dano flutuantes a cada acerto ou coloque 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. Um caminho comum de expansão é encadear ataques num sistema de combos, reaproveitando a mesma hitbox ligada por frame. 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.


