Zoom de camera suave no Godot 4

Aprenda a fazer zoom de camera godot suave com lerp e Tween no Camera2D, controlado por scroll do mouse ou gatilhos de gameplay, com limites min e max.
Um corte seco de zoom estraga a leitura da cena. O jogador percebe o salto, perde a referencia de espaco e o momento que voce queria destacar passa sem peso. Resolver o zoom de camera godot de forma suave e mais simples do que parece: voce trabalha a propriedade zoom do Camera2D, que e um Vector2, e interpola o valor atual ate um valor alvo. Neste tutorial eu mostro duas abordagens que uso de verdade em projetos, com lerp no _process e com Tween, alem do controle por scroll do mouse e por gatilhos de gameplay como entrar numa sala ou encarar um chefe.
Zoom de camera suave no Godot 4
Antes de qualquer codigo, vale entender como o Godot 4 trata a propriedade zoom. Ela mudou de comportamento em relacao ao Godot 3, e muita gente tropeca nisso.
Como a propriedade zoom funciona no Godot 4
No Camera2D o zoom e um Vector2 que multiplica a escala do que aparece na tela. A regra para guardar e direta: valor maior que 1 aproxima e valor menor que 1 afasta. Um zoom de Vector2(2, 2) deixa tudo duas vezes maior, ou seja, voce ve menos mundo porem mais perto. Um zoom de Vector2(0.5, 0.5) afasta e mostra mais area ao redor. O valor padrao e Vector2(1, 1), que e a escala neutra.
Isso e o inverso da intuicao de quem vem do Godot 3, onde o zoom funcionava como divisor. Se voce migrou um projeto e a camera parece invertida, esse e o motivo. Na pratica voce quase sempre mantem x e y iguais para nao distorcer a imagem, mas tecnicamente da para deformar o eixo se o efeito pedir.
Zoom alvo com scroll do mouse
A forma mais comum de controle e o scroll. A ideia e ter uma variavel de zoom alvo separada do zoom atual da camera. O input so altera o alvo, e a interpolacao cuida de chegar la sem solavanco.
extends Camera2D
@export var zoom_min: float = 0.5
@export var zoom_max: float = 4.0
@export var zoom_passo: float = 0.1
@export var velocidade_zoom: float = 8.0
var zoom_alvo: Vector2 = Vector2.ONE
func _ready() -> void:
zoom_alvo = zoom
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
ajustar_zoom(zoom_passo)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
ajustar_zoom(-zoom_passo)
func ajustar_zoom(delta_zoom: float) -> void:
var novo: float = clampf(zoom_alvo.x + delta_zoom, zoom_min, zoom_max)
zoom_alvo = Vector2(novo, novo)
func _process(delta: float) -> void:
zoom = zoom.lerp(zoom_alvo, velocidade_zoom * delta)
Repare em alguns pontos. O clampf garante que o alvo nunca passe dos limites zoom_min e zoom_max, entao por mais que o jogador role o scroll a camera respeita o intervalo. Roda para cima (WHEEL_UP) aumenta o valor e aproxima, roda para baixo afasta, o que casa com a expectativa da maioria dos jogadores. Se voce preferir o sentido contrario, basta trocar o sinal.
O segredo da suavidade esta na ultima linha. Em vez de jogar zoom = zoom_alvo direto, eu uso zoom.lerp(zoom_alvo, ...) multiplicado pelo delta. O lerp do Vector2 interpola entre o valor atual e o alvo a cada frame, e como o alvo nao se move enquanto o input nao muda, a camera desliza ate ele e para. Multiplicar pelo delta mantem a velocidade independente do framerate.
Um detalhe sobre esse tipo de lerp enquadrado no _process: ele e exponencial, nunca toca exatamente o alvo, so chega muito perto. Para a maioria dos jogos isso e irrelevante, mas se voce precisar de precisao absoluta em algum estado, force zoom = zoom_alvo quando a distancia ficar abaixo de um limiar pequeno.
Zoom por gatilhos de gameplay com Tween
Scroll e otimo para o jogador, mas boa parte do impacto de camera vem de eventos que voce dispara: entrar numa sala maior, o chefe aparecer, um momento de tensao que pede aproximacao. Para esses casos o Tween e mais limpo do que controlar lerp no _process, porque voce descreve a animacao inteira numa chamada e ela toca sozinha com a curva que voce escolher.
extends Camera2D
var tween_zoom: Tween
func aplicar_zoom(valor_alvo: float, duracao: float = 0.6) -> void:
var alvo: float = clampf(valor_alvo, 0.5, 4.0)
if tween_zoom and tween_zoom.is_running():
tween_zoom.kill()
tween_zoom = create_tween()
tween_zoom.set_trans(Tween.TRANS_SINE)
tween_zoom.set_ease(Tween.EASE_OUT)
tween_zoom.tween_property(self, "zoom", Vector2(alvo, alvo), duracao)
O tween_property recebe o objeto, o nome da propriedade como string ("zoom"), o valor final e a duracao. O Tween cuida de interpolar entre o zoom atual e o destino dentro do tempo informado. A combinacao TRANS_SINE com EASE_OUT da aquela entrada que comeca firme e desacelera no fim, que e o que costuma parecer natural numa aproximacao de camera. Da para testar outras transicoes como TRANS_CUBIC ou TRANS_BACK dependendo do tom.
A guarda no comeco e importante. Se um gatilho chamar aplicar_zoom enquanto outro Tween de zoom ainda esta rodando, os dois brigam pela mesma propriedade e o resultado treme. Por isso eu mato o tween anterior com kill() antes de criar o novo. Assim a transicao mais recente sempre vence e a camera nao gagueja.
Para conectar isso a gameplay e so chamar o metodo de onde fizer sentido. Uma Area2D que cobre a sala do chefe pode disparar a aproximacao na entrada e voltar ao normal na saida:
func _on_sala_boss_body_entered(corpo: Node2D) -> void:
if corpo.is_in_group("player"):
aplicar_zoom(1.8, 0.8)
func _on_sala_boss_body_exited(corpo: Node2D) -> void:
if corpo.is_in_group("player"):
aplicar_zoom(1.0, 0.8)
Aqui a camera aproxima para 1.8 quando o player entra na area e volta para 1.0, a escala neutra, quando ele sai. A duracao de 0.8 segundo da tempo para o jogador perceber a mudanca sem se sentir empurrado. Note que esse padrao assume que o script de zoom esta no proprio Camera2D; se a Area2D estiver em outro no, basta guardar uma referencia para a camera e chamar camera.aplicar_zoom(...).
Combinando os dois sem conflito
Numa cena real voce vai querer scroll do jogador e gatilhos de gameplay ao mesmo tempo, e ai e preciso decidir quem manda. O conflito aparece quando o jogador rola o scroll durante um Tween de boss, ou quando um gatilho dispara enquanto o jogador estava ajustando o zoom na mao.
A solucao que funciona bem e ter um estado simples na camera. Quando um gatilho de gameplay assume, ele liga uma flag que bloqueia o input de scroll ate a sequencia terminar. Assim o momento roteirizado nao e quebrado por um scroll acidental.
extends Camera2D
@export var zoom_min: float = 0.5
@export var zoom_max: float = 4.0
@export var velocidade_zoom: float = 8.0
var zoom_alvo: Vector2 = Vector2.ONE
var controle_travado: bool = false
func _ready() -> void:
zoom_alvo = zoom
func _unhandled_input(event: InputEvent) -> void:
if controle_travado:
return
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
definir_alvo(zoom_alvo.x + 0.1)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
definir_alvo(zoom_alvo.x - 0.1)
func definir_alvo(valor: float) -> void:
var v: float = clampf(valor, zoom_min, zoom_max)
zoom_alvo = Vector2(v, v)
func evento_gameplay(valor: float, travar: bool) -> void:
controle_travado = travar
definir_alvo(valor)
func _process(delta: float) -> void:
zoom = zoom.lerp(zoom_alvo, velocidade_zoom * delta)
Com controle_travado ligado, o _unhandled_input sai cedo e ignora o scroll. O gatilho de gameplay define o alvo e decide se trava ou nao o controle do jogador. Quando a sequencia acaba, e so chamar evento_gameplay(1.0, false) para devolver o controle e voltar a escala neutra. Tudo continua passando pelo mesmo lerp no _process, entao a transicao permanece suave seja qual for a origem do comando.
Ajustes finos que valem a pena
Alguns acabamentos fazem diferenca no resultado. O primeiro e ancorar o zoom no ponteiro do mouse em vez do centro da tela, util em jogos de estrategia e editores. Para isso voce desloca a posicao da camera proporcionalmente a diferenca entre o cursor e o centro a cada passo de zoom, o que mantem o ponto sob o mouse parado enquanto a cena cresce ou encolhe.
O segundo e cuidar do velocidade_zoom. Valores entre 6 e 10 costumam dar uma resposta gostosa de scroll. Abaixo disso a camera parece preguicosa, acima ela quase volta ao corte seco que estamos tentando evitar. Teste com o jogo rodando, porque a sensacao certa depende da escala da sua arte.
Por fim, lembre que zoom forte revela ou esconde area de jogo e isso afeta o equilibrio. Aproximar demais num jogo de plataforma pode esconder uma plataforma logo a frente. Trate os limites zoom_min e zoom_max como decisao de design, nao apenas como numero tecnico.
Com a propriedade zoom interpolada por lerp ou por Tween, mais o controle por scroll e por gatilhos, voce tem uma camera que respira junto com o jogo. O proximo passo natural e fazer essa mesma camera acompanhar o personagem, e para isso vale ver camera que segue o player (Camera2D follow). Se quiser dar mais impacto aos momentos de aproximacao, combine o zoom com screen shake de camera e veja como os dois efeitos se reforcam.


