Voltar para o Blog
Quest Log

Knockback e invencibilidade ao tomar dano no Godot

Personagem de jogo sendo empurrado para trás por um golpe enquanto pisca em branco

Aprenda knockback godot na prática: empurrão na direção oposta ao dano, i-frames com piscada via modulate, fim do stunlock e código GDScript do Godot 4.

Knockback e invencibilidade ao tomar dano no Godot

Tomar dano num jogo bom é uma experiência completa: o personagem voa pra trás, pisca, fica intocável por um instante e o jogador sente o golpe. Tomar dano num jogo cru é um número descendo na barra de vida e mais nada. A diferença entre os dois é o que vou montar aqui: knockback godot do jeito certo, com empurrão na direção oposta ao dano, i-frames com piscada e zero stunlock. Tudo em GDScript do Godot 4.x.

E aviso logo: o erro mais comum nesse tema não é de matemática, é de arquitetura. Quem mistura a velocidade do knockback com a velocidade de input do jogador acaba com um personagem que cancela o empurrão andando pra frente, ou pior, que trava num canto sendo espancado sem chance de reagir. A solução é separar as duas coisas desde o início, e é por aí que eu começo.

Separando knockback do movimento normal

Num CharacterBody2D típico, o movimento vive em _physics_process lendo input e escrevendo em velocity. Se o knockback escrever na mesma velocity, o frame seguinte de input sobrescreve tudo e o empurrão morre antes de nascer. A saída é manter duas velocidades e somar na hora de mover:

extends CharacterBody2D

const SPEED = 200.0
const FRICTION = 900.0

var knockback = Vector2.ZERO

func _physics_process(delta):
    var input_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")

    # O knockback decai sozinho, frame a frame.
    knockback = knockback.move_toward(Vector2.ZERO, FRICTION * delta)

    velocity = input_dir * SPEED + knockback
    move_and_slide()

O move_toward faz o empurrão perder força num ritmo constante até zerar, o que dá aquela desaceleração seca de jogo de ação. Enquanto o knockback está forte, ele domina o movimento; conforme decai, o controle volta pra mão do jogador de forma natural, sem precisar travar input em momento nenhum nos casos leves.

Se você prefere um decaimento mais suave no fim, troque por lerp:

knockback = knockback.lerp(Vector2.ZERO, 10.0 * delta)

O lerp perde força proporcionalmente, então o golpe começa violento e termina escorregando. Questão de feel: move_toward pra impacto seco, lerp pra deslizada. Eu uso o primeiro em jogos top-down e o segundo quando o personagem está no gelo ou tem peso.

A direção oposta ao dano

O empurrão certo afasta a vítima da fonte do dano. A conta é uma subtração de posições normalizada:

func take_damage(amount: int, source_position: Vector2):
    var direction = (global_position - source_position).normalized()
    knockback = direction * 350.0

global_position - source_position aponta da fonte do golpe pra vítima, exatamente a direção de fuga. O normalized() garante comprimento 1, e o multiplicador define a força. Sem o normalize, um inimigo encostado empurraria fraco e um distante empurraria forte, o contrário do que faz sentido.

Quem chama esse método é o atacante, normalmente na hora da colisão. Num inimigo de contato com Area2D:

extends Area2D

@export var damage = 10

func _ready():
    body_entered.connect(_on_body_entered)

func _on_body_entered(body):
    if body.has_method("take_damage"):
        body.take_damage(damage, global_position)

O has_method deixa qualquer corpo virar alvo válido sem checagem de tipo: player, caixote, outro inimigo. Esse contrato simples (quem leva dano expõe take_damage, quem causa dano só chama) é o mesmo que eu uso no sistema de vida e dano no Godot, e o knockback entra nele sem mudar nada da estrutura.

