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.
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.
Outros usos do mesmo rastro
O personagem é o exemplo óbvio, mas o Line2D que você montou serve para qualquer coisa que se move e merece ser lida pelo jogador.
Projéteis. Uma flecha, uma bala mágica ou um projétil teleguiado ficam muito mais legíveis com um rastro curto atrás. A receita é idêntica: o Line2D vira filho do projétil, com top_level ligado, e segue a global_position dele. Use poucos pontos (de 8 a 12) e dist_minima baixa, porque o projétil é rápido e o traço precisa ser curto e nítido. Se você ainda está montando a parte de disparo, o passo a passo está em como atirar um projétil 2D no Godot, e o rastro entra como acabamento por cima.
Armas brancas e golpes. Em jogo de ação, um corte de espada ganha peso com um arco luminoso. Aqui o top_level e o gradiente fazem o trabalho pesado: um Line2D preso à ponta da arma, alimentado só durante o frame do golpe e limpo com clear_points() logo depois, desenha o arco do movimento sem persistir. Casar isso com um gradient de branco para transparente dá o traço clássico de slash.
Composição com game feel. O rastro raramente age sozinho nos momentos de impacto. Um dash bom combina o afterimage com um tremor de tela (camera shake) e um pequeno congelamento no acerto. É o empilhamento barato de efeitos que transforma uma ação funcional numa ação que o jogador sente. Cada um desses detalhes custa pouco e some rápido, que é exatamente o que você quer num feedback de ação.
Pooling: reaproveitar fantasmas em vez de criar e destruir
O ponto fraco dos fantasmas é que cada instantiate() seguido de queue_free() gera trabalho para o coletor de lixo. Num dash ocasional isso não pesa, mas se o jogo cria fantasmas o tempo todo (vários personagens, inimigos rápidos, projéteis em quantidade), vale usar um pool: um conjunto fixo de nós que você reaproveita, em vez de nascer e morrer a cada efeito.
A ideia é instanciar todos os fantasmas de uma vez no início, deixá-los escondidos, e ir "acordando" o próximo da fila quando precisar. Adapte o script do fantasma para ter um método que reinicia o fade em vez de se autodestruir:
extends Sprite2D
@export var duracao: float = 0.35
func reiniciar(tex: Texture2D, quadro: int, virado: bool) -> void:
texture = tex
frame = quadro
flip_h = virado
modulate = Color(0.6, 0.8, 1.0, 0.5)
visible = true
var tw := create_tween()
tw.tween_property(self, "modulate:a", 0.0, duracao)
tw.tween_callback(hide)
O gerenciador do pool guarda os nós num array e usa um índice circular para pegar sempre o próximo:
extends Node2D
@export var fantasma_cena: PackedScene
@export var tamanho_pool: int = 24
var _pool: Array[Sprite2D] = []
var _proximo: int = 0
func _ready() -> void:
for i: int in tamanho_pool:
var f: Sprite2D = fantasma_cena.instantiate()
add_child(f)
f.hide()
_pool.append(f)
func emitir(pos: Vector2, tex: Texture2D, quadro: int, virado: bool) -> void:
var f: Sprite2D = _pool[_proximo]
_proximo = (_proximo + 1) % _pool.size()
f.global_position = pos
f.reiniciar(tex, quadro, virado)
Agora o personagem chama pool.emitir(global_position, sprite.texture, sprite.frame, sprite.flip_h) no lugar de instanciar. Com tamanho_pool dimensionado para cobrir o pico (duração do fade dividida pelo intervalo de criação, com uma folga), o índice circular sempre encontra um fantasma livre, porque o mais antigo já terminou o fade. Zero alocação em runtime, zero trabalho para o coletor. Esse padrão de reaproveitar nós é uma das otimizações de maior retorno em 2D e aparece de novo em como otimizar o FPS e a performance no Godot.
Rastro que muda de cor com a velocidade
Um toque que dá muita vida: o rastro esquentar de cor conforme o objeto acelera. Fica ótimo em projéteis que ganham energia e em veículos. Para isso, abra mão do gradient (que fixa as cores) e controle o default_color do Line2D por código, interpolando entre uma cor de baixa e uma de alta velocidade:
extends Line2D
@export var cor_lenta: Color = Color(0.3, 0.6, 1.0)
@export var cor_rapida: Color = Color(1.0, 0.4, 0.2)
@export var vel_de_referencia: float = 600.0
@export var max_pontos: int = 24
func _ready() -> void:
top_level = true
clear_points()
func _process(_delta: float) -> void:
var corpo: CharacterBody2D = get_parent()
var fracao: float = clampf(corpo.velocity.length() / vel_de_referencia, 0.0, 1.0)
default_color = cor_lenta.lerp(cor_rapida, fracao)
add_point(corpo.global_position)
while get_point_count() > max_pontos:
remove_point(0)
O lerp entre as duas cores devolve o tom exato para a velocidade atual: azul frio parado, laranja quente no talo. Como aqui o default_color pinta a linha inteira de uma cor só, você mantém o fade de transparência pela width_curve (a ponta velha some encolhendo) em vez do gradiente. É a combinação ideal para o rastro de um carro em derrapagem: no tutorial de carro top-down 2D no Godot, o drift acontece justo em alta velocidade, então o rastro já nasce quente no momento certo.
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.
Perguntas frequentes
Como fazer um rastro (trail) 2D no Godot 4?
A forma mais leve é um Line2D filho do personagem com top_level ligado. A cada frame você chama add_point() com a global_position do personagem e remove o ponto mais antigo com remove_point(0) quando passa de um limite. O top_level evita que a linha ande junto com o pai e destrua o efeito.
Como fazer o rastro desaparecer aos poucos no Godot?
Use as propriedades width_curve e gradient do Line2D. A width_curve é uma Curve que controla a espessura de 0.0 (começo da linha, ponta velha) a 1.0 (fim, junto do personagem), e o gradient controla cor e transparência da mesma forma. Com a ponta velha fina e transparente, o rastro dissolve sozinho conforme os pontos avançam pela lista.
Line2D ou fantasmas (afterimage): qual usar no rastro?
Use Line2D quando o rastro for um efeito abstrato, como faixa de luz ou traço de velocidade: é um nó só, leve e com ótimo controle de fade. Use fantasmas com Sprite2D e Tween quando o rastro precisar reproduzir a silhueta exata do personagem com a animação congelada, como no dash de jogos de luta. Os fantasmas custam mais porque cada cópia é um nó com seu próprio Tween.
Como evitar que o rastro 2D derrube o FPS?
Limite os pontos do Line2D (20 a 40 cobrem quase tudo), adicione pontos por distância em vez de por frame para o custo não escalar com o FPS, e mantenha top_level ligado. Para os fantasmas, controle o intervalo de criação e use uma duração curta de fade para nunca ter muitos vivos ao mesmo tempo.
