Voltar para o Blog
Quest Log

Como Fazer um Jogo de Luta: Hitbox, Frames e Combos na Prática

Dois lutadores 2D se enfrentando em um cenário de jogo de luta com barras de vida no topo da tela

Como fazer um jogo de luta 2D na Godot 4: hitbox e hurtbox, frames de ataque, hitstun, input buffer e máquina de estados em GDScript tipado, na prática.

Como Fazer um Jogo de Luta: Hitbox, Frames e Combos na Prática

Todo mundo que cresceu em Street Fighter ou Mortal Kombat já pensou em como fazer um jogo de luta próprio. E aí vem a surpresa: o gênero que parece simples de fora (dois bonecos e uma barra de vida) é um dos mais exigentes de programar bem. A boa notícia é que os conceitos por trás dele são poucos, têm nome e dá para aprender todos. Neste guia eu explico o vocabulário que define o gênero (hitbox, hurtbox, frames, hitstun, buffer, cancel) e implemento a base de um protótipo de luta 2D na Godot 4, com GDScript tipado e código que roda como está.

O objetivo não é um clone de Street Fighter, e sim um protótipo honesto: dois lutadores, um cenário, andar, socar, bloquear e tomar dano com a lógica de frames correta. Essa base é 80% do entendimento do gênero.

Por que jogo de luta é um dos gêneros mais difíceis de fazer bem

Três motivos, em ordem de dor.

Precisão de frames. Num jogo de luta, tudo é medido em frames a 60 por segundo. Um soco rápido fica ativo por 2 ou 3 frames, ou seja, 50 milissegundos. Se a sua detecção de acerto liga um frame atrasada, o jogo inteiro parece injusto.

Volume de animação. Cada golpe precisa de animação própria com poses claras de antecipação, impacto e recuperação. Um personagem de jogo de luta comercial tem 40, 50, 60 animações. Cada personagem novo multiplica o trabalho de arte, não só o de código.

Balanceamento e netcode. Dois jogadores humanos vão explorar qualquer número mal ajustado em minutos. E se você quiser partidas online, jogo de luta exige rollback netcode, uma das áreas mais difíceis da programação de jogos.

A resposta certa para tudo isso não é desistir, é cortar escopo: 2 personagens, 1 cenário, 2 golpes normais e 1 especial, versus local. Esse jogo cabe na sua agenda e ensina o gênero inteiro.

Os conceitos que definem o gênero

Antes do código, o vocabulário. Boa parte disso conversa com o que mostrei no guia de sistema de combate corpo a corpo, mas no jogo de luta esses conceitos viram a espinha dorsal do design.

Hitbox e hurtbox. A hitbox é a área que machuca: ela nasce e morre junto com o golpe, colada no punho ou na perna. A hurtbox é a área que pode ser machucada: ela envolve o corpo do lutador o tempo todo. O acerto acontece quando a hitbox de um sobrepõe a hurtbox do outro. Separar as duas permite golpes que desviam abaixando a hurtbox e ataques que vencem a troca com hitbox longa e hurtbox recuada.

Startup, active e recovery. Todo golpe tem três fases medidas em frames. Startup é a antecipação: o braço foi para trás, ainda não machuca. Active são os frames em que a hitbox está ligada e o golpe conecta. Recovery é a recuperação: o golpe já passou, o lutador está vulnerável voltando à guarda. Um soco rápido pode ser 4 de startup, 3 de active e 7 de recovery. Todo o balanceamento do gênero mora nesses três números.

Hitstun e blockstun. Quando um golpe acerta, a vítima fica travada por alguns frames sem poder agir: isso é hitstun. Se ela bloqueou, também trava, mas por menos tempo: blockstun. É essa diferença que cria a matemática de vantagem: se o seu golpe deixa o oponente em hitstun por mais frames do que o seu recovery, você age primeiro e pode emendar o próximo golpe. Combo é isso: uma sequência em que cada golpe conecta antes do hitstun do anterior acabar.

Input buffer. O jogador aperta o botão um pouco antes da hora certa, sempre. O buffer guarda esse comando por uma janela curta (10 a 15 frames) e executa assim que o personagem estiver livre. Sem buffer, comandos parecem engolidos e o especial de meia-lua vira loteria. É a melhoria de game feel mais barata que existe.

