Voltar para o Blog
Quest Log

Zoom de camera suave no Godot 4

Cena 2D no Godot 4 com a camera se aproximando suavemente de um personagem.

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.

Ferramenta:Godot

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.

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

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.