Um caso especial: ataque de projétil rápido ou hitbox de espada larga. Usar a posição do projétil funciona, mas em golpes corpo a corpo fica melhor usar a posição de quem segura a arma, não da hitbox, senão um golpe que acerta por trás do ombro empurra pro lado errado. Passe owner.global_position em vez do global_position da Area2D quando a hitbox pertence a um personagem, como nas hitboxes do combate corpo a corpo.

I-frames: a janela de invencibilidade

Knockback sem invencibilidade é meio caminho. Se o inimigo continua encostado, o body_entered dispara de novo no frame seguinte e a vida derrete. A janela de invencibilidade (os i-frames) resolve: depois de tomar dano, o personagem fica intocável por uma fração de segundo.

A versão mínima é uma flag com timer descartável:

var invincible = false

func take_damage(amount: int, source_position: Vector2):
    if invincible:
        return

    health -= amount
    var direction = (global_position - source_position).normalized()
    knockback = direction * 350.0

    invincible = true
    await get_tree().create_timer(0.6).timeout
    invincible = false

O return no topo descarta qualquer dano enquanto a janela está aberta. O await segura a função por 0.6 segundo e reabre. Simples e suficiente pra maioria dos jogos.

Um detalhe de robustez: se o personagem pode morrer e ser liberado da cena durante a espera, o código depois do await roda sobre um node morto. Proteja o fim da função:

    invincible = true
    await get_tree().create_timer(0.6).timeout
    if is_instance_valid(self) and is_inside_tree():
        invincible = false

Pra um player que nunca sai da cena isso é paranoia, mas pra inimigos que tomam knockback e podem morrer no meio da janela, é o que evita erro no console.

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

A piscada com modulate

A invencibilidade precisa ser visível, senão o jogador não entende por que o segundo golpe não entrou. O clássico é a piscada, e o jeito mais direto no Godot 4 é animar o modulate do sprite com Tween:

@onready var sprite = $Sprite2D

func start_blink():
    var tween = create_tween()
    tween.set_loops(6)
    tween.tween_property(sprite, "modulate:a", 0.3, 0.05)
    tween.tween_property(sprite, "modulate:a", 1.0, 0.05)

Cada loop apaga e acende o alpha em 0.1 segundo; seis loops cobrem os 0.6 segundo da janela. Chame start_blink() dentro do take_damage, logo depois de ligar a flag, e o feedback visual fica colado na mecânica.

Importante: a duração da piscada deve bater com a duração dos i-frames. Se a piscada termina antes, o jogador acha que pode ser atingido e não pode; se termina depois, ele acha que está protegido e toma dano. Eu defino as duas a partir da mesma constante:

const INVINCIBILITY_TIME = 0.6
const BLINK_INTERVAL = 0.1

func start_blink():
    var tween = create_tween()
    tween.set_loops(int(INVINCIBILITY_TIME / BLINK_INTERVAL))
    tween.tween_property(sprite, "modulate:a", 0.3, BLINK_INTERVAL / 2.0)
    tween.tween_property(sprite, "modulate:a", 1.0, BLINK_INTERVAL / 2.0)

Se em vez de transparência você quiser o flash branco de jogo de 16 bits, o modulate puro não alcança, porque ele só multiplica cores (multiplicar por branco não muda nada). Aí entra um shader mínimo no material do sprite:

shader_type canvas_item;

uniform float flash_amount : hint_range(0.0, 1.0) = 0.0;

void fragment() {
    vec4 tex = texture(TEXTURE, UV);
    COLOR = vec4(mix(tex.rgb, vec3(1.0), flash_amount), tex.a);
}

E no GDScript, um pulso rápido no uniform:

func flash_white():
    sprite.material.set_shader_parameter("flash_amount", 1.0)
    var tween = create_tween()
    tween.tween_property(sprite.material, "shader_parameter/flash_amount", 0.0, 0.15)

Eu costumo combinar os dois: flash branco no frame do impacto, piscada de alpha durante o resto da janela. O flash comunica "você foi atingido agora" e a piscada comunica "você está protegido até parar de piscar".

