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.
Zoom ancorado no ponteiro do mouse
Em jogo de estratégia, editor de níveis ou qualquer cena em que o jogador navega um mapa grande, como um jogo isométrico com TileMap no Godot, o zoom centrado na tela frustra: você aponta para um canto, rola o scroll e o canto escapa do enquadramento. O comportamento esperado é o do Google Maps, em que o ponto sob o cursor fica parado enquanto a cena cresce ou encolhe. O truque é compensar a posição da câmera na mesma medida em que o mundo muda de escala.
A versão direta, com o zoom aplicado na hora, usa a posição do mouse em coordenadas de mundo antes e depois de trocar o zoom:
extends Camera2D
@export var zoom_min: float = 0.5
@export var zoom_max: float = 4.0
@export var zoom_passo: float = 0.1
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
zoom_no_mouse(zoom_passo)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
zoom_no_mouse(-zoom_passo)
func zoom_no_mouse(delta_zoom: float) -> void:
var mouse_antes: Vector2 = get_global_mouse_position()
var novo: float = clampf(zoom.x + delta_zoom, zoom_min, zoom_max)
zoom = Vector2(novo, novo)
var mouse_depois: Vector2 = get_global_mouse_position()
global_position += mouse_antes - mouse_depois
A lógica é curta mas merece atenção. O get_global_mouse_position devolve onde o cursor aponta no mundo, e esse ponto depende da transformação da câmera. Ao mudar o zoom, o mesmo cursor passa a apontar para outro ponto do mundo. A diferença entre os dois (mouse_antes - mouse_depois) é exatamente o quanto a câmera precisa andar para colocar o ponto original de volta debaixo do cursor. Somar isso em global_position fixa a âncora.
Para manter a suavidade das seções anteriores, separe o alvo do valor aplicado: em vez de setar zoom seco, guarde zoom_alvo e a posição alvo, e deixe o lerp do _process convergir para os dois ao mesmo tempo. O deslize continua, só que agora ancorado no mouse em vez do centro. Uma observação de projeto: se o seu jogo é top-down ou de estratégia com visão sem perspectiva, vale conferir quando usar a câmera ortográfica no Godot, porque a escolha de projeção muda como o zoom é percebido.
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. Quando a aproximacao esconde muito do cenario, uma boa saida e dar ao jogador um minimapa 2D no Godot para ele nao perder a nocao do mapa enquanto a camera esta perto.
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. E se o seu jogo for 3D, o mesmo principio de seguir o alvo com suavizacao reaparece na camera em terceira pessoa na Godot.
Perguntas frequentes
O zoom do Camera2D no Godot 4 aumenta ou diminui a cena?
Valor maior que 1 aproxima e mostra menos mundo, valor menor que 1 afasta e mostra mais área. Vector2(2, 2) deixa tudo duas vezes maior; Vector2(0.5, 0.5) afasta. Isso é o inverso do Godot 3, onde o zoom funcionava como divisor, e é a causa mais comum de câmera parecer invertida depois de migrar um projeto.
Devo usar lerp no _process ou Tween para o zoom?
Use lerp no _process para controle contínuo do jogador, como o scroll do mouse, em que o alvo muda a qualquer momento. Use Tween para eventos roteirizados com início e fim definidos, como aproximar numa sala de chefe, porque você descreve a animação inteira numa chamada e escolhe a curva. Os dois podem conviver na mesma câmera.
Por que o lerp do zoom nunca chega exatamente no valor alvo?
O lerp enquadrado no _process é exponencial: ele se aproxima do alvo por frações a cada frame e chega muito perto, mas nunca toca exatamente. Para a maioria dos jogos isso é irrelevante. Se precisar de precisão absoluta num estado, force zoom = zoom_alvo quando a distância ficar abaixo de um limiar pequeno.
Como impedir que o jogador aproxime ou afaste demais?
Guarde o valor num zoom alvo e aplique clampf(valor, zoom_min, zoom_max) antes de atribuir. Assim, por mais que o jogador role o scroll, a câmera respeita o intervalo. Trate esses limites como decisão de design, não só número técnico, porque zoom forte revela ou esconde área de jogo e afeta o equilíbrio.
Dois gatilhos de zoom estão brigando e a câmera treme. Como resolvo?
Se dois Tween disputam a mesma propriedade, mate o anterior com kill() antes de criar o novo. Se o conflito é entre scroll do jogador e um gatilho de gameplay, use uma flag de controle travado que bloqueia o input de scroll enquanto a sequência roteirizada roda e libera no fim.
