Minha visão sobre o gerenciamento de estados de ação no desenvolvimento de jogos

20 de março de 2026
Daniel LuEngenheiro Full-Stack | Criador de Conteúdo

Neste artigo, a partir de erros comuns de sobreposição e falha na recuperação de estados de ação, e com base na minha experiência em uma equipe iniciante, compartilho como construir uma máquina de estados de ação de alta extensibilidade e baixo acoplamento no Unity usando um Pool de Estados Ativos e avaliação dinâmica de prioridade.

CategoriasTecnologiaUnityDesenvolvimento Web

O problema: O bug da sobreposição de estados de ação

Recentemente, entrei para uma equipe que está dando seus primeiros passos. Ao ajudá-los a organizar o sistema de combate e a lógica de representação visual, encontrei um bug clássico e muito irritante.

O cenário era o seguinte: O personagem recebe um Debuff de "Congelamento" que dura 5 segundos; nesse momento, a animação muda para [Congelado] (o personagem fica parado tremendo). No segundo 2, o personagem é atingido por um golpe pesado de um inimigo com efeito de tontura, o que ativa uma animação de [Tonto] de 2 segundos (o personagem leva as mãos à cabeça, zonzo).

O verdadeiro problema ocorre quando termina a animação de tontura: Quando os 2 segundos de tontura acabam, de acordo com a lógica interna, o personagem ainda deveria ter 1 segundo de Debuff de "Congelamento". No entanto, em vez de voltar para a animação de [Congelado], o personagem volta diretamente para o estado [Idle] padrão. Isso faz com que o jogador veja o personagem respirando normalmente em espera, mas não consiga movê-lo (porque a lógica ainda está "congelada"), criando uma desconexão total entre o visual e a lógica.

Ao revisar o código, percebi que o motivo era simples: o sistema anterior era baseado em gatilhos (triggers). Quando a tontura era ativada, forçava o Animator a reproduzi-la; quando terminava, ordenava ao Animator voltar ao estado padrão de forma bruta, sem considerar outros efeitos lógicos ativos.

A onipresença do problema e as limitações das soluções tradicionais

Pessoalmente, acredito que este problema seja extremamente comum no desenvolvimento de jogos. Sempre que houver sistemas complexos de Buffs ou efeitos de habilidades sobrepostos, surgirá o desafio de como gerenciar a prioridade e a recuperação das animações.

Muitas equipes, no início e para ver resultados rápidos, costumam adotar duas práticas que trazem grandes riscos e que eu costumo desaconselhar:

Risco A: Manipulação direta de variáveis do Animator

Cada sistema (Buffs, habilidades, danos) chama diretamente animator.SetBool("isFrozen", true). À medida que o sistema cresce, é impossível saber qual parte do código definiu o booleano como false e quando, gerando um caos na máquina de estados difícil de depurar.

Risco B: Abuso da rede de transições do Unity Mecanim

A janela do Animator é intuitiva quando há poucas linhas. Mas se você vincula a lógica de negócio às transições — configurando dezenas de condições como Speed > 0, isFrozen == true, isStunned == false — o Animator acaba parecendo uma "teia de aranha". Adicionar um único estado novo exige conectar infinitas linhas novas, e o custo de manutenção sobe de forma exponencial.

Minha estratégia: Pool de estados ativos e avaliação dinâmica de prioridade

Para resolver a sobreposição de estados e o desacoplamento, introduzi na equipe uma ideia: o Pool de Estados Ativos (Active State Pool) com avaliação dinâmica de prioridade.

A ideia central é uma só: O pedido do estado deve estar totalmente separado da execução do estado.

A camada de lógica (Buffs ou sistemas de habilidades) só se encarrega de Registrar e Remover o estado de ação que deseja. Um AnimationStateManager centralizado decide, em cada frame, qual ordem de animação enviar ao Animator.

Fluxograma do mecanismo principal:

Loading diagram...

