Voltar para o Blog
Quest Log

Animação de Sprite 2D: Como Animar os Personagens do Seu Jogo

Sprite sheet de pixel art com os frames sequenciais do ciclo de corrida de um personagem 2D

Aprenda animação de sprite 2D na prática: sprite sheets, frames, AnimatedSprite2D no Godot 4, timing por FPS e como ligar a animação ao movimento.

Animação de Sprite 2D: Como Animar os Personagens do Seu Jogo

Animação de sprite 2D é o que separa um retângulo que desliza pela tela de um personagem que parece vivo. E a boa notícia: a parte técnica é bem menor do que parece. Você precisa entender o que é um sprite sheet, como a engine troca os frames, e como amarrar isso ao estado do personagem (parado, correndo, pulando). O resto é timing.

Esse tutorial cobre o fluxo completo no Godot 4: montar o sprite sheet, configurar o AnimatedSprite2D, tocar a animação certa na hora certa por código, lidar com animações que não fazem loop (ataque, dano) e ajustar o FPS até a coisa parecer boa. Todo código aqui é GDScript do Godot 4.x e roda como está.

O que é um sprite sheet (e por que usar)

Animação 2D clássica é troca de imagem: você mostra o frame 1, depois o frame 2, depois o 3, rápido o bastante pro olho ler movimento. Um sprite sheet é uma imagem única com todos esses frames lado a lado, geralmente numa grade. Um ciclo de corrida de 8 frames de 32x32 pixels vira uma imagem de 256x32.

Dá pra usar um arquivo separado por frame? Dá, e o Godot aceita. Mas o sprite sheet ganha em quase tudo:

  • Performance: uma textura só na memória da GPU significa menos trocas de textura durante o desenho. Em jogo com muitos personagens na tela, isso importa.
  • Organização: um arquivo por personagem (ou por ação) em vez de dezenas de PNGs soltos.
  • Workflow: Aseprite, Libresprite e praticamente toda ferramenta de pixel art exporta sprite sheet direto, com os frames já alinhados na grade.

A única regra que vale seguir com disciplina: todos os frames com o mesmo tamanho de célula. Se o personagem é 32x32 mas o ataque com a espada estendida ocupa mais, exporte a célula maior (48x32, por exemplo) pra todas as animações daquele personagem. Frames de tamanhos diferentes na mesma sheet viram dor de cabeça de alinhamento depois.

Um detalhe que poupa retrabalho: defina o "pé" do personagem na mesma altura em todos os frames. Se o chão visual muda de um frame pro outro, o personagem parece flutuar ou afundar enquanto anda, e você vai caçar esse bug achando que é física.

Animação de sprite 2D no Godot: AnimatedSprite2D

O Godot tem dois caminhos pra animar sprite: o AnimatedSprite2D, que é o node especializado nisso, e o combo Sprite2D + AnimationPlayer, que é mais geral. Começo pelo primeiro porque ele resolve o caso comum com menos atrito.

A estrutura de cena pra um personagem jogável fica assim:

Player (CharacterBody2D)
├── CollisionShape2D
└── AnimatedSprite2D

Montando as animações no SpriteFrames

O AnimatedSprite2D guarda as animações num resource chamado SpriteFrames. No Inspector, clique em Sprite Frames > New SpriteFrames e depois clique no resource pra abrir o painel de edição na parte de baixo do editor.

Nesse painel, o fluxo é:

  1. Crie uma animação nova pra cada ação e nomeie: idle, run, jump, fall, attack. Nomes minúsculos e consistentes, porque você vai digitar eles em código.
  2. Com a animação selecionada, clique no ícone de grade (Add frames from sprite sheet), escolha o PNG da sheet e informe quantas colunas e linhas a grade tem. O Godot fatia a imagem e você seleciona quais células entram, na ordem.
  3. Configure o FPS de cada animação (campo de velocidade no painel). Esse número é quantos frames trocam por segundo, e a gente volta nele na seção de timing.
  4. Marque o loop nas animações cíclicas (idle, run) e desmarque nas que tocam uma vez (attack, hit, death).

Se a arte é pixel art, tem um ajuste obrigatório: em Project Settings > Rendering > Textures, mude o Default Texture Filter pra Nearest. Sem isso o Godot suaviza a textura ao escalar e o pixel art vira uma mancha borrada.

