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.

Ferramenta:Godot

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: float) -> void:
    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() -> void:
    noise.noise_type = FastNoiseLite.TYPE_PERLIN
    noise.seed = randi()
    noise.frequency = 0.5

func add_trauma(amount: float) -> void:
    trauma = clamp(trauma + amount, 0.0, 1.0)

func _process(delta: float) -> void:
    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() -> void:
    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() -> void:
    spawn_bullet()
    camera.add_trauma(0.15)

# No script do inimigo, ao morrer:
func die() -> void:
    queue_free()
    camera.add_trauma(0.3)

# Na explosão do barril:
func explode() -> void:
    camera.add_trauma(0.6)

# Quando o PLAYER leva dano (sempre mais forte que causar dano):
func take_damage(amount) -> void:
    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) -> void:
    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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 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() -> void:
    var config = ConfigFile.new()
    config.set_value("accessibility", "screen_shake", screen_shake_strength)
    config.save("user://settings.cfg")

func load_settings() -> void:
    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() -> void:
    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) -> void:
    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, knockback e vibração do controle 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. E se quiser levar o impacto pro próximo nível, combine o tremor com slow motion controlado por time scale: meio segundo de câmera lenta no golpe crítico vende o peso melhor que qualquer partícula.

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. O tremor mora na mesma câmera que segue o jogador, então vale também acertar o zoom suave da câmera na Godot, que dá aquele respiro de foco antes e depois do impacto. Depois disso, conectar os eventos reais do jogo é trabalho de meia hora.