Voltar para o Blog
Quest Log

Máquina de Estados na Unity: FSM para Jogador e Inimigo

Diagrama de estados com caixas brilhantes ligadas por setas ao lado de um personagem 2D pulando

Unity state machine em C#: FSM com IState (Enter, Tick, Exit) para jogador parado, andando e pulando e inimigo que patrulha, persegue e ataca, sem if aninhado.

Ferramenta:UnityC#

Máquina de Estados na Unity: FSM para Jogador e Inimigo

Máquina de estados na Unity é a forma de dizer, em código, que o personagem está em exatamente um estado por vez (parado, andando, pulando) e que cada estado sabe o que fazer e para onde ir. Uma unity state machine bem feita troca aquele Update cheio de if (isGrounded && !isJumping && !isAttacking) por classes pequenas, cada uma com Enter, Tick e Exit. Este tutorial mostra a versão simples com enum, o ponto em que ela quebra e a versão com classes aplicada ao jogador e a um inimigo que patrulha, persegue e ataca.

Todo o código usa a API do Unity 6.3 LTS (Rigidbody2D.linearVelocity) e o Input System, que vem ativo nos projetos novos.

O que é máquina de estados e por que usar

Escreva o movimento de um personagem sem estrutura e você chega rápido a algo assim: se está no chão e apertou pular, pula; se está no ar, não pode pular de novo, a não ser que tenha pulo duplo; se está atacando, não anda; se tomou dano, fica atordoado, mas só se não estiver rolando. Cada regra nova cruza com todas as anteriores. Os bugs aparecem como "o personagem anda durante a animação de ataque" ou "pulou duas vezes encostado na parede".

A máquina de estados finita (FSM) corta esse emaranhado com uma regra: o personagem está em um estado só. Em vez de perguntar "posso andar?" combinando cinco booleanos, você pergunta "em que estado estou?". O estado Attack simplesmente não tem código de andar, então não existe como andar atacando.

O conceito é o mesmo em qualquer engine. Se você já leu o post sobre máquina de estados no Godot, a estrutura abaixo vai parecer familiar; muda a API, não a ideia. E ela é um dos padrões clássicos descritos em padrões de projeto para jogos.

Versão simples com enum e switch

Para começar, um enum e um switch resolvem. Arquivo SimplePlayerFSM.cs:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody2D))]
public class SimplePlayerFSM : MonoBehaviour
{
    private enum PlayerState { Idle, Run, Jump }

    [SerializeField] private float moveSpeed = 6f;
    [SerializeField] private float jumpForce = 12f;
    [SerializeField] private Transform groundCheck;
    [SerializeField] private LayerMask groundLayer;

    private Rigidbody2D rb;
    private InputAction moveAction;
    private InputAction jumpAction;
    private PlayerState state = PlayerState.Idle;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        moveAction = InputSystem.actions.FindAction("Player/Move");
        jumpAction = InputSystem.actions.FindAction("Player/Jump");
    }

    private void Update()
    {
        float x = moveAction.ReadValue<Vector2>().x;
        bool grounded = Physics2D.OverlapCircle(groundCheck.position, 0.1f, groundLayer);

        switch (state)
        {
            case PlayerState.Idle:
                if (jumpAction.WasPressedThisFrame() && grounded) { Jump(); }
                else if (Mathf.Abs(x) > 0.01f) { state = PlayerState.Run; }
                break;
            case PlayerState.Run:
                rb.linearVelocity = new Vector2(x * moveSpeed, rb.linearVelocity.y);
                if (jumpAction.WasPressedThisFrame() && grounded) { Jump(); }
                else if (Mathf.Abs(x) <= 0.01f) { state = PlayerState.Idle; }
                break;
            case PlayerState.Jump:
                rb.linearVelocity = new Vector2(x * moveSpeed, rb.linearVelocity.y);
                if (grounded && rb.linearVelocity.y <= 0f) { state = PlayerState.Idle; }
                break;
        }
    }

    private void Jump()
    {
        rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
        state = PlayerState.Jump;
    }
}

