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. 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.

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, 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.