Executando verificação de segurança...
1

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

4 padrões de Tween no Godot 4

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_OUT com TRANS_BACK (overshoot ligeiro, depois assenta)
  • Algo bouncing: TRANS_ELASTIC ou TRANS_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: AnimationPlayer ou AnimationTree.
  • Lerp por física (rigid bodies, character controllers): mexer em position via Tween conflita com integração de física. Use move_and_slide ou aplique força.
  • Sequências longas com lógica de jogo no meio: depois de uns 6 steps, fica mais limpo migrar pra AnimationPlayer com método tracks.

Pegadinhas que custaram tempo

Algumas coisas que aprendi à força:

  1. Esquecer create_tween() em uma variável e tentar reutilizar: cada chamada de create_tween() cria uma instância nova. Se você quer reutilizar (caso do loop infinito), guarde a referência.
  2. Tween em @onready no _ready antes do nó pai estar na árvore: causa erro silencioso. Coloque a primeira create_tween depois do nó estar na cena, não em @onready.
  3. Misturar set_parallel(true) no construtor com parallel() por step: dá inconsistência. Escolha uma das duas estratégias e mantenha.
  4. Animar Color direto em vez de modulate: modulate é a propriedade que multiplica todas as cores do nó e dos filhos. Animar color em alguns nós (como Label) 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_tweentween_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.

Carregando publicação patrocinada...