Tocando animações por código

A API do AnimatedSprite2D é pequena e direta:

@onready var anim = $AnimatedSprite2D

func exemplos():
    anim.play("run")        # toca a animação
    anim.stop()             # para e volta pro frame inicial
    anim.pause()            # congela no frame atual
    anim.frame = 0          # pula pra um frame específico
    anim.flip_h = true      # espelha horizontalmente
    print(anim.animation)   # nome da animação atual

Um comportamento que trabalha a seu favor: chamar play("run") quando run já está tocando não reinicia a animação. Isso permite chamar play() todo frame dentro da lógica de movimento sem a animação ficar travada no primeiro frame, que é um bug clássico de quem vem de outras engines esperando outro comportamento.

Ligando a animação ao movimento do personagem

Animação solta não serve pra nada. O que você quer é: o estado do personagem decide a animação. Aqui está um controlador de plataforma completo com a parte visual amarrada:

extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

@onready var anim = $AnimatedSprite2D

func _physics_process(delta):
    if not is_on_floor():
        velocity.y += gravity * delta

    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    var direction = Input.get_axis("ui_left", "ui_right")
    if direction:
        velocity.x = direction * SPEED
        # Espelha o sprite em vez de desenhar frames virados pro outro lado.
        anim.flip_h = direction < 0
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)

    move_and_slide()
    _update_animation(direction)

func _update_animation(direction):
    if not is_on_floor():
        # Subindo é pulo, descendo é queda.
        anim.play("jump" if velocity.y < 0 else "fall")
    elif direction != 0:
        anim.play("run")
    else:
        anim.play("idle")

Três decisões nesse código valem comentário.

O flip_h corta sua sheet pela metade. Você desenha o personagem virado só pra um lado e espelha por código. A exceção é personagem assimétrico (espada sempre na mão direita, cicatriz num lado do rosto), aí precisa desenhar os dois lados.

A animação é decidida depois do move_and_slide(). É o movimento que dita o visual, nunca o contrário. Se você um dia trocar o controle (dash, escada, nado), a função _update_animation continua sendo o único lugar que traduz estado em animação.

Pulo e queda são animações separadas. Dividir o ar em "subindo" e "descendo" usando o sinal de velocity.y é barato e melhora muito a leitura do movimento. Com 2 ou 3 frames de cada já fica bom.

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

Animações que não fazem loop: ataque, dano e morte

Animação cíclica é simples: toca e esquece. O problema aparece com ação que toca uma vez e precisa terminar antes do personagem voltar ao normal. Se o seu ataque pode ser interrompido pela animação de run no frame seguinte, o jogador nunca vê o golpe.

A solução é um sinal que o AnimatedSprite2D emite quando a animação acaba, o animation_finished. Com await, o código fica linear:

var attacking = false

func _unhandled_input(event):
    if event.is_action_pressed("attack") and not attacking:
        attack()

func attack():
    attacking = true
    anim.play("attack")
    # Espera a animação de ataque terminar antes de liberar o estado.
    await anim.animation_finished
    attacking = false

func _update_animation(direction):
    # Enquanto ataca, nenhuma outra animação pode atropelar.
    if attacking:
        return
    if not is_on_floor():
        anim.play("jump" if velocity.y < 0 else "fall")
    elif direction != 0:
        anim.play("run")
    else:
        anim.play("idle")

Detalhe que derruba muita gente: o animation_finished só dispara se a animação não estiver em loop. Se o seu ataque nunca "termina", volte no painel do SpriteFrames e confira se o loop está desmarcado naquela animação.

Esse padrão de flag (attacking) aguenta bem um personagem com poucas ações. Quando o personagem acumula estados (ataque, dano, dash, escada, agachar), a quantidade de if cresce até virar espaguete. Esse é o momento de migrar pra uma máquina de estados, onde cada estado é dono da sua animação. Mas não comece por aí: pra protótipo e pra jogo pequeno, a flag resolve.

Timing: o FPS da animação é game design