InputSystem.actions é o asset de ações do projeto (o InputSystem_Actions que vem no template do Unity 6), e as ações Move e Jump do mapa Player já existem nele. Para o ground check, crie um filho vazio nos pés do personagem, arraste para groundCheck e coloque o chão numa layer própria.

Quando o enum não aguenta mais

O enum funciona até o dia em que um estado precisa de código ao entrar e ao sair. O pulo quer tocar som e partícula ao começar. O ataque quer ligar uma hitbox ao entrar e desligar ao sair. O atordoamento tem um timer próprio. No switch, você começa a criar variáveis como previousState, attackTimer, stunTimer no corpo da classe, e o case de cada estado começa a verificar se acabou de entrar nele.

Sinais claros de que é hora de mudar:

  • O switch passou de 80 linhas.
  • Há variáveis no MonoBehaviour usadas por um estado só.
  • Você escreve if (state != previousState) para detectar entrada.
  • Dois estados compartilham código copiado.

FSM com classes: IState, Enter, Tick e Exit

A versão com classes dá a cada estado o próprio arquivo, com três momentos bem definidos. Arquivo IState.cs:

public interface IState
{
    void Enter();
    void Tick(float deltaTime);
    void Exit();
}

Arquivo StateMachine.cs, a máquina em si, que não sabe nada de jogador nem de inimigo:

public class StateMachine
{
    public IState Current { get; private set; }

    public void ChangeState(IState next)
    {
        if (next == null || next == Current)
        {
            return;
        }

        Current?.Exit();
        Current = next;
        Current.Enter();
    }

    public void Tick(float deltaTime)
    {
        Current?.Tick(deltaTime);
    }
}

Por que uma classe comum e não um MonoBehaviour? Porque a máquina não precisa estar na cena. O dono (jogador ou inimigo) cria a máquina e chama Tick no próprio Update. Os estados também são classes comuns, que recebem o dono no construtor. Isso deixa cada estado testável e sem custo de componente.

Exemplo do jogador

O PlayerController guarda o que todos os estados compartilham (física, input, parâmetros) e cria os estados uma vez só. Arquivo PlayerController.cs:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 6f;
    [SerializeField] private float jumpForce = 12f;
    [SerializeField] private Transform groundCheck;
    [SerializeField] private LayerMask groundLayer;

    private InputAction moveAction;
    private InputAction jumpAction;
    private readonly StateMachine machine = new StateMachine();

    public Rigidbody2D Rb { get; private set; }
    public float MoveSpeed => moveSpeed;
    public float JumpForce => jumpForce;
    public float MoveX => moveAction.ReadValue<Vector2>().x;
    public bool JumpPressed => jumpAction.WasPressedThisFrame();
    public bool IsGrounded => Physics2D.OverlapCircle(groundCheck.position, 0.1f, groundLayer);

    public PlayerIdleState Idle { get; private set; }
    public PlayerRunState Run { get; private set; }
    public PlayerJumpState Jump { get; private set; }

    private void Awake()
    {
        Rb = GetComponent<Rigidbody2D>();
        moveAction = InputSystem.actions.FindAction("Player/Move");
        jumpAction = InputSystem.actions.FindAction("Player/Jump");

        Idle = new PlayerIdleState(this);
        Run = new PlayerRunState(this);
        Jump = new PlayerJumpState(this);
    }

    private void Start()
    {
        machine.ChangeState(Idle);
    }

    private void Update()
    {
        machine.Tick(Time.deltaTime);
    }

    public void ChangeState(IState next)
    {
        machine.ChangeState(next);
    }
}

Os três estados, cada um no seu arquivo. PlayerIdleState.cs:

using UnityEngine;

public class PlayerIdleState : IState
{
    private readonly PlayerController player;

    public PlayerIdleState(PlayerController player) { this.player = player; }

    public void Enter()
    {
        player.Rb.linearVelocity = new Vector2(0f, player.Rb.linearVelocity.y);
    }

