Rastro (trail) de movimento 2D no Godot 4

Aprenda a criar trail godot 2d com Line2D e fantasmas (afterimage). Rastro que encolhe, desaparece com gradiente e dicas de performance no Godot 4.
Um rastro de movimento é um daqueles detalhes baratos que melhoram muito a leitura visual de uma cena. Ele mostra a direção e a velocidade do personagem, dá peso a um dash e ajuda o jogador a entender o que aconteceu mesmo quando a ação é rápida. Neste tutorial você vai montar um trail godot 2d do zero, primeiro com Line2D (a abordagem mais comum e leve) e depois com fantasmas (afterimage) usando Sprite2D e Tween. No fim você vai saber qual escolher para cada situação e como não derrubar o frame rate no processo.
Rastro (trail) de movimento 2D no Godot 4
A ideia central de qualquer rastro é simples: a cada frame você registra onde o personagem estava e desenha algo nessas posições antigas, fazendo esse algo sumir aos poucos. Tudo que muda entre as técnicas é o "algo" que você desenha. Com Line2D você desenha uma linha contínua que passa pelas posições recentes. Com fantasmas você instancia uma cópia da sprite em cada posição e deixa ela desaparecer. Vamos pelas duas.
Por que usar Line2D para um trail godot 2d
O Line2D é a opção padrão porque ele já sabe desenhar uma linha a partir de uma lista de pontos. Você só precisa alimentar essa lista com a posição do personagem a cada frame e remover os pontos mais velhos para o rastro ter um tamanho fixo. Como é um único nó desenhando uma única malha, o custo é baixo mesmo com dezenas de pontos.
Crie uma cena com o seu personagem (por exemplo um CharacterBody2D) e adicione um Line2D como filho. No Inspetor, defina uma width inicial (uns 8 pixels já dá pra ver) e uma cor em default_color. Marque também a propriedade top_level como true. Isso é importante e a gente volta nesse ponto mais pra frente.
Adicionando e removendo pontos a cada frame
O script abaixo vai num Line2D que é filho do personagem. Ele guarda a posição global do dono a cada frame, empurra esse ponto pro fim da lista e descarta o ponto mais antigo quando passa do limite.
extends Line2D
@export var max_pontos: int = 20
func _ready() -> void:
top_level = true
clear_points()
func _process(_delta: float) -> void:
var pos_alvo: Vector2 = get_parent().global_position
add_point(pos_alvo)
while get_point_count() > max_pontos:
remove_point(0)
add_point coloca o ponto no fim e remove_point(0) tira o primeiro (o mais velho). Como o top_level está ligado, o Line2D ignora a transformação do pai, então global_position e as coordenadas locais da linha coincidem. Sem isso, a linha herdaria o movimento do personagem e os pontos antigos andariam junto com ele, destruindo o efeito.
Já dá pra rodar. O personagem vai arrastar uma linha de comprimento fixo atrás de si. Mas ela tem a mesma espessura e cor do início ao fim, o que parece artificial. Vamos fazer ela desaparecer.
Fazendo o rastro encolher com width_curve e gradiente
O Line2D tem duas propriedades feitas pra isso. A width_curve é uma Curve que controla a espessura ao longo da linha (0.0 é o começo da linha, 1.0 é o fim). A gradient é um Gradient que controla a cor e a transparência da mesma forma.
O detalhe é entender a ordem dos pontos. No nosso código o ponto mais antigo está no índice 0 (começo da linha) e o mais novo está no fim. Então a "cabeça" do rastro, junto do personagem, é o fim da curva (1.0). Se você quer um rastro grosso perto do personagem e fino na ponta que se desfaz, a curva deve ir de um valor baixo em 0.0 até alto em 1.0.
Dá pra configurar tudo pelo Inspetor, mas montar por código deixa claro o que cada coisa faz:
extends Line2D
@export var max_pontos: int = 20
func _ready() -> void:
top_level = true
clear_points()
var curva := Curve.new()
curva.add_point(Vector2(0.0, 0.0))
curva.add_point(Vector2(1.0, 1.0))
width_curve = curva
var grad := Gradient.new()
grad.set_color(0, Color(0.4, 0.8, 1.0, 0.0))
grad.set_color(1, Color(0.4, 0.8, 1.0, 1.0))
gradient = grad
func _process(_delta: float) -> void:
add_point(get_parent().global_position)
while get_point_count() > max_pontos:
remove_point(0)
Agora a ponta velha do rastro é fina e transparente, e perto do personagem ela é cheia e opaca. O efeito de dissolver aparece de graça, porque conforme os pontos avançam pela lista eles passam pela parte fina e transparente da curva e do gradiente antes de serem removidos.
Para um visual mais redondo, ligue joint_mode em Line2D.LINE_JOINT_ROUND e begin_cap_mode/end_cap_mode em Line2D.LINE_CAP_ROUND no Inspetor. Sem cantos vivos o rastro fica bem mais agradável em curvas.
Limitando pontos por distância em vez de por frame
Adicionar um ponto a cada frame tem um problema: parado, o personagem empilha vários pontos no mesmo lugar e gasta posições da lista à toa. Quando ele anda devagar, o rastro mal aparece, e quando o jogo cai de FPS o comportamento muda. Uma solução melhor é só adicionar um ponto quando o personagem se afastou o suficiente do último ponto registrado.
extends Line2D
@export var max_pontos: int = 20
@export var dist_minima: float = 6.0
func _ready() -> void:
top_level = true
clear_points()
func _process(_delta: float) -> void:
var pos: Vector2 = get_parent().global_position
if get_point_count() == 0:
add_point(pos)
return
var ultimo: Vector2 = get_point_position(get_point_count() - 1)
if pos.distance_to(ultimo) >= dist_minima:
add_point(pos)
while get_point_count() > max_pontos:
remove_point(0)
Assim o espaçamento entre pontos fica constante independente da velocidade ou do frame rate, e parar não consome a lista. Esse padrão combina bem com mecânicas rápidas como um dash de personagem 2D, onde o rastro precisa ser claro e consistente.
Limpando o rastro quando o personagem teleporta
Se o personagem some de um lugar e aparece em outro (teleporte, respawn, troca de sala), os pontos antigos vão criar uma linha reta gigante ligando os dois lugares. Resolva chamando clear_points() no momento da mudança. Como o Line2D é filho, dá pra acessar de fora:
# no script do personagem, ao teleportar
func teleportar(novo_destino: Vector2) -> void:
global_position = novo_destino
$Line2D.clear_points()
Vale também esvaziar o rastro aos poucos quando o personagem para, para a linha não ficar congelada atrás dele. Para isso, remova um ponto antigo de tempos em tempos mesmo sem adicionar novos. Um Timer ou um acumulador de delta resolvem.
A alternativa: fantasmas (afterimage) com Sprite2D e Tween
O Line2D desenha uma forma geométrica. Quando você quer que o rastro seja a própria silhueta do personagem (estilo afterimage de jogos de luta e de ação), a abordagem é outra: a cada intervalo você instancia um Sprite2D com a mesma textura e quadro do personagem na posição atual, e deixa esse fantasma desaparecer sozinho.
Crie uma cena Fantasma.tscn com um único Sprite2D na raiz e este script. Ele recebe a textura e configura o próprio fade no _ready:
extends Sprite2D
@export var duracao: float = 0.35
func iniciar(tex: Texture2D, frame_atual: int, hframes: int, vframes: int, virado: bool) -> void:
texture = tex
self.hframes = hframes
self.vframes = vframes
frame = frame_atual
flip_h = virado
func _ready() -> void:
modulate = Color(0.6, 0.8, 1.0, 0.5)
var tw := create_tween()
tw.tween_property(self, "modulate:a", 0.0, duracao)
tw.tween_callback(queue_free)
O create_tween anima só o canal alfa do modulate até zero ao longo de duracao e, no fim, libera o nó com queue_free. Se quiser entender melhor como o Tween encadeia animações por código, vale a leitura sobre animação com Tween no Godot.
Agora, no personagem, instancie um fantasma de tempos em tempos. Use um acumulador para não depender do frame rate:
extends CharacterBody2D
@export var fantasma_cena: PackedScene
@export var intervalo: float = 0.05
var _acumulador: float = 0.0
@onready var sprite: Sprite2D = $Sprite2D
func _physics_process(delta: float) -> void:
# ... seu movimento normal aqui ...
if velocity.length() > 10.0:
_acumulador += delta
if _acumulador >= intervalo:
_acumulador = 0.0
_criar_fantasma()
else:
_acumulador = intervalo
func _criar_fantasma() -> void:
var f := fantasma_cena.instantiate()
get_parent().add_child(f)
f.global_position = global_position
f.iniciar(sprite.texture, sprite.frame, sprite.hframes, sprite.vframes, sprite.flip_h)
Repare que o fantasma é adicionado ao pai do personagem (get_parent().add_child(f)), não ao próprio personagem. Se fosse filho do personagem, ele andaria junto e nunca ficaria pra trás. Adicionando ao pai, ele fica fixo no mundo na posição onde foi criado, exatamente como queremos. Copiar frame, hframes e vframes garante que o fantasma mostre o mesmo quadro de animação que o personagem tinha naquele instante.
Quando usar cada abordagem
Use o Line2D quando o rastro for um efeito abstrato: uma faixa de luz, um traço de velocidade, um cometa. Ele é mais leve, tem comprimento previsível e os controles de width_curve e gradient dão muito acabamento com pouco esforço. É a primeira escolha para a maioria dos casos.
Use fantasmas quando o rastro precisar reproduzir a forma exata do personagem, com a animação congelada em cada cópia. É o que dá aquele efeito de "vários personagens" durante um dash ou um golpe rápido. O custo é maior porque cada fantasma é um nó separado com seu próprio Tween, então controle a frequência e a duração para não acumular muitos ao mesmo tempo.
Nada impede combinar os dois: uma Line2D fina para o traço de velocidade mais alguns fantasmas no pico do dash costuma render um resultado bem bonito.
Dicas de performance
Algumas regras simples mantêm o rastro barato. Limite o número de pontos da Line2D (o max_pontos); 20 a 40 já cobre quase tudo e remover os antigos é o que impede a lista de crescer pra sempre. Adicione pontos por distância, não por frame, para o custo não escalar com o FPS e o comportamento ficar igual em máquinas diferentes.
Sempre marque top_level = true no Line2D. Além de corrigir o problema da herança de transform, isso evita recalcular a posição dos pontos relativos ao pai a cada movimento do personagem.
Para os fantasmas, o ponto crítico é a frequência. Um intervalo de 0.05 segundos gera 20 fantasmas por segundo, o que já é bastante. Combine isso com uma duracao curta (0.3 a 0.4 segundos) para que os fantasmas se autodestruam rápido e nunca tenha muitos vivos de uma vez. E só gere fantasmas quando o personagem realmente se move acima de uma velocidade mínima, como no código acima, para não desperdiçar nós com o personagem parado.
Se você precisa de muitas partículas pequenas saindo junto do rastro (poeira, faíscas, brilho), não use sprites individuais para isso. Um sistema dedicado como o que vimos em partículas com GPUParticles2D roda na GPU e aguenta milhares de partículas sem pesar na lógica do jogo.
Fechando
Com essas duas técnicas você cobre praticamente qualquer rastro 2D que um jogo precisa. O Line2D resolve traços de velocidade e efeitos de luz com pouquíssimo código e ótimo controle de fade via width_curve e gradient. Os fantasmas com Tween entregam o afterimage com a silhueta do personagem para os momentos de mais impacto. A partir daqui, brinque com cores, durações e número de pontos até o rastro casar com a sensação que o seu jogo precisa transmitir. O efeito é barato, e o ganho de leitura visual é grande.