Cancels. Cancelar é interromper o recovery de um golpe com o startup de outro, normalmente um normal cancelado em especial. É o que faz combos clássicos funcionarem e dá profundidade ao sistema sem adicionar botões. No protótipo você não precisa dele no dia um, mas a máquina de estados que a gente vai montar já deixa a porta aberta.

Como fazer um jogo de luta na prática: a base na Godot 4

Abra um projeto 2D no Godot 4 e crie no Input Map as ações esquerda, direita, soco e defender. A cena de cada lutador fica assim:

Lutador (CharacterBody2D)
├── CollisionShape2D
├── Sprite2D (ou AnimatedSprite2D)
├── AnimationPlayer
├── Hitbox (Area2D)
│   └── CollisionShape2D
└── Hurtbox (Area2D)
    └── CollisionShape2D

A CollisionShape2D da raiz é o corpo físico (empurrão e chão). A Hurtbox cobre o tronco e fica no grupo hurtbox (aba Node > Groups). A Hitbox fica na frente do lutador, na altura do soco, e começa desligada. Nas camadas de física, coloque as hurtboxes numa layer própria e faça a mask da Hitbox apontar só para ela, assim golpe não colide com parede.

A máquina de estados do lutador

Lutador vive trocando de estado: parado, andando, atacando, bloqueando, tomando dano. Se você tentar controlar isso com uma pilha de booleanos, o código vira nó em uma semana. A solução é uma máquina de estados, o mesmo padrão que expliquei no guia de máquina de estados na Godot. Para o protótipo, um enum e um match resolvem:

class_name Lutador
extends CharacterBody2D

enum Estado { IDLE, WALK, ATTACK, HIT, BLOCK }

const VELOCIDADE: float = 220.0

var estado: Estado = Estado.IDLE
var vida: int = 100

@onready var animacao: AnimationPlayer = $AnimationPlayer

func _physics_process(_delta: float) -> void:
    match estado:
        Estado.IDLE, Estado.WALK, Estado.BLOCK:
            _processar_movimento()
        _:
            velocity.x = 0.0
    move_and_slide()

func _processar_movimento() -> void:
    if Input.is_action_pressed("defender"):
        estado = Estado.BLOCK
        velocity.x = 0.0
        return
    var direcao: float = Input.get_axis("esquerda", "direita")
    velocity.x = direcao * VELOCIDADE
    estado = Estado.WALK if direcao != 0.0 else Estado.IDLE

A regra de ouro: cada estado decide o que pode acontecer dentro dele. Atacando ou em hitstun, o input de movimento é ignorado por completo, e é isso que impede o clássico bug de andar no meio do soco.

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

O ataque controlado pelo AnimationPlayer

O golpe é um estado com começo, meio e fim, e quem dita esse tempo é o AnimationPlayer. Crie uma animação chamada soco com a duração do golpe inteiro (startup, active e recovery). O script entra no estado ATTACK, toca a animação e só devolve o controle quando ela termina:

func _ready() -> void:
    animacao.animation_finished.connect(_ao_terminar_animacao)

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("soco") and estado in [Estado.IDLE, Estado.WALK]:
        _iniciar_ataque()

func _iniciar_ataque() -> void:
    estado = Estado.ATTACK
    velocity.x = 0.0
    animacao.play(&"soco")

func _ao_terminar_animacao(nome: StringName) -> void:
    if nome == &"soco" and estado == Estado.ATTACK:
        estado = Estado.IDLE

Repare que o recovery já está implementado de graça: enquanto a animação não acaba, o estado segue ATTACK e o lutador não anda nem bloqueia. Punir golpe errado é exatamente isso.

A hitbox ligada e desligada por frame

Agora o truque central do gênero na Godot: a hitbox é uma Area2D que o próprio AnimationPlayer liga e desliga. Na animação soco, adicione um track de propriedade apontando para Hitbox:monitoring. Coloque uma chave com valor false no tempo zero, uma com true no início dos frames ativos (aos 4 frames, por exemplo) e outra com false no fim deles. Seus frames de startup e active ficam desenhados na timeline, ajustáveis sem tocar em código.