    public void Tick(float deltaTime)
    {
        if (player.JumpPressed && player.IsGrounded) { player.ChangeState(player.Jump); return; }
        if (Mathf.Abs(player.MoveX) > 0.01f) { player.ChangeState(player.Run); }
    }

    public void Exit() { }
}

PlayerRunState.cs:

using UnityEngine;

public class PlayerRunState : IState
{
    private readonly PlayerController player;

    public PlayerRunState(PlayerController player) { this.player = player; }

    public void Enter() { }

    public void Tick(float deltaTime)
    {
        float x = player.MoveX;
        player.Rb.linearVelocity = new Vector2(x * player.MoveSpeed, player.Rb.linearVelocity.y);

        if (player.JumpPressed && player.IsGrounded) { player.ChangeState(player.Jump); return; }
        if (Mathf.Abs(x) <= 0.01f) { player.ChangeState(player.Idle); }
    }

    public void Exit() { }
}

PlayerJumpState.cs:

using UnityEngine;

public class PlayerJumpState : IState
{
    private readonly PlayerController player;
    private float timeInState;

    public PlayerJumpState(PlayerController player) { this.player = player; }

    public void Enter()
    {
        timeInState = 0f;
        player.Rb.linearVelocity = new Vector2(player.Rb.linearVelocity.x, player.JumpForce);
    }

    public void Tick(float deltaTime)
    {
        timeInState += deltaTime;
        player.Rb.linearVelocity = new Vector2(player.MoveX * player.MoveSpeed, player.Rb.linearVelocity.y);

        bool falling = player.Rb.linearVelocity.y <= 0f;
        if (timeInState > 0.1f && falling && player.IsGrounded)
        {
            player.ChangeState(Mathf.Abs(player.MoveX) > 0.01f ? player.Run : player.Idle);
        }
    }

    public void Exit() { }
}

Repare no timeInState: logo depois do impulso, o ground check ainda encosta no chão por um frame ou dois. Sem a espera curta, o pulo voltaria para Idle no mesmo instante. Esse tipo de detalhe fica contido no estado que precisa dele, sem poluir o resto. O movimento escreve em linearVelocity dentro do Update, o que é aceitável para velocidade direta; se você aplicar forças, leve essa parte para um FixedTick chamado no FixedUpdate.

Uma FSM organiza um personagem. O jogo de verdade tem vários personagens, cada um com a sua, conversando com animação, câmera, UI, som e fases. Fazer esse conjunto crescer sem virar um emaranhado de dependências é a parte difícil de aprender sozinho, porque nenhum tutorial isolado mostra o projeto inteiro de pé.

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

Exemplo do inimigo

O inimigo usa a mesma StateMachine e a mesma IState. Muda o dono e os estados: patrulha entre dois pontos, persegue quando vê o jogador, ataca quando chega perto. Arquivo EnemyBrain.cs:

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class EnemyBrain : MonoBehaviour
{
    [SerializeField] private Transform pointA;
    [SerializeField] private Transform pointB;
    [SerializeField] private Transform player;
    [SerializeField] private float patrolSpeed = 2f;
    [SerializeField] private float chaseSpeed = 4f;
    [SerializeField] private float sightRange = 6f;
    [SerializeField] private float attackRange = 1.2f;
    [SerializeField] private float attackCooldown = 0.8f;

    private readonly StateMachine machine = new StateMachine();

    public Rigidbody2D Rb { get; private set; }
    public Transform PointA => pointA;
    public Transform PointB => pointB;
    public float PatrolSpeed => patrolSpeed;
    public float ChaseSpeed => chaseSpeed;
    public float AttackCooldown => attackCooldown;
    public float DistanceToPlayer => Vector2.Distance(transform.position, player.position);
    public bool CanSeePlayer => DistanceToPlayer <= sightRange;
    public bool InAttackRange => DistanceToPlayer <= attackRange;
    public Vector2 PlayerPosition => player.position;

    public EnemyPatrolState Patrol { get; private set; }
    public EnemyChaseState Chase { get; private set; }
    public EnemyAttackState Attack { get; private set; }

    private void Awake()
    {
        Rb = GetComponent<Rigidbody2D>();
        Patrol = new EnemyPatrolState(this);
        Chase = new EnemyChaseState(this);
        Attack = new EnemyAttackState(this);
    }

    private void Start() { machine.ChangeState(Patrol); }

    private void Update() { machine.Tick(Time.deltaTime); }

    public void ChangeState(IState next) { machine.ChangeState(next); }

    public void MoveTowards(Vector2 target, float speed)
    {
        float dirX = Mathf.Sign(target.x - Rb.position.x);
        Rb.linearVelocity = new Vector2(dirX * speed, Rb.linearVelocity.y);
    }
}

