Screen Shake no Godot: Tremer a Câmera do Jeito Certo

Aprenda screen shake godot do jeito certo: sistema de trauma com decaimento, FastNoiseLite na Camera2D, intensidade por evento e opção de acessibilidade.
Screen Shake no Godot: Tremer a Câmera do Jeito Certo
Screen shake é o tempero mais barato que existe pra fazer um impacto parecer impacto. Uma explosão sem tremida é um sprite trocando de frame; com tremida, é uma explosão. Mas é fácil errar a mão: a maioria dos tutoriais de screen shake godot ensina a jogar randf_range direto no offset da câmera, e o resultado é aquele tremor de gelatina que parece bug de renderização, não força.
Nesse tutorial eu monto o sistema que virou padrão da indústria desde a palestra clássica do Vlambeer sobre game feel: trauma que acumula e decai, intensidade elevada ao quadrado, e ruído suave com FastNoiseLite em vez de random puro. No final ainda entra a parte que quase todo mundo esquece: a opção de acessibilidade pra quem enjoa com a tela tremendo. Tudo em GDScript do Godot 4.x, em cima de uma Camera2D comum.
Por que random puro fica ruim
O jeito ingênuo é esse: a cada frame, se a câmera está tremendo, sorteia um offset.
# O jeito que eu NÃO recomendo.
func _process(delta):
if shaking:
offset = Vector2(randf_range(-8, 8), randf_range(-8, 8))
Dois problemas. Primeiro, random puro não tem continuidade: a câmera teleporta de um ponto pra outro a cada frame, sem trajetória. O olho lê isso como vibração elétrica, não como uma câmera física sendo sacudida. Segundo, não tem noção de intensidade acumulada: dois tiros seguidos tremem igual a um tiro, e a explosão grande treme igual ao tiro de pistola, a menos que você espalhe ifs pelo código.
O sistema de trauma resolve os dois de uma vez. A ideia tem duas peças:
- Trauma é um número de 0 a 1 que representa "quanto a câmera apanhou". Cada evento soma trauma (tiro soma pouco, explosão soma muito) e ele decai sozinho com o tempo. Eventos seguidos acumulam, então uma sequência de impactos treme mais que um impacto isolado, de graça.
- O shake real usa trauma ao quadrado. Como trauma vive entre 0 e 1, elevar ao quadrado faz valores baixos quase sumirem e valores altos baterem forte. Trauma 0.3 vira shake 0.09, quase nada; trauma 0.9 vira 0.81, pancada. É essa curva que separa o "tremeu porque levei um arranhão" do "tremeu porque o chefe socou o chão".
E no lugar do random, ruído Perlin via FastNoiseLite: um valor que varia suavemente conforme você caminha por ele. A câmera passa a percorrer uma trajetória contínua e errática, que é exatamente como uma câmera de verdade se comporta quando alguém esbarra nela.
Screen shake godot com trauma e FastNoiseLite
O script completo, pra colocar direto na sua Camera2D. Se a sua câmera ainda não segue o player direito, vale resolver isso antes, eu mostro o setup em como fazer a Camera2D seguir o player no Godot.
extends Camera2D
# Quanto o trauma cai por segundo. 1.0 = um shake cheio dura ~1s.
@export var decay: float = 1.0
# Deslocamento máximo da câmera, em pixels, com trauma no talo.
@export var max_offset: Vector2 = Vector2(16, 12)
# Rotação máxima em radianos. Pouco já é muito.
@export var max_roll: float = 0.05
# Velocidade com que percorremos o ruído. Maior = tremor mais nervoso.
@export var noise_speed: float = 50.0
var trauma: float = 0.0
var noise := FastNoiseLite.new()
var noise_time: float = 0.0
func _ready():
noise.noise_type = FastNoiseLite.TYPE_PERLIN
noise.seed = randi()
noise.frequency = 0.5
func add_trauma(amount: float):
trauma = clamp(trauma + amount, 0.0, 1.0)
func _process(delta):
if trauma > 0.0:
trauma = max(trauma - decay * delta, 0.0)
noise_time += delta * noise_speed
_apply_shake()
else:
offset = Vector2.ZERO
rotation = 0.0
func _apply_shake():
var shake := trauma * trauma
# Três fatias diferentes do mesmo ruído, uma por eixo.
offset.x = max_offset.x * shake * noise.get_noise_2d(noise_time, 0.0)
offset.y = max_offset.y * shake * noise.get_noise_2d(0.0, noise_time)
rotation = max_roll * shake * noise.get_noise_2d(noise_time, noise_time)
Os pontos que carregam o design:
get_noise_2d com coordenadas diferentes por eixo. O ruído é uma paisagem; cada eixo da câmera caminha por uma região diferente dela. Se os três usassem a mesma coordenada, a câmera iria sempre na diagonal, o que fica visivelmente artificial. Com fatias separadas, x, y e rotação dançam de forma independente mas suave.
O retorno do ruído já vem entre -1 e 1, então multiplicar pelo máximo e pelo shake resolve, sem randf_range em lugar nenhum.
A rotação é o segredo do shake gostoso. Offset puro parece a tela deslizando; um pingo de roll vende a ideia de que a câmera física girou no soquete. Mas o teto é baixo mesmo: 0.05 radiano são uns 3 graus, e já é bastante. Acima disso enjoa rápido.
Zerar offset e rotation quando o trauma acaba evita a câmera ficar permanentemente torta por um resíduo de ruído do último frame.
Intensidade por evento: cada impacto com seu peso
Com o add_trauma exposto, o resto do jogo só precisa dizer o quanto cada evento dói. Eu gosto de manter uma tabela mental simples:
# No script da arma:
func shoot():
spawn_bullet()
camera.add_trauma(0.15)
# No script do inimigo, ao morrer:
func die():
queue_free()
camera.add_trauma(0.3)
# Na explosão do barril:
func explode():
camera.add_trauma(0.6)
# Quando o PLAYER leva dano (sempre mais forte que causar dano):
func take_damage(amount):
health -= amount
camera.add_trauma(0.5)
Como o trauma acumula e satura em 1.0, esses valores compõem sozinhos: limpar uma sala atirando sem parar mantém um tremor de fundo constante, e uma explosão no meio disso estoura o medidor. Você não escreve nenhuma lógica de combinação; a soma com clamp já é a lógica.
Pra acessar a câmera de qualquer lugar sem acoplar tudo, o caminho limpo no Godot 4 é um grupo:
# No _ready da câmera:
add_to_group("camera")
# Em qualquer script do jogo:
func shake_camera(amount: float):
var cam = get_tree().get_first_node_in_group("camera")
if cam:
cam.add_trauma(amount)
Funciona com troca de cena, com múltiplas fases, e não exige autoload nem caminho absoluto de node.
Acessibilidade: a opção que separa amador de profissional
Screen shake causa desconforto real em parte dos jogadores: enjoo de movimento, vertigem, dor de cabeça. Não é frescura e não é minoria desprezível. Praticamente todo jogo comercial recente traz um slider ou toggle de screen shake nas opções, e o seu deveria trazer também. A boa notícia é que no sistema de trauma isso custa três linhas.
A ideia: um multiplicador global de 0.0 a 1.0 que escala o resultado final do shake. Guardo num autoload de configurações:
# settings.gd, registrado como autoload "Settings"
extends Node
var screen_shake_strength: float = 1.0
func save_settings():
var config = ConfigFile.new()
config.set_value("accessibility", "screen_shake", screen_shake_strength)
config.save("user://settings.cfg")
func load_settings():
var config = ConfigFile.new()
if config.load("user://settings.cfg") == OK:
screen_shake_strength = config.get_value("accessibility", "screen_shake", 1.0)
E na câmera, o multiplicador entra no cálculo final:
func _apply_shake():
var shake := trauma * trauma * Settings.screen_shake_strength
offset.x = max_offset.x * shake * noise.get_noise_2d(noise_time, 0.0)
offset.y = max_offset.y * shake * noise.get_noise_2d(0.0, noise_time)
rotation = max_roll * shake * noise.get_noise_2d(noise_time, noise_time)
Na tela de opções, um HSlider de 0 a 1 conectado direto:
func _on_shake_slider_value_changed(value: float):
Settings.screen_shake_strength = value
Settings.save_settings()
Detalhe importante: escalar o resultado em vez de bloquear o add_trauma mantém o resto do sistema intacto. Se algum dia você usar trauma pra outra coisa (vibração de controle, flash de tela), a preferência de shake visual não contamina o resto. E quem joga com 0.3 ainda sente a hierarquia dos impactos, só que em volume baixo.
Calibrando: os números que importam
Não existe valor universal, mas existe método. Minha rotina de ajuste:
- Comece pelo evento mais forte do jogo (a maior explosão, o golpe do chefe) e dê a ele trauma 0.7 a 0.9. Ajuste
max_offsetaté esse evento parecer certo. Em pixel art com viewport pequeno, 16x12 pixels costuma ser teto; em resolução cheia, pode dobrar. - Desça a escada. Cada evento menor recebe uma fração: dano recebido em torno de 0.5, kill em 0.3, tiro entre 0.1 e 0.2. Se o tiro comum fica invisível, lembre que é o quadrado agindo: trauma 0.15 gera shake 0.02, e está tudo bem, tiro comum deve ser quase subliminar.
- Ajuste o
decaypelo ritmo do jogo. Jogo frenético pede decaimento rápido (1.5 a 2.0) pra câmera assentar entre as trocas de tiro. Jogo mais lento aceita 0.8 e deixa o tremor respirar. noise_speedcontrola a textura. Em torno de 50 dá um tremor orgânico; acima de 100 vira vibração de furadeira, o que pode até ser o efeito desejado num terremoto.
Um teste honesto: grave dez segundos de gameplay e assista sem jogar. Jogando, a gente tolera muito mais tremida do que assistindo, e quem assiste é o seu trailer.
O screen shake é uma peça de um conjunto maior: hitstop, flash, partículas e knockback trabalhando juntos no mesmo impacto. Eu destrincho esse conjunto em game feel e juice: o que faz um jogo ser gostoso, que é a continuação natural desse artigo.
Fechando
O resumo do sistema cabe em quatro regras. Trauma de 0 a 1 que acumula por evento e decai com o tempo. Shake proporcional ao trauma ao quadrado, pra impactos grandes dominarem. FastNoiseLite no lugar de random, com uma fatia de ruído por eixo e um toque de rotação. E um multiplicador de acessibilidade escalando o resultado final, salvo nas configurações.
São uns 60 linhas de GDScript no total e o efeito muda a percepção do jogo inteiro. Monta numa cena de teste com um botão que chama add_trauma(0.2) e outro que chama add_trauma(0.8), e calibra os exports até a diferença entre os dois contar a história sozinha. Depois disso, conectar os eventos reais do jogo é trabalho de meia hora.