Aqui está a parte que ninguém te conta: o número de frames importa menos que o ritmo deles. Os valores de FPS que funcionam pra pixel art na prática:

  • Idle: 4 a 6 FPS. Respiração lenta, quase parada.
  • Corrida: 8 a 12 FPS. Abaixo disso parece câmera lenta, acima o olho perde os frames.
  • Ataque: 12 a 15 FPS na descida do golpe. Ataque lento parece fraco, não importa o dano que cause.

Não trate esses números como lei, trate como ponto de partida. O método é o mesmo do tuning de física: roda, sente, ajusta. E lembre que FPS da animação não tem nada a ver com FPS do jogo: o jogo roda a 60, a animação troca frame no ritmo que você configurou no SpriteFrames.

Dois truques de timing que elevam o resultado sem desenhar nada novo:

Frames segurados. Em vez de distribuir o tempo igual, segure mais tempo nos frames de impacto e antecipação. No painel do SpriteFrames, cada frame tem um campo de duração relativa: um frame com duração 2.0 fica na tela o dobro do tempo. Um ataque com antecipação longa e golpe rápido parece muito mais forte que o mesmo desenho com timing uniforme.

speed_scale pra variação dinâmica. A propriedade multiplica a velocidade de qualquer animação em runtime. Personagem com buff de velocidade? anim.speed_scale = 1.5 e o ciclo de corrida acompanha, sem criar animação nova.

Sprite2D + AnimationPlayer: o outro caminho

O AnimatedSprite2D resolve troca de frames, e só. Quando a animação precisa de mais (mover a hitbox do ataque junto com o golpe, disparar som no frame exato do passo, escalar o sprite num squash and stretch), o combo é outro: um Sprite2D com a sheet inteira, animado por um AnimationPlayer.

O Sprite2D tem as propriedades hframes e vframes, que dizem quantas colunas e linhas a textura tem. Com elas configuradas, a propriedade frame escolhe qual célula aparece. No AnimationPlayer, você cria keyframes da propriedade frame ao longo do tempo, e na mesma timeline pode animar qualquer outra propriedade de qualquer node da cena: posição da hitbox, escala, modulate, chamadas de método.

Pra entender o mecanismo por baixo, dá até pra animar um Sprite2D na mão, sem AnimationPlayer:

extends Sprite2D

@export var fps = 10.0
var frame_acumulado = 0.0

func _process(delta):
    frame_acumulado += fps * delta
    frame = int(frame_acumulado) % (hframes * vframes)

Esse script é didático, não recomendação: em jogo de verdade, use AnimatedSprite2D pro caso simples e AnimationPlayer quando precisar sincronizar mais coisas que o frame. A regra prática que eu uso: começou a precisar de hitbox que acompanha o golpe ou som por frame, é hora do AnimationPlayer.

Erros comuns que fazem a animação parecer amadora

Depois de revisar muito projeto de aluno, os mesmos problemas aparecem em quase todos:

  1. Filtro de textura errado. Pixel art com filtro Linear fica borrado. É o erro número um, e a correção é um dropdown nas configurações do projeto.
  2. Animação reiniciando todo frame. Acontece quando se manipula frame = 0 ou se chama stop() seguido de play() dentro do loop. Confie no play(): ele já ignora a chamada se a animação atual é a mesma.
  3. Ataque em loop. Animação de ação com loop marcado nunca emite animation_finished e o personagem fica preso golpeando o ar.
  4. Tudo no mesmo FPS. Idle a 12 FPS parece um personagem com pressa de existir. Cada animação tem o seu ritmo.
  5. Pé desalinhado entre frames. O personagem "nada" no chão enquanto corre. Se o movimento parece estranho e a física está certa, abra a sheet e confira o alinhamento.

Fechando

O caminho da animação de sprite 2D é curto: sheet com células uniformes, SpriteFrames com uma animação nomeada por ação, play() decidido pelo estado do personagem depois do movimento, await animation_finished pras ações que tocam uma vez. Isso cobre o personagem jogável inteiro de um jogo de plataforma.

O que separa o resultado mediano do bom não é técnica, é timing. Pegue um ciclo de corrida qualquer e teste a 6, 10 e 14 FPS: o desenho é o mesmo e a sensação muda completamente. Esse experimento de dez minutos ensina mais sobre animação do que qualquer parágrafo que eu escreva aqui.