Voltar para o Blog
Quest Log

Parry e Bloqueio na Godot 4: Defesa com Escudo e Parry Perfeito

Cavaleiro de pixel art bloqueando um golpe com escudo enquanto uma faísca de parry perfeito brilha no ponto de impacto

Parry e bloqueio na Godot 4: escudo com stamina, janela de parry com Timer, guard break e riposte em GDScript tipado. Tutorial 2D completo passo a passo.

Ferramenta:Godot

Parry na Godot 4 é uma daquelas mecânicas que parecem mágica quando funcionam e viram frustração quando o timing está errado por um frame. A boa notícia: o sistema inteiro, bloqueio com escudo, janela de parry perfeito, guard break e riposte, cabe em uma state machine pequena, um node Timer e uma função take_damage bem pensada. Neste tutorial eu monto tudo isso em GDScript tipado para um jogo 2D, no estilo que você espera de um souls-like: defesa que custa stamina, parry que recompensa timing e punição clara para quem erra.

Se você ainda não tem golpes, hitbox e hurtbox funcionando, comece pelo sistema de combate corpo a corpo. Aqui eu assumo que o inimigo já ataca e o player já tem um script de vida.

Bloquear e dar parry são decisões de design diferentes

Antes do código, a distinção que define tudo:

  • Bloqueio: o jogador segura o botão. Não exige timing. Em troca, o dano só é reduzido (ou anulado, dependendo do jogo) e cada golpe bloqueado drena stamina. É a opção segura e barata de executar, cara em recurso.
  • Parry: o jogador aperta o botão no momento exato. A janela é curta, algo entre 0.1s e 0.25s. Se o golpe inimigo chega dentro dela, o dano é zero e o atacante fica atordoado, aberto para um riposte. É a opção arriscada com a maior recompensa.

O truque de usabilidade que quase todo jogo de ação usa: os dois ficam no mesmo botão. Apertar abre a janela de parry; se o golpe não chegar a tempo e o botão continuar pressionado, o personagem desce para o bloqueio normal. O jogador nunca é punido por tentar o parry e falhar no timing, ele só recebe a recompensa menor. Isso mantém a mecânica convidativa sem tirar o teto de habilidade.

Os estados do defensor: enum tipado e state machine

O defensor precisa de quatro estados, e um enum tipado deixa isso explícito e barato de conferir:

extends CharacterBody2D
class_name Player

enum State { IDLE, BLOCKING, PARRY_WINDOW, STAGGERED }

const PARRY_WINDOW_TIME: float = 0.15
const BLOCK_DAMAGE_MULTIPLIER: float = 0.2
const STAMINA_COST_PER_DAMAGE: float = 1.5
const STAGGER_TIME: float = 1.2

var state: State = State.IDLE
var health: int = 100
var stamina: float = 100.0
var max_stamina: float = 100.0

@onready var parry_timer: Timer = $ParryTimer
@onready var stagger_timer: Timer = $StaggerTimer

func _ready() -> void:
    parry_timer.one_shot = true
    parry_timer.timeout.connect(_on_parry_timer_timeout)
    stagger_timer.one_shot = true
    stagger_timer.timeout.connect(_on_stagger_timer_timeout)

IDLE é o estado neutro (na prática você teria também mover, atacar etc., mas para este tutorial o que importa é a defesa). PARRY_WINDOW é o intervalo curtíssimo logo após apertar o botão. BLOCKING é a guarda mantida. STAGGERED é o personagem desguardado após um guard break, sem poder agir.

Adicione dois nodes Timer como filhos do player na cena (ParryTimer e StaggerTimer). Usar Timer em vez de contar delta na mão simplifica o código e evita bugs de acumulador.

A transição de entrada e saída da defesa fica no _physics_process:

func _physics_process(_delta: float) -> void:
    match state:
        State.IDLE:
            if Input.is_action_just_pressed("block"):
                _start_defense()
        State.BLOCKING:
            if not Input.is_action_pressed("block"):
                state = State.IDLE
        State.PARRY_WINDOW, State.STAGGERED:
            pass

func _start_defense() -> void:
    state = State.PARRY_WINDOW
    parry_timer.start(PARRY_WINDOW_TIME)

func _on_parry_timer_timeout() -> void:
    if state != State.PARRY_WINDOW:
        return
    if Input.is_action_pressed("block"):
        state = State.BLOCKING
    else:
        state = State.IDLE

func _on_stagger_timer_timeout() -> void:
    if state == State.STAGGERED:
        state = State.IDLE

Leia o fluxo com calma porque é aqui que a mecânica nasce. Apertou o botão: PARRY_WINDOW por 0.15s. O Timer estourou e o botão continua pressionado: vira BLOCKING. O botão foi solto antes do timeout: volta para IDLE. Durante PARRY_WINDOW e STAGGERED o jogador não controla nada, quem manda são os Timers. Lembre de criar a ação block no Input Map (Projeto, Configurações do Projeto, aba Input Map).

