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

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.
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:
- Sem defesa: dano cheio em
_apply_damage. - 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. - 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_successse 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.


