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.

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.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 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.

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.