Como o parry funciona na Godot 4: o ataque consulta o estado do defensor

Essa é a decisão de arquitetura mais importante do sistema: o atacante nunca decide o resultado do golpe. Ele só entrega o dano e uma referência de quem atacou. Quem resolve o que acontece é o defensor, dentro de take_damage, consultando o próprio estado:

func take_damage(amount: int, attacker: Node2D) -> void:
    match state:
        State.PARRY_WINDOW:
            _on_parry_success(attacker)
        State.BLOCKING:
            _on_block(amount)
        _:
            _apply_damage(amount)

func _on_parry_success(attacker: Node2D) -> void:
    parry_timer.stop()
    state = State.IDLE
    if attacker.has_method("stagger"):
        attacker.stagger()
    _play_parry_feedback()

func _apply_damage(amount: int) -> void:
    health -= amount
    if health <= 0:
        queue_free()

Do lado do inimigo, o golpe é uma Area2D de hitbox que, ao encostar, chama a função:

# No script do inimigo
@export var attack_damage: int = 20

func _on_hitbox_body_entered(body: Node2D) -> void:
    if body.has_method("take_damage"):
        body.take_damage(attack_damage, self)

Passar self como attacker é o que permite o riposte: no parry bem sucedido, o defensor chama attacker.stagger() e o inimigo congela, vulnerável. Uma versão mínima de stagger no inimigo:

# No script do inimigo
var can_act: bool = true

func stagger() -> void:
    can_act = false
    # Cancele aqui a animação de ataque em andamento
    await get_tree().create_timer(1.5).timeout
    can_act = true

Durante esses 1.5s o seu ataque normal pode aplicar dano dobrado no inimigo atordoado, e pronto: você tem o loop de parry e riposte que sustenta jogos inteiros. Se a ideia é levar isso adiante, o guia de como fazer um souls-like na Godot mostra onde esse sistema se encaixa no projeto completo, e o par natural do parry no combate 3D é o lock-on que trava a câmera no alvo.

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

Dano reduzido, dano zero e o custo de stamina do bloqueio

Os três resultados possíveis de um golpe defendido, do pior para o melhor:

  1. Sem defesa: dano cheio em _apply_damage.
  2. Bloqueio: dano multiplicado por BLOCK_DAMAGE_MULTIPLIER (0.2 no nosso caso, ou seja, 80% de redução) e stamina descontada. Se quiser escudo que anula tudo, use multiplicador 0.0, mas aí a stamina precisa doer mais para o bloqueio não virar resposta universal.
  3. Parry: dano zero, atacante atordoado. Alguns jogos ainda refletem parte do dano de volta (projéteis rebatidos são o exemplo clássico): basta chamar attacker.take_damage() dentro de _on_parry_success se o seu design pedir.

O bloqueio com custo e guard break:

func _on_block(amount: int) -> void:
    var cost: float = amount * STAMINA_COST_PER_DAMAGE
    if stamina < cost:
        _guard_break(amount)
        return
    stamina -= cost
    var reduced: int = int(round(amount * BLOCK_DAMAGE_MULTIPLIER))
    _apply_damage(reduced)

func _guard_break(amount: int) -> void:
    stamina = 0.0
    state = State.STAGGERED
    stagger_timer.start(STAGGER_TIME)
    _apply_damage(amount)

O custo escala com o dano do golpe: golpe pesado come mais barra. Quando a stamina não cobre o custo, a guarda quebra: o personagem entra em STAGGERED, fica 1.2s sem poder agir e ainda toma o dano cheio do golpe. É a mesma punição que o inimigo sofre no parry, só que invertida, e é ela que impede o jogador de segurar o bloqueio para sempre. A regeneração da barra (com delay após o gasto e estado de exaustão) eu cubro em detalhe no tutorial de sistema de stamina na Godot; o componente de lá conversa direto com este script, basta trocar as variáveis soltas pela API do node.

Feedback: hitstop, som e flash vendem o parry

Mecanicamente o sistema já funciona. Mas parry sem feedback parece bug: o golpe encosta e nada acontece. Três efeitos baratos resolvem:

@onready var parry_sound: AudioStreamPlayer2D = $ParrySound
@onready var sprite: Sprite2D = $Sprite2D

func _play_parry_feedback() -> void:
    parry_sound.play()
    _flash()
    _hitstop(0.08)

func _flash() -> void:
    sprite.modulate = Color(3.0, 3.0, 3.0)
    var tween: Tween = create_tween()
    tween.tween_property(sprite, "modulate", Color.WHITE, 0.15)