EnemyPatrolState.cs:

using UnityEngine;

public class EnemyPatrolState : IState
{
    private readonly EnemyBrain enemy;
    private Transform currentTarget;

    public EnemyPatrolState(EnemyBrain enemy) { this.enemy = enemy; }

    public void Enter() { currentTarget = enemy.PointA; }

    public void Tick(float deltaTime)
    {
        if (enemy.CanSeePlayer) { enemy.ChangeState(enemy.Chase); return; }

        enemy.MoveTowards(currentTarget.position, enemy.PatrolSpeed);
        if (Mathf.Abs(currentTarget.position.x - enemy.Rb.position.x) < 0.2f)
        {
            currentTarget = currentTarget == enemy.PointA ? enemy.PointB : enemy.PointA;
        }
    }

    public void Exit() { }
}

EnemyChaseState.cs:

public class EnemyChaseState : IState
{
    private readonly EnemyBrain enemy;

    public EnemyChaseState(EnemyBrain enemy) { this.enemy = enemy; }

    public void Enter() { }

    public void Tick(float deltaTime)
    {
        if (!enemy.CanSeePlayer) { enemy.ChangeState(enemy.Patrol); return; }
        if (enemy.InAttackRange) { enemy.ChangeState(enemy.Attack); return; }
        enemy.MoveTowards(enemy.PlayerPosition, enemy.ChaseSpeed);
    }

    public void Exit() { }
}

EnemyAttackState.cs:

using UnityEngine;

public class EnemyAttackState : IState
{
    private readonly EnemyBrain enemy;
    private float cooldown;

    public EnemyAttackState(EnemyBrain enemy) { this.enemy = enemy; }

    public void Enter()
    {
        enemy.Rb.linearVelocity = new Vector2(0f, enemy.Rb.linearVelocity.y);
        cooldown = 0f;
    }

    public void Tick(float deltaTime)
    {
        if (!enemy.InAttackRange) { enemy.ChangeState(enemy.Chase); return; }

        cooldown -= deltaTime;
        if (cooldown <= 0f)
        {
            Debug.Log("Inimigo ataca", enemy);
            cooldown = enemy.AttackCooldown;
        }
    }

    public void Exit() { }
}

As transições do inimigo ficam explícitas: Patrol vai para Chase quando vê o jogador; Chase vai para Attack quando chega perto e volta para Patrol quando perde o jogador de vista; Attack volta para Chase quando o jogador se afasta. Quer um estado de "procurar na última posição vista"? Crie EnemySearchState, ligue em Chase no lugar da volta direta para Patrol, e nenhum outro estado muda. A parte de perseguição e patrulha com mais detalhe (visão, pathfinding) está no post sobre IA de inimigo que persegue e patrulha.

FSM do código vs Animator: quem manda em quem

O Animator Controller também é uma máquina de estados, e é tentador colocar a lógica do jogo nele: transição de Idle para Run quando o parâmetro Speed passa de 0.1, e o código perguntando ao Animator em que estado está. Isso funciona em protótipo e complica rápido, porque as regras de jogo ficam espalhadas entre código e grafo de animação, com tempo de transição e exit time interferindo na lógica.

