4 padrões de Tween no Godot 4 que substituem 90% das suas animações

Quando você começa a programar jogos, a primeira coisa que descobre é que coisa parada não vende. Personagens precisam ricochetear, botões precisam pulsar, dano numérico precisa subir e desaparecer. E aí você cai no _process(delta) escrevendo lerp manual em cinco lugares diferentes do código.
A maioria dos tutoriais brasileiros sobre Godot ainda mostra animação por _process. Funciona, mas a Tween do Godot 4 resolve 90% desses casos com 5 linhas de código, sem entupir o código de jogo com matemática de easing.
Este post é sobre quando usar Tween, como evitar os bugs clássicos e os 4 padrões que cobrem quase tudo no dia a dia. Testado no Godot 4.4.
O básico: animar um valor em uma linha
Tween em Godot 4 é uma classe que você cria sob demanda. Não tem nó, não precisa configurar nada na cena.
extends Node2D
func _ready() -> void:
var tween := create_tween()
tween.tween_property(self, "position:x", 500.0, 1.0)
Isso anima a posição X do nó até 500 ao longo de 1 segundo. create_tween() é um helper de Node que já amarra a Tween no ciclo de vida do nó (se o nó for liberado, a Tween morre junto).
"position:x" é a parte interessante. O Godot 4 entende paths de propriedade com sub-componentes, então você pode animar só o X, só o Y, só o canal alfa de uma cor ("modulate:a"), e por aí vai. Antes precisava criar uma variável intermediária e atualizar manualmente.
Easing: por que parece "robótico" e como arrumar
A animação acima vai parecer mecânica. Sem easing, é movimento linear, e cérebro humano lê isso como "máquina". Adicione easing:
var tween := create_tween()
tween.tween_property(self, "position:x", 500.0, 1.0) \
.set_trans(Tween.TRANS_QUAD) \
.set_ease(Tween.EASE_OUT)
TRANS_QUAD + EASE_OUT é meu default para 80% das animações de UI. Começa rápido, desacelera no fim. Combinação que parece natural sem chamar atenção.
A regra prática que uso:
- UI aparecendo:
EASE_OUT(decelera no destino) - UI saindo:
EASE_IN(acelera ao desaparecer) - Algo voltando para o lugar:
EASE_OUTcomTRANS_BACK(overshoot ligeiro, depois assenta) - Algo bouncing:
TRANS_ELASTICouTRANS_BOUNCE
A documentação tem um gráfico bom de cada combinação. Vale guardar nos favoritos.
Padrão 1: pulsar um botão (efeito "clique")
Cenário clássico: jogador clica num botão e você quer feedback visual. Sem Tween, você cria uma AnimationPlayer. Com Tween:
func pulse_button(button: Control) -> void:
var tween := create_tween()
tween.tween_property(button, "scale", Vector2(1.1, 1.1), 0.08)
tween.tween_property(button, "scale", Vector2(1.0, 1.0), 0.12) \
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
Por padrão, Tweens executam em sequência (uma propriedade depois da outra). 80ms para escalar pra 1.1, 120ms para voltar com um leve overshoot. O resultado parece um "boing" gostoso.
Padrão 2: dano numérico subindo (damage popup)
Quando o jogador acerta um inimigo, é comum mostrar o número do dano subindo e desaparecendo. Tween paralela resolve:
func spawn_damage_label(amount: int, world_position: Vector2) -> void:
var label := preload("res://ui/damage_label.tscn").instantiate()
label.text = str(amount)
label.global_position = world_position
get_tree().current_scene.add_child(label)
var tween := create_tween().set_parallel(true)
tween.tween_property(label, "global_position:y", world_position.y - 60, 0.6) \
.set_ease(Tween.EASE_OUT)
tween.tween_property(label, "modulate:a", 0.0, 0.6) \
.set_delay(0.2)
tween.chain().tween_callback(label.queue_free)
Três coisas acontecem em paralelo: sobe 60 pixels em Y, fica transparente após 200ms de atraso, e quando tudo termina o nó é liberado da memória.
set_parallel(true) no início faz com que os tween_property rodem ao mesmo tempo. chain() quebra o paralelismo: tudo depois do chain() espera o último step terminar.
Padrão 3: câmera screenshake
Screenshake feito por escrever em position direto no _process é um clássico bug no Godot 2/3. A câmera "dança" no lugar errado se você esquece de zerar entre frames. Com Tween, fica imune:
func screen_shake(intensity: float = 8.0, duration: float = 0.4) -> void:
var camera := get_viewport().get_camera_2d()
if not camera:
return
var original := camera.offset
var tween := create_tween()
for i in 6:
var random_offset := Vector2(
randf_range(-intensity, intensity),
randf_range(-intensity, intensity)
)
tween.tween_property(camera, "offset", random_offset, 0.04)
intensity *= 0.7
tween.tween_property(camera, "offset", original, 0.1)
A intensidade decai a cada iteração (multiplica por 0.7), então o tremor desacelera naturalmente. No final, garante que a câmera volta ao offset original com .tween_property em vez de assignment direto, evitando "snap" feio.
Padrão 4: loop infinito (HP baixo piscando)
Para coisas que precisam piscar até uma condição mudar (HP baixo, item para coletar destacando), use set_loops(0):
var low_hp_tween: Tween
func _ready() -> void:
low_hp_tween = create_tween().set_loops()
low_hp_tween.tween_property($HpBar, "modulate", Color.RED, 0.5)
low_hp_tween.tween_property($HpBar, "modulate", Color.WHITE, 0.5)
func stop_low_hp_warning() -> void:
if low_hp_tween and low_hp_tween.is_valid():
low_hp_tween.kill()
set_loops() sem argumento = loop infinito. Para parar, chame kill(). Sempre cheque is_valid() primeiro porque a Tween pode ter morrido junto com o nó pai.
Essa é a fonte mais comum de bug travado no console: você chama tween.kill() numa Tween cujo nó pai já foi liberado, e ela explode com NRE.
O que eu não uso Tween para
Tween é ótima, mas não é solução universal:
- Animações com keyframes complexos (4 ou mais propriedades sincronizadas, blend de animações 2D, etc.): use
AnimationPlayer. É feita pra isso. - Animações skeletais 3D:
AnimationPlayerouAnimationTree. - Lerp por física (rigid bodies, character controllers): mexer em
positionvia Tween conflita com integração de física. Usemove_and_slideou aplique força. - Sequências longas com lógica de jogo no meio: depois de uns 6 steps, fica mais limpo migrar pra
AnimationPlayercom método tracks.
Pegadinhas que custaram tempo
Algumas coisas que aprendi à força:
- Esquecer
create_tween()em uma variável e tentar reutilizar: cada chamada decreate_tween()cria uma instância nova. Se você quer reutilizar (caso do loop infinito), guarde a referência. - Tween em
@onreadyno_readyantes do nó pai estar na árvore: causa erro silencioso. Coloque a primeiracreate_tweendepois do nó estar na cena, não em@onready. - Misturar
set_parallel(true)no construtor comparallel()por step: dá inconsistência. Escolha uma das duas estratégias e mantenha. - Animar
Colordireto em vez demodulate:modulateé a propriedade que multiplica todas as cores do nó e dos filhos. Animarcolorem alguns nós (comoLabel) só afeta o texto, mas não a transparência geral.
Conclusão
Os 4 padrões cobrem 90% das animações que aparecem em UI de jogo, popups de dano, screenshake e indicadores de estado. Vale internalizar a sintaxe (create_tween → tween_property encadeado → set_ease/set_trans) e parar de escrever lerp manual em _process.
A documentação oficial do Tween é boa. Vale olhar também o Tweener subclasses (MethodTweener, IntervalTweener, CallbackTweener) quando você precisar de algo mais customizado que tween_property.
Se sua codebase de jogo tem _process(delta) cheio de matemática de easing manual, vale uma tarde refatorando pra Tween. O código fica menor, mais legível, e desbloqueia animações que antes não compensavam o esforço.