Evitando stunlock

Stunlock é quando o jogador toma um golpe, o knockback joga ele em cima de outro inimigo, que dá outro golpe, e assim até morrer sem nunca recuperar o controle. É a morte mais injusta que existe e quase sempre nasce de três descuidos:

I-frames curtos demais ou ausentes. Se a janela é menor que o tempo de o knockback afastar o personagem do inimigo, o segundo hit entra antes de o jogador respirar. Regra de bolso que me serve bem: a invencibilidade deve durar mais que o decaimento do knockback. Com empurrão de 350 e fricção de 900, o knockback zera em uns 0.4 segundo, então 0.6 de i-frames dá folga.

Knockback que empilha. Se cada hit soma no vetor existente (knockback += direction * force), dois inimigos em lados opostos se cancelam e o personagem fica parado apanhando. Sempre sobrescreva: knockback = direction * force. O golpe mais recente dita a direção, ponto.

Input travado durante o empurrão inteiro. Alguns jogos congelam o controle enquanto o knockback dura. Pode funcionar em jogo de luta, mas em action RPG vira frustração. Com a soma de velocidades da primeira seção, o jogador já recupera influência gradualmente conforme o empurrão decai, sem nenhum estado de "stun" explícito. Se você quiser um hitstun de verdade, faça ele bem mais curto que os i-frames, tipo 0.15 segundo, só pra vender o impacto.

Tem ainda o caso do inimigo que também leva knockback. Aí vale o mesmo sistema, com um ajuste: dê a inimigos pesados um multiplicador de resistência (knockback = direction * force * (1.0 - knockback_resistance)) e exporte com @export var knockback_resistance = 0.0. Chefe com resistência 1.0 não se move, capanga com 0.0 voa, e tudo usa o mesmo código.

O script completo

Juntando tudo num player top-down:

extends CharacterBody2D

const SPEED = 200.0
const FRICTION = 900.0
const KNOCKBACK_FORCE = 350.0
const INVINCIBILITY_TIME = 0.6

@onready var sprite = $Sprite2D

var health = 100
var knockback = Vector2.ZERO
var invincible = false

func _physics_process(delta):
    var input_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    knockback = knockback.move_toward(Vector2.ZERO, FRICTION * delta)
    velocity = input_dir * SPEED + knockback
    move_and_slide()

func take_damage(amount: int, source_position: Vector2):
    if invincible:
        return

    health -= amount
    knockback = (global_position - source_position).normalized() * KNOCKBACK_FORCE
    start_blink()

    if health <= 0:
        die()
        return

    invincible = true
    await get_tree().create_timer(INVINCIBILITY_TIME).timeout
    invincible = false

func start_blink():
    var tween = create_tween()
    tween.set_loops(6)
    tween.tween_property(sprite, "modulate:a", 0.3, 0.05)
    tween.tween_property(sprite, "modulate:a", 1.0, 0.05)

func die():
    queue_free()

Quarenta linhas, e o dano deixou de ser um número silencioso. O ponto que eu quero que fique: knockback é uma variável separada que decai sozinha, invincible é uma janela que descarta dano repetido, e a piscada dura exatamente o que a janela dura.

Fechando

Knockback no Godot se resume a três decisões: velocidade de empurrão separada da velocidade de input, direção calculada como vítima menos fonte, e i-frames mais longos que o decaimento do empurrão. A piscada com modulate fecha o ciclo de feedback, e o shader de flash entra quando você quiser o toque retrô.

Pra testar de verdade, ponha o player numa sala com três inimigos de contato e tente morrer encurralado num canto. Se você sempre consegue escapar entre um golpe e outro, o sistema está calibrado. Se morre preso, mexa primeiro na duração dos i-frames, depois na força do knockback, nessa ordem. É um ajuste de minutos que separa um combate frustrante de um combate justo.