A divisão que recomendo: o código decide, o Animator mostra. A FSM em C# é a fonte da verdade. No Enter de cada estado, você avisa o Animator:

// dentro de PlayerJumpState.Enter(), com um campo Animator exposto no PlayerController
player.Animator.CrossFade(PlayerController.JumpHash, 0.05f);

Com public static readonly int JumpHash = Animator.StringToHash("Jump"); no PlayerController e um Animator exposto como propriedade, o estado de animação passa a ser consequência do estado de jogo. O Animator fica com o que ele faz bem (blend, transições suaves, eventos de animação), e o código fica com as regras.

Para IA com muitas ramificações, a Unity mantém o pacote oficial Behavior (com.unity.behavior), um editor visual de behavior graphs. Em outubro de 2026 a página do pacote no manual lista a versão 1.0.16. Ele faz sentido quando a IA tem dezenas de decisões encadeadas e alguém que não programa precisa editá-las. Para jogador e inimigos com três a seis estados, a FSM em C# deste post é mais direta de ler e depurar.

Erros comuns na máquina de estados

Trocar de estado e continuar executando o Tick antigo. Se o Tick chama ChangeState e segue rodando o código de baixo, o estado que acabou de sair ainda mexe na velocidade naquele frame. Por isso os exemplos usam return logo depois de cada troca. Trate ChangeState como a última coisa que o estado faz no frame.

Criar estado novo a cada troca. Escrever ChangeState(new PlayerRunState(this)) funciona, mas aloca um objeto a cada transição e perde qualquer dado guardado no estado. Crie os estados uma vez no Awake, como no PlayerController, e reutilize.

Esquecer o Exit. Estado que liga algo no Enter (hitbox de ataque, partícula, colisor de escudo) tem que desligar no Exit. Se não, a próxima transição deixa a hitbox ligada e o inimigo leva dano andando.

Transição nos dois sentidos no mesmo frame. Chase manda para Attack quando a distância é menor que 1.2, e Attack manda de volta quando é maior que 1.2. Com o jogador parado bem na borda, o inimigo pisca entre os dois estados. A correção é uma folga (histerese): entrar em Attack abaixo de 1.2 e sair só acima de 1.5.

Estado sabendo demais. Se PlayerJumpState começa a ler a vida do jogador, o inventário e a câmera, a lógica voltou a se espalhar. O estado lê o que o dono expõe e decide transições; regra de outros sistemas fica em outros sistemas, avisada por evento.

Próximo passo

Monte o PlayerController com os três estados e acrescente um quarto, PlayerFallState, para quando o personagem sai de uma plataforma sem pular. Repare quantos arquivos você precisou mexer: o estado novo e as transições que levam a ele. Depois aplique a mesma estrutura num inimigo do seu projeto e compare com o código antigo.

Perguntas frequentes

O que é uma máquina de estados finita em jogos?

É uma forma de organizar o comportamento em estados exclusivos, como parado, andando e pulando, em que o objeto está em exatamente um por vez. Cada estado cuida da própria lógica e decide para qual outro estado ir. Isso troca dezenas de booleanos soltos por uma pergunta simples: em que estado estou agora?

Devo usar enum ou classes para a FSM na Unity?

Enum com switch serve para três ou quatro estados com pouca lógica. Quando cada estado precisa de código de entrada e saída, timers próprios e regras de transição, classes com uma interface como IState ficam mais fáceis de ler, testar e estender.

O Animator da Unity já não é uma máquina de estados?

É, mas de animação. Ele é ótimo para decidir qual clipe tocar e como misturar, e ruim para guardar regra de jogo. O caminho mais estável é a FSM do código decidir o estado e avisar o Animator, e não o contrário.

O que é o pacote Unity Behavior?

É o pacote oficial com.unity.behavior, um editor visual de behavior graphs para IA de personagens. Ele serve quando a IA tem muitas ramificações e você quer editá-la visualmente. Para um jogador ou um inimigo com três ou quatro estados, uma FSM em C# costuma ser mais simples de manter.