Sob esta arquitetura, quando o [Tonto] de prioridade 100 termina e é removido do pool, o administrador reavalia e descobre que a maior prioridade restante é a de [Congelado] (prioridade 50). Então, ordena automaticamente ao Animator voltar para essa animação. O bug se resolve naturalmente.

Prática no Unity: Deixando o Animator puro

Com este sistema, a forma de usar o Animator no Unity muda. Podemos esquecer as redes complexas de linhas e condições. O Animator passa a ser um simples reprodutor de ações.

No Animator Controller, você só precisa arrastar todos os estados (States) que precisar sem conectá-los com linhas (ou conectá-los todos a partir do AnyState sem condições). Pelo código, você controla a mudança de estado diretamente com animator.CrossFade() ou animator.Play().

Exemplo de pseudocódigo do administrador central:

// 1. Estrutura de dados do pedido de estado
public class AnimStateRequest
{
    public string StateName; // Nome no Animator
    public int Priority;     // Prioridade
    public object Source;    // Origem (para validar ao remover, ex: instância do Buff)
}

// 2. Administrador centralizado
public class AnimationStateManager : MonoBehaviour
{
    private Animator _animator;
    private List<AnimStateRequest> _activeStates = new List<AnimStateRequest>();
    private string _currentPlayingState;

    // Registrar estado
    public void AddStateRequest(string stateName, int priority, object source)
    {
        _activeStates.Add(new AnimStateRequest { StateName = stateName, Priority = priority, Source = source });
        EvaluateHighestPriorityState();
    }

    // Remover registro
    public void RemoveStateRequest(object source)
    {
        _activeStates.RemoveAll(req => req.Source == source);
        EvaluateHighestPriorityState();
    }

    // Avaliação dinâmica do estado com maior prioridade
    private void EvaluateHighestPriorityState()
    {
        if (_activeStates.Count == 0)
        {
            PlayAnimation("Idle"); // Estado padrão
            return;
        }

        // Ordenar para encontrar a maior prioridade
        _activeStates.Sort((a, b) => b.Priority.CompareTo(a.Priority));
        string targetState = _activeStates[0].StateName;

        if (_currentPlayingState != targetState)
        {
            PlayAnimation(targetState);
        }
    }

    // Controle do Animator do Unity
    private void PlayAnimation(string stateName)
    {
        _currentPlayingState = stateName;
        // CrossFade para uma transição suave de 0.1s
        _animator.CrossFade(stateName, 0.1f); 
    }
}

Evolução da arquitetura: Minhas ideias para sistemas complexos

Este sistema de Pool ativo + Prioridade oferece uma ótima escalabilidade. Considere algumas extensões interessantes para o futuro:

  1. Gestão de prioridades idênticas: Se houver dois pedidos com prioridade 50, pode-se ampliar a lógica para que prevaleça o último a chegar ou o que tiver mais tempo restante.
  2. Máscaras de avatar (Avatar Mask): Em jogos 3D, às vezes você precisa que o tronco superior atire enquanto o inferior corre ou recebe um golpe. É possível criar dois managers independentes (UpperBodyPool e LowerBodyPool) e passar diferentes índices de camada para o CrossFade.
  3. Uso da Playable API: Para habilidades com tempos muito rígidos, o Animator pode ter latência. Nesses casos, este manager pode ser integrado ao Playable Graph do Unity para misturar AnimationClips diretamente via código.

Estas são minhas reflexões e conclusões sobre o gerenciamento de estados de ação baseadas na minha experiência recente. Esta solução resolveu os problemas da nossa equipe, mas não é necessariamente uma "bala de prata"; deve ser ajustada conforme o tipo de jogo. Minha visão é limitada, então se vocês tiverem soluções melhores ou detectarem falhas na minha lógica, ficarei feliz em debater. Por favor, corrijam-me se eu estiver errado.


Artigo original da equipe iknowabit. Suporte técnico: Exploração de padrões de arquitetura avançada em C# e motor Unity.