Game Feel e Juice: Como Deixar o Jogo Gostoso de Jogar

Game feel e juice na prática: screen shake, hit stop, squash and stretch e feedback visual no Godot 4 para deixar seu jogo gostoso de jogar.
Game Feel e Juice: Como Deixar o Jogo Gostoso de Jogar
Game feel é a diferença entre um jogo que funciona e um jogo que é gostoso de jogar. A mecânica pode estar matematicamente idêntica em dois protótipos: mesmo dano, mesma velocidade, mesma colisão. Um parece uma planilha com sprites. O outro parece vivo. O que muda é o feedback: o tremor da tela no impacto, a pausa de meio décimo de segundo quando o golpe conecta, o inimigo que pisca branco antes de morrer.
A comunidade chama esse conjunto de técnicas de "juice". E a boa notícia é que juice é barato. Não exige arte nova, não exige redesenhar mecânica. São camadas de feedback que você empilha em cima do que já existe, quase sempre com poucas linhas de código. Nesse artigo eu mostro as quatro técnicas que dão mais retorno por esforço, com código GDScript de Godot 4 que roda como está, e o teste de antes e depois pra você sentir cada uma.
O que é game feel, na prática
A definição que eu uso: game feel é tudo que o jogo responde de volta quando o jogador faz algo. Apertou o botão de ataque? O jogo precisa confirmar que recebeu (animação que dispara na hora), confirmar que aconteceu (o golpe saiu) e confirmar que importou (o impacto teve peso).
Quando falta esse retorno, o jogador descreve o jogo com palavras vagas: "travado", "mole", "sem graça". Ele raramente sabe apontar o motivo, porque o motivo é ausência. Não tem nada errado na tela, tem algo faltando nela.
O caminho contrário também existe: dá pra exagerar. Tela que treme a cada passo cansa em dois minutos. Juice é tempero, e a régua é simples: o tamanho do feedback acompanha o tamanho do evento. Pulo comum, feedback discreto. Golpe crítico que mata o chefe, festa completa.
Vamos às técnicas, na ordem que eu adicionaria num projeto real.
Screen shake: o tremor que dá peso
Screen shake é a técnica mais conhecida de juice e a mais fácil de fazer errado. O erro clássico é tremer a câmera com intensidade fixa por tempo fixo: fica mecânico e, pior, dois impactos seguidos não somam.
O jeito que resolve isso é o sistema de trauma. Em vez de "tremer por X segundos", cada evento adiciona trauma (um valor de 0 a 1), e o trauma decai sozinho com o tempo. A intensidade do tremor é o trauma elevado ao quadrado, então tremores fracos são bem fracos e tremores fortes são dramáticos. Impactos seguidos acumulam naturalmente.
extends Camera2D
var trauma := 0.0
const TRAUMA_DECAY = 2.5 # quanto de trauma decai por segundo
const MAX_OFFSET = 14.0 # deslocamento máximo em pixels
func add_trauma(amount: float):
trauma = clamp(trauma + amount, 0.0, 1.0)
func _process(delta):
if trauma > 0.0:
trauma = maxf(trauma - TRAUMA_DECAY * delta, 0.0)
# Elevar ao quadrado deixa a curva mais expressiva:
# trauma baixo quase não treme, trauma alto treme muito.
var shake = trauma * trauma
offset = Vector2(
randf_range(-1.0, 1.0) * MAX_OFFSET * shake,
randf_range(-1.0, 1.0) * MAX_OFFSET * shake
)
else:
offset = Vector2.ZERO
Uso: camera.add_trauma(0.3) num acerto comum, camera.add_trauma(0.6) numa explosão. A câmera volta ao normal sozinha.
Antes e depois: acerte um inimigo sem shake e depois com 0.3 de trauma. Sem o tremor, o golpe parece passar através do inimigo. Com ele, parece que conectou em algo sólido. Mesma colisão, mesma matemática, sensação completamente diferente.
Uma nota de acessibilidade que vale levar a sério: screen shake enjoa parte dos jogadores. Coloque um slider de intensidade nas opções (multiplicar MAX_OFFSET por um fator de 0 a 1 resolve). Jogos grandes fazem isso por bom motivo.
Hit stop: a pausa que vende o impacto
Hit stop (ou hit pause, ou freeze frame) é congelar o jogo por uma fração de segundo no instante do impacto. É a técnica favorita dos jogos de luta: quando o soco conecta, tudo para por alguns frames. O cérebro lê essa pausa como peso.
O detalhe contraintuitivo é a duração. Estamos falando de algo entre 0.05 e 0.15 segundos. Se você consegue perceber conscientemente a pausa, ela provavelmente está longa demais. O efeito funciona abaixo do nível da percepção: o jogador não vê a pausa, sente o impacto.
No Godot, o jeito mais direto é mexer no Engine.time_scale:
func hit_stop(duration := 0.08, scale := 0.05):
Engine.time_scale = scale
# O quarto argumento (ignore_time_scale = true) é essencial:
# sem ele o timer também fica lento e a pausa dura 20x mais.
await get_tree().create_timer(duration, true, false, true).timeout
Engine.time_scale = 1.0
Repare que eu uso scale = 0.05 em vez de zero. Congelar a 5% da velocidade em vez de parar completamente fica mais orgânico: partículas continuam se arrastando devagar, e o retorno pra velocidade normal não dá tranco.
Antes e depois: o teste clássico é um ataque corpo a corpo. Sem hit stop, espada e inimigo parecem feitos de ar. Com 0.08 segundos de pausa no frame do acerto, a espada parece encontrar resistência. Combine com um pouco de trauma na câmera e o mesmo ataque de antes vira outro jogo.
Cuidado com um ponto: Engine.time_scale é global. Se o seu jogo tem timer de fase ou speedrun mode, contabilize isso. Pra jogos onde o global incomoda, a alternativa é pausar só os nodes envolvidos, mas comece pelo jeito simples e só complique se precisar.
Squash and stretch: animação que nasceu no desenho animado
Squash and stretch é o princípio mais antigo da animação clássica: objetos vivos se esticam no movimento e se achatam no impacto. Uma bola que cai estica na queda e achata no quique. Um personagem que pula estica na subida e amassa no pouso.
Em jogo, você não precisa desenhar frames extras pra isso. Escalar o sprite por código resolve, e o Tween do Godot 4 faz o trabalho de devolver a escala ao normal com uma curva elástica:
@onready var sprite: Sprite2D = $Sprite2D
func squash_pouso():
# Achata: mais largo, mais baixo.
sprite.scale = Vector2(1.3, 0.7)
var tween = create_tween()
tween.tween_property(sprite, "scale", Vector2.ONE, 0.18)\
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
func stretch_pulo():
# Estica: mais fino, mais alto.
sprite.scale = Vector2(0.75, 1.25)
var tween = create_tween()
tween.tween_property(sprite, "scale", Vector2.ONE, 0.18)\
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
Chame stretch_pulo() no frame em que o pulo dispara e squash_pouso() quando is_on_floor() volta a ser verdadeiro depois de uma queda. A transição TRANS_BACK passa um pouco do alvo antes de assentar, o que dá aquele rebote de desenho animado de graça.
Dois cuidados práticos. Primeiro, escale o sprite, nunca o corpo de colisão: se a CollisionShape muda de tamanho junto, você cria bug de física pra ganhar efeito visual, péssimo negócio. Segundo, se o seu sprite gira ou espelha, confira o pivô: o squash deve acontecer a partir dos pés do personagem, não do centro, senão ele afunda no chão visualmente. Ajuste o offset do Sprite2D pra deixar a origem na base.
Antes e depois: o pouso é onde mais aparece. Sem squash, o personagem para de cair e pronto, como um cursor de mouse. Com squash, ele aterrissa. É o tipo de detalhe que ninguém aponta e todo mundo sente.
Flash de dano: feedback que não deixa dúvida
Quando algo toma dano, o jogador precisa saber instantaneamente, sem ler barra de vida. A convenção dos jogos é piscar o alvo de branco por alguns frames.
Aqui tem uma pegadinha do Godot: o modulate do sprite multiplica a cor da textura, então ele só consegue escurecer ou tingir, nunca levar a imagem ao branco puro. Pra um flash branco de verdade, o caminho é um shader simples de canvas item:
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);
}
Salve como flash.gdshader, crie um ShaderMaterial no sprite apontando pra ele, e dispare o flash por código:
func flash_dano():
var mat := sprite.material as ShaderMaterial
mat.set_shader_parameter("flash_amount", 1.0)
var tween = create_tween()
tween.tween_property(mat, "shader_parameter/flash_amount", 0.0, 0.12)
O sprite vai a branco total e volta ao normal em pouco mais de um décimo de segundo. Combine com um pequeno knockback (empurrar o corpo na direção oposta ao golpe) e o dano fica impossível de não perceber.
Um aviso de quem já caiu nessa: se vários inimigos compartilham o mesmo material, o flash de um pisca todos. Marque Resource > Local to Scene no ShaderMaterial pra cada instância ter a sua cópia.
Montando o pacote: a ordem do impacto
As técnicas acima funcionam isoladas, mas o efeito real aparece quando elas disparam juntas, no mesmo frame do acerto. Um golpe bem temperado num action game costuma empilhar, em ordem de importância:
- Hit stop (0.05 a 0.1s): o congelamento vende o contato.
- Flash branco no alvo: confirma quem tomou o dano.
- Screen shake proporcional ao dano: dá peso.
- Squash no alvo: deformação de impacto.
- Som de impacto: metade do game feel é áudio, e merece artigo próprio.
- Partículas na direção do golpe: faíscas, poeira, o que combinar com o jogo.
A disciplina que eu recomendo: adicione uma camada por vez e jogue depois de cada uma. Primeiro porque você aprende o que cada técnica contribui de verdade. Segundo porque é assim que você acha o ponto de parada. Juice em excesso vira ruído visual, e a única forma de calibrar é sentindo no controle, não olhando o código.
E teste o caminho inverso também: depois de tudo pronto, desligue tudo de uma vez e jogue trinta segundos. O choque do antes e depois é a melhor prova de quanto essas camadas carregam.
Fechando
Game feel não é um sistema que você implementa uma vez, é uma camada de polimento que você espalha por todo ponto de contato entre jogador e jogo. As quatro técnicas daqui (screen shake com trauma, hit stop, squash and stretch e flash de dano) cobrem o essencial e cabem numa tarde de trabalho.
Se quiser um exercício concreto: pegue qualquer protótipo seu que tenha um ataque ou uma colisão, e adicione só o hit stop de 0.08 segundos. Uma função, seis linhas. Jogue antes, jogue depois. Esse contraste ensina mais sobre game feel do que qualquer texto, incluindo este.


