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.

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.

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

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.