Tween no Godot: Animações Suaves por Código

Aprenda a usar Tween no Godot 4 para animar por código: mover, escalar e dar fade em nodes, encadear animações e aplicar easing para deixar tudo suave.
Tween no Godot: Animações Suaves por Código
Tween no Godot é a ferramenta pra animar qualquer coisa por código: mover um painel pra dentro da tela, dar fade num sprite que morreu, fazer a moeda flutuar, dar aquele "punch" de escala quando o jogador acerta um golpe. Tudo sem abrir o AnimationPlayer, sem criar keyframe, sem timeline. Três linhas de GDScript e pronto.
E é exatamente por isso que tween é a primeira ferramenta de "juice" que vale dominar. A diferença entre um jogo que parece protótipo e um que parece produto raramente é a mecânica: é o menu que desliza em vez de aparecer do nada, o número de dano que sobe e some, o botão que cresce um pouco quando o mouse passa. Tudo isso é tween.
Todo código aqui é Godot 4.x e roda como está.
Tween ou AnimationPlayer?
Regra prática que eu uso: se a animação é fixa e coreografada (um cutscene, uma porta que abre sempre igual, um ciclo de idle), AnimationPlayer. Se a animação depende de valores que só existem em runtime (mover até a posição que o jogador clicou, fade no inimigo que acabou de morrer, atualizar a barra de vida pro valor atual), tween.
O AnimationPlayer pede que você saiba os valores de antemão pra colocar nos keyframes. O tween aceita qualquer variável. É essa flexibilidade que faz ele ser onipresente em código de gameplay e UI.
Primeiro tween: mover um node
No Godot 4 não existe mais node de Tween na cena (isso era Godot 3). Você cria o tween por código, de dentro de qualquer node:
func _ready():
var tween = create_tween()
tween.tween_property(self, "position", Vector2(400, 300), 0.5)
Isso move o node da posição atual até (400, 300) em meio segundo. O tween_property recebe quatro coisas: o objeto, o nome da propriedade (como string), o valor final e a duração em segundos.
Dois detalhes que economizam código:
# as_relative(): o valor vira deslocamento em vez de destino.
# Aqui o node anda 200px pra direita de onde estiver.
tween.tween_property(self, "position", Vector2(200, 0), 0.5).as_relative()
# from(): força um valor inicial em vez de partir do atual.
# Útil pra item que "nasce" pequeno e cresce.
tween.tween_property(self, "scale", Vector2.ONE, 0.3).from(Vector2.ZERO)
O tween começa sozinho no fim do frame e morre sozinho quando termina. Você não precisa dar play nem limpar nada depois.
E dá pra animar subpropriedade usando dois-pontos no caminho. É assim que se faz fade, animando só o alfa do modulate:
var tween = create_tween()
tween.tween_property($Sprite2D, "modulate:a", 0.0, 0.4)
Easing: o que separa "mexeu" de "ficou bom"
Por padrão o tween interpola linear, e linear parece mecânico. Objeto de verdade acelera e desacelera. É isso que set_trans e set_ease controlam:
var tween = create_tween()
tween.tween_property(self, "position", Vector2(400, 300), 0.5)\
.set_trans(Tween.TRANS_QUAD)\
.set_ease(Tween.EASE_OUT)
O set_trans escolhe a curva, o set_ease escolhe onde ela age: EASE_IN (começa devagar), EASE_OUT (termina devagar) ou EASE_IN_OUT (os dois). As curvas que eu realmente uso no dia a dia:
- TRANS_QUAD / TRANS_CUBIC: suavização padrão. Quad é sutil, cubic é mais pronunciada. Servem pra 90% dos casos.
- TRANS_SINE: a mais suave de todas. Boa pra movimento contínuo, tipo flutuação de item.
- TRANS_BACK: passa um pouco do alvo e volta. Perfeita pra UI com personalidade.
- TRANS_ELASTIC: oscila tipo mola. Chamativa, use com moderação.
- TRANS_BOUNCE: quica no final. Boa pra objeto que cai.
Pra UI, TRANS_QUAD ou TRANS_CUBIC com EASE_OUT é quase sempre a resposta: o elemento chega rápido e assenta suave, que é como interface responsiva se comporta.
Se o tween inteiro usa a mesma curva, configure no tween em vez de repetir em cada passo:
var tween = create_tween().set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
Escalar e dar fade: as receitas que mais uso
Punch de escala (acertou o golpe, pegou a moeda, clicou no botão): cresce rápido, volta com um leve overshoot.
func punch():
var tween = create_tween()
tween.tween_property(self, "scale", Vector2(1.2, 1.2), 0.1)\
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
tween.tween_property(self, "scale", Vector2.ONE, 0.2)\
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
Fade out e remover da cena (inimigo morto, item coletado): anima o alfa e chama queue_free no final com tween_callback, que executa qualquer função como um passo da sequência.
func morrer():
var tween = create_tween()
tween.tween_property(self, "modulate:a", 0.0, 0.4)
tween.tween_callback(queue_free)
Flutuação infinita (item colecionável, ícone de quest): dois movimentos com TRANS_SINE e set_loops() sem argumento, que repete pra sempre.
func _ready():
var tween = create_tween().set_loops()
tween.tween_property(self, "position:y", position.y - 8.0, 1.0)\
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
tween.tween_property(self, "position:y", position.y, 1.0)\
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
set_loops(3) repetiria três vezes e pararia.
Encadear tweens: sequência, paralelo e pausa
Aqui está o pulo do gato do sistema novo: cada chamada de tween_property no mesmo tween entra numa fila e roda em sequência, um depois do outro. O exemplo do fade acima já usa isso (primeiro o fade, depois o callback). Dá pra montar coreografia inteira num tween só:
var tween = create_tween()
tween.tween_property(self, "position", Vector2(300, 0), 0.5).as_relative()
tween.tween_interval(0.3) # pausa de 0.3s no meio da sequência
tween.tween_property(self, "rotation", TAU, 0.5).as_relative()
tween.tween_callback(func(): print("acabou"))
Quando você quer que dois passos rodem juntos, tem dois jeitos. Pra um passo específico, prefixe com parallel(), que faz ele rodar junto do passo anterior:
var tween = create_tween()
tween.tween_property(self, "position", Vector2(300, 0), 0.5).as_relative()
tween.parallel().tween_property(self, "modulate:a", 0.5, 0.5)
# Os dois acima rodam juntos. O de baixo espera os dois terminarem.
tween.tween_property(self, "scale", Vector2(1.5, 1.5), 0.3)
Pra um tween onde quase tudo é paralelo (um painel que desliza e aparece ao mesmo tempo), inverta o padrão com set_parallel:
var tween = create_tween().set_parallel(true)
tween.tween_property(painel, "position:y", 200.0, 0.4)
tween.tween_property(painel, "modulate:a", 1.0, 0.4)
# chain() volta pro modo sequencial só neste passo:
tween.chain().tween_callback(func(): botao.disabled = false)
Essa composição (sequência por padrão, paralelo onde pedir, intervalo e callback como passos) cobre praticamente toda animação de gameplay e UI que eu já precisei escrever.
tween_method: animar o que não é propriedade
Nem tudo que você quer animar é uma propriedade de node. Contador de pontos subindo, por exemplo: o que muda é o texto de uma Label, e texto não se interpola. O tween_method resolve: ele interpola um valor e chama sua função com o valor intermediário a cada frame.
@onready var label = $PontosLabel
func animar_pontos(de: float, ate: float):
var tween = create_tween()
tween.tween_method(_atualizar_label, de, ate, 0.8)\
.set_trans(Tween.TRANS_QUINT).set_ease(Tween.EASE_OUT)
func _atualizar_label(valor: float):
label.text = str(int(valor))
Com EASE_OUT o contador corre rápido no começo e desacelera perto do valor final, que é o efeito clássico de placar de arcade. O mesmo padrão serve pra animar parâmetro de shader, volume de áudio, ou qualquer setter seu.
Ciclo de vida: o erro que todo mundo comete
Tween não é reutilizável. Quando termina, ele se torna inválido: você não dá play de novo, você cria outro. E o erro clássico derivado disso é criar tweens empilhados na mesma propriedade. Acontece muito em hover de botão: o mouse entra e sai rápido, cada evento cria um tween de escala, e os dois brigam pelo mesmo valor, tremendo o botão.
O padrão correto é guardar a referência e matar o anterior antes de criar o novo:
var tween: Tween
func _on_mouse_entered():
_escalar_para(Vector2(1.1, 1.1))
func _on_mouse_exited():
_escalar_para(Vector2.ONE)
func _escalar_para(alvo: Vector2):
if tween:
tween.kill()
tween = create_tween()
tween.tween_property(self, "scale", alvo, 0.15)\
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
Pelo mesmo motivo, nunca crie tween dentro de _process: são 60 tweens novos por segundo disputando a mesma propriedade.
Mais três coisas sobre o ciclo de vida que valem saber:
Esperar o tween terminar. O sinal finished funciona com await, o que deixa código de morte e transição muito limpo:
func transicao_de_fase():
var tween = create_tween()
tween.tween_property($FadeRect, "modulate:a", 1.0, 0.5)
await tween.finished
get_tree().change_scene_to_file("res://fases/fase_2.tscn")
O tween fica preso ao node que o criou. Usando create_tween() de dentro de um node, o tween pausa junto com o node e morre se o node for liberado da memória. Isso é bom: evita tween fantasma operando em node que não existe mais. Se precisar de um tween independente (pra animar durante uma pausa de jogo, por exemplo), crie pelo SceneTree e ajuste o modo de pausa:
var tween = get_tree().create_tween()
tween.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
Velocidade global. tween.set_speed_scale(2.0) roda o tween inteiro em dobro da velocidade, sem mexer nas durações. Útil pra modo turbo e pra testar animação longa sem esperar.
Fechando
Tween no Godot 4 se resume a um fluxo: create_tween(), empilhar passos (tween_property, tween_interval, tween_callback, tween_method), temperar com set_trans e set_ease, e respeitar o ciclo de vida matando o tween antigo antes de criar outro na mesma propriedade.
O que faz diferença mesmo é usar. Pegue um projeto seu e adicione três tweens hoje: um EASE_OUT num painel de UI, um punch de escala em alguma coleta, um fade na morte de inimigo. São minutos de trabalho e o jogo inteiro muda de cara. Depois disso você não consegue mais deixar nada aparecer na tela "do nada", e é aí que o seu jogo começa a parecer profissional.


