Voltar para o Blog
Quest Log

Script de Movimento 2D na Unity (C# com Input System)

Personagem 2D em pixel art correndo sobre plataformas dentro de uma janela de editor, com setas de velocidade saindo dos pés

Script de movimento Unity 2D pronto em C#: Input System, Rigidbody2D.linearVelocity, aceleração, virar o sprite e os erros que fazem o player passar na parede.

Ferramenta:UnityC#

Script de Movimento 2D na Unity (C# com Input System)

Um script de movimento Unity 2D que funciona de verdade lê o input pelo Input System, aplica velocidade no Rigidbody2D com linearVelocity, acelera e freia em vez de ligar e desligar, e vira o sprite para o lado certo. Aqui você monta o PlayerMovement2D.cs completo, pronto para colar num projeto do Unity 6.3 LTS, e entende por que cada linha está ali.

No fim você vai ter um personagem que anda com peso, não atravessa parede, não treme com a câmera e não gruda no muro quando pula encostado nele. Esses três problemas aparecem em quase todo primeiro jogo de plataforma, e todos têm causa conhecida.

O que o script de movimento 2D precisa fazer

Antes de abrir o editor, vale listar o trabalho. Um movimento horizontal de plataforma faz quatro coisas:

  1. Ler a intenção do jogador. Um valor de -1 a 1 no eixo X, vindo de teclado, controle ou touch.
  2. Transformar intenção em velocidade. Velocidade alvo = input vezes velocidade máxima.
  3. Chegar na velocidade alvo com uma curva. Aceleração ao sair do lugar, desaceleração ao soltar o botão. É isso que separa "anda" de "controla bem".
  4. Mostrar para onde está indo. Virar o sprite, e de preferência o ponto de onde sai o tiro junto.

O pulo fica de fora de propósito. Ele tem ground check, altura variável e coyote time, e merece o próprio script: está no tutorial de pulo na Unity 2D com ground check. Separar os dois deixa cada arquivo pequeno e fácil de ajustar.

Preparar o personagem: Rigidbody2D e collider

Crie um GameObject chamado Player com um SpriteRenderer. Depois adicione os componentes nesta ordem e ajuste no Inspector:

Rigidbody2D

  • Body Type: Dynamic.
  • Gravity Scale: 3 é um bom ponto de partida para plataforma (o padrão 1 deixa a queda lenta, com cara de lua).
  • Collision Detection: Continuous. Evita que o personagem atravesse colliders finos em alta velocidade.
  • Interpolate: Interpolate. Suaviza a posição desenhada entre os passos de física. Sem isso a câmera vai expor tremedeira.
  • Constraints: marque Freeze Rotation Z. Sem isso o personagem tomba quando bate na quina de uma plataforma.

CapsuleCollider2D

  • Direction: Vertical. A cápsula tem a base arredondada, então o personagem não engancha em emendas de tiles como acontece com um BoxCollider2D.

Physics Material 2D

  • Crie em Assets > Create > 2D > Physics Material 2D, coloque Friction 0 e arraste para o campo Material do collider do player. Isso resolve o "grudar na parede", que aparece lá nos erros comuns.

O chão e as paredes podem ser um Tilemap com Tilemap Collider 2D ou sprites com BoxCollider2D. Ponha tudo que é cenário numa layer Ground; o script de pulo vai precisar dela.

Ler input com o Input System

Projetos novos do Unity 6 já vêm com o pacote Input System instalado e com um asset chamado InputSystem_Actions configurado como project-wide actions (em Edit > Project Settings > Input System Package). Esse asset traz um mapa Player com ações prontas como Move, Jump, Look e Interact, e as project-wide actions já ficam habilitadas por padrão. No Unity 6.3 LTS, a versão do pacote listada no manual é a 1.20.1.

Na prática isso significa que você não precisa criar asset nem gerar classe C#. Basta pedir a ação pelo nome:

InputAction moveAction = InputSystem.actions.FindAction("Move");
Vector2 move = moveAction.ReadValue<Vector2>();

A ação Move devolve um Vector2: WASD e setas no teclado, analógico esquerdo no controle. Para plataforma você usa só o x. Guarde a referência da ação no Awake em vez de chamar FindAction todo frame: a busca é por string e não tem por que repetir.

Se o seu projeto veio de uma versão antiga e ainda usa Input.GetAxis, confira em Edit > Project Settings > Player > Active Input Handling. Em Input System Package (New) o código antigo para de funcionar; em Both os dois convivem.

Script movimento unity 2d completo

Crie o arquivo PlayerMovement2D.cs (o nome da classe tem que ser igual ao do arquivo) e cole:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerMovement2D : MonoBehaviour
{
    [Header("Velocidade")]
    [SerializeField] private float maxSpeed = 7f;
    [SerializeField] private float acceleration = 70f;
    [SerializeField] private float deceleration = 90f;
    [SerializeField] private float turnAcceleration = 120f;
    [SerializeField, Range(0f, 1f)] private float airControl = 0.65f;

    [Header("Referências")]
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private Transform visual;

    [Header("Chão (para o controle no ar)")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private Vector2 groundCheckSize = new Vector2(0.5f, 0.1f);
    [SerializeField] private LayerMask groundLayer;

    private InputAction moveAction;
    private float inputX;
    private bool facingRight = true;

    public bool FacingRight => facingRight;

    private void Awake()
    {
        if (rb == null)
        {
            rb = GetComponent<Rigidbody2D>();
        }

        moveAction = InputSystem.actions.FindAction("Move");
    }

    private void Update()
    {
        inputX = moveAction.ReadValue<Vector2>().x;

        if (inputX > 0.01f && !facingRight)
        {
            Flip();
        }
        else if (inputX < -0.01f && facingRight)
        {
            Flip();
        }
    }

    private void FixedUpdate()
    {
        float targetSpeed = inputX * maxSpeed;
        float currentSpeed = rb.linearVelocity.x;

        float rate;
        if (Mathf.Abs(targetSpeed) < 0.01f)
        {
            rate = deceleration;
        }
        else if (Mathf.Sign(targetSpeed) != Mathf.Sign(currentSpeed) && Mathf.Abs(currentSpeed) > 0.01f)
        {
            rate = turnAcceleration;
        }
        else
        {
            rate = acceleration;
        }

        if (!IsGrounded())
        {
            rate *= airControl;
        }

        float newSpeed = Mathf.MoveTowards(currentSpeed, targetSpeed, rate * Time.fixedDeltaTime);
        rb.linearVelocity = new Vector2(newSpeed, rb.linearVelocity.y);
    }

    private bool IsGrounded()
    {
        if (groundCheck == null)
        {
            return true;
        }

        return Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer) != null;
    }

    private void Flip()
    {
        facingRight = !facingRight;
        Transform target = visual != null ? visual : transform;
        Vector3 scale = target.localScale;
        scale.x *= -1f;
        target.localScale = scale;
    }

    private void OnDrawGizmosSelected()
    {
        if (groundCheck == null)
        {
            return;
        }

        Gizmos.color = Color.green;
        Gizmos.DrawWireCube(groundCheck.position, groundCheckSize);
    }
}

Configure no Inspector: arraste o próprio Rigidbody2D em Rb, crie um filho vazio GroundCheck encostado nos pés e arraste em Ground Check, e marque a layer Ground em Ground Layer. O campo Visual é opcional; ele aparece de novo na seção de virar o sprite.

Três decisões do script merecem explicação:

  • Input no Update, física no FixedUpdate. O input é amostrado a cada frame e a velocidade é aplicada no passo fixo da física. Ler input dentro do FixedUpdate funciona para valores contínuos como o Move, mas perde toques rápidos de botão; manter o padrão desde já evita surpresa quando o pulo entrar.
  • Só o X é escrito. new Vector2(newSpeed, rb.linearVelocity.y) preserva a velocidade vertical, que pertence à gravidade e ao pulo. No Unity 6 também existe rb.linearVelocityX = newSpeed;, que faz a mesma coisa em uma linha.
  • Time.fixedDeltaTime no MoveTowards. A aceleração está em unidades por segundo ao quadrado. Multiplicar pelo passo fixo garante o mesmo comportamento com 50 ou 60 passos de física por segundo. É o mesmo princípio do delta time: o resultado depende do tempo que passou, não de quantas vezes o código rodou.

Aceleração e desaceleração

O coração do script é este trecho:

float newSpeed = Mathf.MoveTowards(currentSpeed, targetSpeed, rate * Time.fixedDeltaTime);

MoveTowards anda da velocidade atual até a velocidade alvo, no máximo rate * dt por passo. Com maxSpeed = 7 e acceleration = 70, o personagem leva 0,1 segundo para chegar na velocidade máxima. Com deceleration = 90, freia em menos de 0,08 segundo. Esses números dão um movimento responsivo, com um mínimo de inércia que o olho percebe.

Por que três taxas e não uma?

  • acceleration controla a arrancada. Valor alto (100+) deixa o controle seco, estilo Celeste. Valor baixo (20 a 30) dá sensação de personagem pesado, estilo plataforma com gelo.
  • deceleration controla o freio ao soltar. Se for menor que a aceleração, o personagem "desliza" um pouco ao parar, o que costuma parecer descontrole.
  • turnAcceleration entra quando o jogador inverte a direção com o personagem ainda andando. Sem essa taxa separada, virar parece lento, porque a velocidade precisa primeiro ir a zero e depois crescer do outro lado com a aceleração normal.

O airControl multiplica tudo quando o personagem está no ar. Com 0.65, mudar de direção no meio do pulo continua possível, mas custa mais. Coloque 1 para controle total no ar (comum em jogos de precisão) ou 0.3 para pulos mais comprometidos.

A forma de testar: deixe o jogo rodando e mexa nos valores pelo Inspector com o Play ligado. Quando achar o ponto, copie o componente (os três pontinhos, Copy Component), saia do Play e use Paste Component Values. Ajuste fino de sensação é o assunto do post de game feel e juice, que vale ler depois que o movimento estiver no lugar.

Um personagem que anda bem ainda está longe de um jogo. Na hora de juntar este script com pulo, animação, câmera, inimigos e uma fase que tenha começo e fim, é comum o projeto virar uma pilha de scripts que se atropelam e ninguém mais sabe quem mexe na velocidade de quem. Essa costura entre sistemas é onde mais gente trava aprendendo sozinha, e é o que um curso de Unity organizado em projeto completo resolve melhor do que tutoriais soltos.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 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 Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Virar o sprite na direção: flipX, escala ou rotação

A busca "unity flip sprite" tem três respostas, e cada uma tem consequência diferente.

1. SpriteRenderer.flipX

spriteRenderer.flipX = inputX < 0f;

É o mais simples e só espelha o desenho. O transform não muda, então os filhos continuam no mesmo lugar: o FirePoint da arma continua do lado direito, o collider de ataque continua apontando para a direita, e partículas filhas não viram. O flipX também não vira hitbox. Se o seu BoxCollider2D tem offset (por exemplo, um personagem com uma espada que sai do corpo), o collider fica do lado errado quando ele olha para a esquerda. Serve bem para personagens simétricos sem filhos.

2. Escala X negativa

É o que o script acima usa no Flip(). Escala negativa espelha o objeto e todos os filhos, incluindo colliders 2D, pontos de spawn e partículas. A física 2D da Unity aceita escala negativa nos colliders. O cuidado: aplique no objeto Visual (um filho que segura sprite e pontos de ataque) quando o personagem tiver UI presa nele, como uma barra de vida, senão a barra também espelha e o texto fica de trás para frente.

3. Rotação de 180 graus no Y

transform.Rotate(0f, 180f, 0f);

Visualmente dá o mesmo resultado da escala negativa, com uma vantagem: transform.right passa a apontar para a esquerda. Isso deixa o código de tiro trivial, porque o projétil sai sempre em firePoint.right, sem if de direção. Para quem tem muito código que usa transform.right como "frente", é o jeito mais limpo.

Qual usar? Personagem simples sem filhos: flipX. Personagem com arma, ataque ou hitbox deslocada: escala negativa num filho Visual, ou rotação no Y se o seu código de tiro usa right. Escolha um e use no projeto inteiro, porque misturar os três num mesmo personagem gera o bug clássico do personagem que vira duas vezes e fica de costas.

Erros comuns: tremedeira, atravessar parede, grudar

Atravessar parede. Quase sempre é transform.position += ... ou transform.Translate(...) num objeto com Rigidbody2D. Isso teleporta o personagem para a nova posição a cada frame, ignorando colisões; a física só percebe a sobreposição depois e empurra de volta, quando consegue. Com velocidade alta, o collider pula a parede inteira entre um frame e outro. A correção é mover pelo Rigidbody2D (como no script) e manter Collision Detection em Continuous. Se mesmo assim atravessa, confira se a parede tem collider e se a layer dela colide com a do player em Edit > Project Settings > Physics 2D > Layer Collision Matrix.

Tremedeira com a câmera. O personagem se move em passos de física e a tela desenha em frames variáveis. Se a câmera segue a posição do transform no Update, ela pega o personagem em momentos diferentes do passo. Solução em duas partes: Interpolate no Rigidbody2D e câmera seguindo no LateUpdate, ou Cinemachine com o Brain no modo de atualização padrão.

Grudar na parede. O jogador pula encostado num muro, segura a direção do muro e o personagem fica parado no ar. É atrito: o collider empurra a parede e a fricção segura a queda. O Physics Material 2D com Friction 0 no collider do player resolve. Se você quer atrito no chão (para escorregar menos em rampas), aplique o material sem atrito só nas paredes.

velocity não existe ou dá aviso. Tutorial antigo. No Unity 6 é linearVelocity, e drag virou linearDamping.

Personagem lento demais no ar ou flutuando. Gravity Scale em 1. Suba para 3 ou 4 e ajuste a força do pulo depois.

Input não responde. A ação foi buscada por um nome que não existe no asset (o FindAction não diferencia maiúsculas, mas um erro de digitação devolve null) ou o Active Input Handling está em Input Manager (Old). Um Debug.Log(moveAction.ReadValue<Vector2>()) no Update mostra na hora se o problema é input ou física.

Próximo passo

Com o PlayerMovement2D no lugar, o próximo arquivo é o pulo. O tutorial de pulo na Unity 2D com ground check, altura variável e coyote usa o mesmo GroundCheck e a mesma layer Ground deste post, então os dois scripts encaixam sem mudar nada.

Antes de seguir, faça um exercício rápido: crie três presets de valores (pesado, padrão e seco) e troque entre eles no Play. Perceber como a aceleração muda a sensação do jogo é a base para todo ajuste de movimento que você vai fazer depois, em qualquer engine. Se quiser comparar com outra abordagem, o post sobre movimento de personagem de plataforma 2D faz a mesma lógica de aceleração e fricção no Godot, e ver as duas lado a lado ajuda a separar o que é conceito do que é API.

Perguntas frequentes

Devo mover o personagem 2D pelo transform ou pelo Rigidbody2D?

Se o personagem colide com o cenário, mova pelo Rigidbody2D (linearVelocity, AddForce ou MovePosition). Mexer direto em transform.position teleporta o objeto a cada frame e a física só descobre a parede depois que o collider já entrou nela. Transform puro serve para objetos sem colisão, como um efeito decorativo.

Por que Rigidbody2D.velocity dá erro ou aviso no Unity 6?

A Unity renomeou a propriedade para linearVelocity (e drag para linearDamping) no Unity 6. Tutoriais antigos usam rb.velocity; no Unity 6.3 LTS use rb.linearVelocity, ou linearVelocityX e linearVelocityY para mexer em um eixo só.

Como virar o sprite do personagem 2D na Unity?

Os três jeitos comuns são SpriteRenderer.flipX, escala X negativa no transform e rotação de 180 graus no eixo Y. O flipX só espelha o desenho; escala e rotação viram também os filhos, como o ponto de onde sai o tiro e as hitboxes.

Meu personagem 2D treme quando a câmera segue. Como resolvo?

Ligue Interpolate no Rigidbody2D e mova a câmera em LateUpdate (ou use Cinemachine). A física roda em passos fixos e a tela em frames variáveis; sem interpolação, o sprite salta entre posições de física e a câmera evidencia o salto.