O script da Hitbox só precisa reagir ao acerto:

extends Area2D

@export var dano: int = 8
@export var frames_de_hitstun: int = 14

func _ready() -> void:
    monitoring = false
    area_entered.connect(_ao_acertar)

func _ao_acertar(alvo: Area2D) -> void:
    if alvo.is_in_group("hurtbox") and alvo.owner != owner:
        var oponente: Lutador = alvo.owner as Lutador
        if oponente != null:
            oponente.receber_golpe(dano, frames_de_hitstun)

A checagem alvo.owner != owner impede o lutador de acertar a própria hurtbox. Como dano e frames_de_hitstun são @export, cada golpe novo é só duplicar a Hitbox e ajustar números no Inspector.

Dano, hitstun e bloqueio no oponente

Falta o outro lado: receber o golpe. O método vive no script do Lutador e trata os dois casos, acerto limpo e bloqueio:

var frames_de_stun: int = 0

func receber_golpe(dano: int, frames_de_hitstun: int) -> void:
    if estado == Estado.BLOCK:
        vida -= int(dano * 0.15)
        frames_de_stun = int(frames_de_hitstun / 2.0)
    else:
        vida -= dano
        frames_de_stun = frames_de_hitstun
        estado = Estado.HIT

func _atualizar_stun() -> void:
    if frames_de_stun > 0:
        frames_de_stun -= 1
        if frames_de_stun == 0 and estado == Estado.HIT:
            estado = Estado.IDLE

Chame _atualizar_stun() na primeira linha do _physics_process e adicione if frames_de_stun > 0: return no topo de _processar_movimento, para ninguém sair andando durante o blockstun. Bloquear reduz o dano para 15% (o chip damage clássico) e corta o stun pela metade. Como o contador roda no passo de física, cada unidade é exatamente 1 frame a 60 por segundo. Quando vida chegar a zero, encerre o round do jeito que preferir: para o protótipo, recarregar a cena já serve.

Input buffer simples com timer

Por último, a melhoria que faz o protótipo parecer profissional. Em vez de exigir o botão no momento exato, guarde o pedido de soco por uma janela curta e consuma quando o lutador estiver livre. Substitua o _unhandled_input do passo do ataque por este:

const JANELA_DE_BUFFER: float = 0.15

var buffer_de_soco: float = 0.0

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("soco"):
        buffer_de_soco = JANELA_DE_BUFFER

func _consumir_buffer(delta: float) -> void:
    buffer_de_soco = maxf(buffer_de_soco - delta, 0.0)
    if buffer_de_soco > 0.0 and estado in [Estado.IDLE, Estado.WALK]:
        buffer_de_soco = 0.0
        _iniciar_ataque()

Chame _consumir_buffer(delta) no _physics_process (troque o _delta por delta). O efeito prático: o jogador aperta soco nos últimos frames do recovery e o golpe sai no primeiro frame livre, emendado. Isso já é a fundação de combo e de cancel: quando quiser permitir cancelar um normal em especial, é só deixar o buffer ser consumido também durante certos frames do estado ATTACK.

Para o especial de sequência (baixo, frente, soco), o princípio é o mesmo com um passo a mais: guarde os últimos inputs numa lista com o tempo de cada um e compare com a receita do golpe dentro da janela.

Balanceamento mínimo: dano, alcance e velocidade

Com dois golpes funcionando, o balanceamento do protótipo se resume a três eixos que se pagam entre si:

  • Dano: quanto tira de vida. Golpe forte tira mais.
  • Alcance: o tamanho e a posição da CollisionShape2D da hitbox. Quanto mais longe alcança, mais seguro é o golpe.
  • Velocidade: os frames de startup e recovery na timeline da animação. Quanto mais rápido sai e recupera, mais difícil é punir.

A regra é simples: nenhum golpe pode ganhar nos três eixos. O soco rápido tira 8 de vida, sai em 4 frames e tem alcance curto. O chute forte tira 18, alcança longe, mas leva 12 frames para sair e deixa você exposto se errar. Como tudo isso virou número em @export e chave na timeline, ajustar é rápido: jogue contra alguém, veja qual golpe está resolvendo todas as situações e taxe ele em um dos três eixos.

