Voltar para o Blog
Quest Log

Rastro (trail) de movimento 2D no Godot 4

Personagem 2D correndo deixando um rastro luminoso que se desfaz atrás dele numa cena de plataforma.

Aprenda a criar trail godot 2d com Line2D e fantasmas (afterimage). Rastro que encolhe, desaparece com gradiente e dicas de performance no Godot 4.

Ferramenta:Godot

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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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.