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
switchpassou 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é.
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.