Escopo honesto: protótipo em semanas, jogo completo em anos

É aqui que a maioria dos projetos de jogo de luta morre. O que você montou neste guia (máquina de estados, hitbox por frame, hitstun, buffer) é um protótipo jogável. Com dedicação nas horas vagas, dois personagens com meia dúzia de golpes cada num cenário só é um projeto de semanas.

Um jogo de luta completo é outro animal: 8 ou mais personagens balanceados entre si, centenas de animações, modo treino com frame data, e principalmente netcode rollback, que exige que toda a simulação do seu jogo seja determinística e re-executável. Estúdios inteiros levam anos nisso. Não comece por aí.

O caminho certo: versus local, dois jogadores na mesma máquina. Duplique as ações no Input Map com sufixo (esquerda_p2, soco_p2), aponte cada evento para o dispositivo certo (teclado para um, controle para o outro) e faça o script do Lutador receber o prefixo das ações via @export var prefixo: String. Nada de servidor, nada de sincronização: o loop de diversão inteiro na sua mão.

Fechando

Recapitulando o caminho de como fazer um jogo de luta que funciona: escopo mínimo de 2 personagens e 1 cenário, os cinco conceitos que definem o gênero (hitbox e hurtbox, as três fases do golpe, hitstun e blockstun, buffer e cancel), e a implementação na Godot 4 com máquina de estados, hitbox como Area2D chaveada pelo AnimationPlayer, dano com stun contado em frames e input buffer com timer.

O próximo passo é abrir a engine e montar isso com dois retângulos coloridos antes de qualquer arte. Se travar em alguma base da engine no caminho, ou quiser aprender Godot de forma estruturada em vez de caçar peça por peça, vale investir nisso primeiro: o protótipo de luta é dez vezes mais rápido de montar quando os fundamentos estão sólidos. E quando o primeiro round terminar com alguém rindo no sofá, você entende por que tanta gente ama esse gênero.

Perguntas frequentes

É difícil fazer um jogo de luta?

É um dos gêneros mais difíceis de fazer bem, mas não de começar. O protótipo (dois personagens, um cenário, dois ou três golpes) usa conceitos simples: máquina de estados, Area2D como hitbox e um buffer de input. O que torna o gênero pesado é a escala: dezenas de golpes por personagem, balanceamento fino e netcode. Comece pelo protótipo e cresça só se fizer sentido.

Qual engine usar para fazer um jogo de luta?

Para um jogo de luta 2D próprio, a Godot 4 resolve muito bem: AnimationPlayer para ligar hitbox por frame, Area2D para detecção e física a 60 passos por segundo. Unity também serve, e existem bases prontas como o IKEMEN GO (herdeiro do M.U.G.E.N) se você quer montar lutadores sem programar a fundação. Para aprender de verdade, fazer o seu na Godot ensina mais.

O que é hitbox e hurtbox?

Hitbox é a área que causa dano: ela só existe nos frames ativos do golpe, colada no punho ou no pé do lutador. Hurtbox é a área que recebe dano: ela envolve o corpo o tempo todo. O golpe conecta quando a hitbox de um lutador sobrepõe a hurtbox do outro. Separar as duas é o que permite trocas justas, golpes que passam por baixo e ataques que vencem outros ataques.

Como funciona o input de um golpe especial?

O jogo guarda os últimos comandos do jogador por uma janela curta de tempo (o input buffer) e compara essa sequência com a receita do especial, tipo baixo, diagonal, frente e soco. Se a sequência aparece dentro da janela, o especial sai. Sem buffer, o jogador precisaria de precisão de frame perfeita e os comandos pareceriam engolidos. Uma janela de 0,15 a 0,25 segundo já muda tudo.

Dá para fazer um jogo de luta sozinho?

Um protótipo jogável com dois personagens no mesmo teclado, sim, e em semanas. Um jogo de luta comercial completo, com elenco grande, animação de qualidade, balanceamento sério e netcode rollback, é projeto de anos e raramente é trabalho de uma pessoa só. O caminho honesto: faça o protótipo local, valide se é divertido e só então pense em escalar.