func _hitstop(duration: float) -> void:
    Engine.time_scale = 0.05
    await get_tree().create_timer(duration, true, false, true).timeout
    Engine.time_scale = 1.0

O hitstop congela o jogo por um instante reduzindo Engine.time_scale quase a zero. O detalhe crítico é o quarto argumento de create_timer: ignore_time_scale = true. Sem ele, o timer que deveria restaurar a velocidade também roda em câmera lenta e o jogo fica travado por segundos. Com 0.08s de congelamento real, o impacto fica tátil sem atrapalhar o ritmo.

O flash multiplica o modulate do sprite acima de 1.0 (funciona melhor com um material que permita estourar o branco, mas mesmo o modulate puro já lê como impacto) e um Tween traz de volta ao normal. O som de metal agudo é o que o cérebro do jogador vai associar ao acerto: escolha um efeito curto e distinto do som de bloqueio normal, que deve ser mais grave e abafado.

Balanceamento: o tamanho da janela define o jogo

Toda a dificuldade da mecânica mora em PARRY_WINDOW_TIME, e a regra é simples: janela mais longa, parry mais fácil. Referências práticas:

  • 0.1s (6 frames a 60fps): exigente. O jogador precisa ler a animação do inimigo e reagir à antecipação, não ao golpe. Combina com jogo de nicho para quem gosta de punição.
  • 0.15s (9 frames): o ponto de partida que eu recomendo para indie 2D. Difícil o bastante para o acerto ter gosto de conquista, tolerante o bastante para não afastar ninguém.
  • 0.2s a 0.25s (12 a 15 frames): acessível. O jogador acerta com frequência e se sente habilidoso. Se o seu jogo empurra o parry como mecânica central, com inimigos atacando o tempo todo, fique nessa faixa.

Dois ajustes finos que valem o teste. Primeiro, a telegrafia do inimigo: a janela só é justa se a animação de antecipação do ataque durar mais que ela; um golpe que sai em 0.2s com janela de 0.15s vira adivinhação. Segundo, o custo de errar: alguns jogos adicionam um lag de recuperação se a janela passa em branco e o botão já foi solto, para punir o spam de parry. Comece sem esse lag (o nosso código simplesmente desce para bloqueio ou idle) e só adicione se o playtest mostrar jogador apertando o botão sem parar.

Com estados tipados, Timer controlando a janela e o take_damage centralizando a decisão, o sistema fica pequeno, previsível e fácil de estender: novos tipos de golpe (imblocável, quebra-guarda garantido) viram só mais um match dentro da mesma função. Teste a janela com gente de fora, ajuste uma constante e o jogo inteiro muda de personalidade.

Perguntas frequentes

Qual a diferença entre bloqueio e parry num jogo de ação?

Bloqueio é segurar o botão: reduz ou anula o dano em troca de stamina, sem exigir timing. Parry é apertar o botão no instante certo: uma janela curta (0.1s a 0.25s) que, se acertada, zera o dano e atordoa o atacante, abrindo espaço para um contra-ataque.

Como criar a janela de parry na Godot 4?

Use um node Timer com one_shot ativado. Ao apertar o botão de defesa, o personagem entra no estado PARRY_WINDOW e o Timer inicia com 0.15s. Se o golpe chegar antes do timeout, é parry. No timeout, o estado vira BLOCKING se o botão seguir pressionado, ou IDLE se foi solto.

Quem decide se o golpe foi bloqueado: o atacante ou o defensor?

O defensor. O ataque inimigo sempre chama take_damage no alvo, e é dentro de take_damage que o defensor consulta o próprio estado: em PARRY_WINDOW ele atordoa o atacante, em BLOCKING aplica dano reduzido e gasta stamina, e em qualquer outro estado toma o dano cheio.

Como funciona o guard break quando a stamina zera?

Antes de descontar o custo do bloqueio, cheque se a stamina cobre o valor. Se não cobrir, o personagem entra no estado STAGGERED por um tempo fixo (1s a 1.5s), fica sem poder agir e toma o dano do golpe. Isso pune quem bloqueia tudo sem administrar o recurso.

Qual o tamanho ideal da janela de parry?

Depende do público. 0.1s é exigente, estilo souls-like clássico. 0.15s é um bom padrão para jogos indie 2D. 0.2s a 0.25s deixa a mecânica acessível e faz o jogador se sentir habilidoso com frequência. Como a janela é uma constante no código, teste os valores em playtest.

Preciso pausar o jogo inteiro para fazer o hitstop do parry?

Não. Reduza Engine.time_scale para algo como 0.05 e use get_tree().create_timer com ignore_time_scale ativado para restaurar a escala depois de 0.05s a 0.1s reais. O congelamento curto vende o impacto sem travar UI nem áudio.