Voltar para o Blog
Quest Log

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

Cena de jogo 2D com a câmera tremendo durante uma explosão, com linhas de impacto ao redor da tela

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.

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

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:

  1. 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_offset até esse evento parecer certo. Em pixel art com viewport pequeno, 16x12 pixels costuma ser teto; em resolução cheia, pode dobrar.
  2. 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.
  3. Ajuste o decay pelo 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.
  4. noise_speed controla 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.