Mon point de vue sur la gestion des états d'action dans le développement de jeux
20 mars 2026
Cet article traite des bugs courants où les états d'action se chevauchent et échouent à se rétablir. En me basant sur mon expérience au sein d'une équipe qui débute, je partage comment construire une machine d'états d'action à haute extensibilité et faible couplage dans Unity en utilisant un Pool d'États Actifs et une évaluation dynamique des priorités.
Catégories:Tecnologia、Unity、Développement Web
Le point critique : Le bug de chevauchement d'états que j'ai rencontré
J'ai récemment rejoint une équipe qui vient de se lancer. En les aidant à structurer leur système de combat et leur logique de présentation, je suis tombé sur un bug très classique et agaçant.
Voici le scénario : Le personnage reçoit un Debuff "Gel" qui dure 5 secondes ; à ce moment-là, l'action passe à [Gel] (le personnage est figé sur place et tremble). À la 2e seconde, le personnage reçoit un coup lourd d'un ennemi avec un effet d'étourdissement (stun), déclenchant une action de [Stun] pendant 2 secondes (le personnage se tient la tête, étourdi).
Le vrai problème survient après la fin de l'étourdissement : Lorsque les 2 secondes de l'action Stun sont terminées, logiquement, le personnage devrait encore avoir 1 seconde de Debuff "Gel". Pourtant, au lieu de revenir à l'action [Gel], le personnage retombe directement dans l'état [Idle] (repos) par défaut. Le joueur voit alors son personnage respirer normalement en position de repos, alors que le joystick ne permet aucun mouvement (car la logique de base est toujours gelée). Cela crée une déconnexion flagrante entre le visuel et la logique sous-jacente.
Après avoir examiné le code, j'ai trouvé la raison très simple : le système précédent utilisé par l'équipe était basé sur des déclencheurs (triggers). Lorsqu'un Stun était déclenché, le système forçait l'Animator à jouer le Stun ; à la fin du Stun, il forçait brutalement l'Animator à revenir à l'état par défaut sans tenir compte des autres effets logiques en cours.
La prévalence du problème et les limites des solutions traditionnelles
Personnellement, je pense que ce problème est extrêmement courant dans le développement de jeux. Tant que votre jeu possède des systèmes de Buffs complexes ou des effets de compétences qui se superposent, vous serez confronté à ce problème de chevauchement et de récupération des états d'action.
Beaucoup d'équipes, au début, pour obtenir des résultats rapides, ont tendance à adopter deux approches qui présentent, selon moi, de gros risques :
Risque A : Différents systèmes contrôlant directement les variables de l'Animator
Chaque système (Buffs, compétences, réactions aux coups) appelle directement animator.SetBool("isFrozen", true). Quand le système devient complexe, vous ne savez plus quel morceau de code a remis le Bool à false et quand, ce qui sème le chaos dans la machine d'états et rend le débogage extrêmement difficile.
Risque B : Abus du réseau de transitions Unity Mecanim
La fenêtre de l'Animator d'Unity est très intuitive quand il y a peu de transitions. Mais si vous liez la logique métier à ces transitions — en réglant des tas de conditions comme Speed > 0, isFrozen == true, isStunned == false — l'Animator finit par ressembler à une "toile d'araignée". Ajouter un seul nouvel état nécessite de connecter d'innombrables nouvelles lignes, et le coût de maintenance augmente de manière exponentielle.
Ma solution : Pool d'états actifs et évaluation dynamique des priorités
Pour résoudre les problèmes d'états qui se chevauchent, de récupération échouée et de couplage trop élevé, j'ai proposé une idée à l'équipe : un mécanisme de Pool d'États Actifs (Active State Pool) et d'Évaluation Dynamique des Priorités.
L'idée centrale est la suivante : La demande d'état et l'exécution de l'état doivent être totalement séparées.
La couche métier (système de Buff ou de compétence) est uniquement responsable d'Enregistrer et de Désenregistrer l'état d'action qu'elle souhaite. Ensuite, un AnimationStateManager centralisé décide, à chaque frame, quelle instruction de mouvement doit réellement être envoyée à l'Animator.
Flux du mécanisme central :
Loading diagram...
Dans cette architecture, lorsque le "Stun" avec une priorité de 100 se termine et est désenregistré, le gestionnaire réévalue le pool et trouve que la priorité la plus élevée restante est "Gel" avec une priorité de 50. Il ordonne alors automatiquement à l'Animator de revenir à l'action [Gel]. Le bug précédent est ainsi résolu naturellement.
Mise en œuvre dans Unity : Garder l'Animator épuré
Une fois cette architecture appliquée, je pense que la façon d'utiliser l'Animator dans Unity doit aussi évoluer. Nous pouvons abandonner les transitions complexes et les conditions Trigger/Bool, et laisser l'Animator devenir un simple lecteur de mouvements.
Dans l'Animator Controller, il vous suffit de glisser tous les States nécessaires sans avoir besoin de les relier (ou reliez-les tous depuis AnyState sans condition). Au niveau du code, utilisez Animator.CrossFade() ou Animator.Play() pour piloter directement les transitions d'état.
Exemple de pseudo-code du gestionnaire central :
// 1. Définir la structure de données de la demande d'état
public class AnimStateRequest
{
public string StateName; // Nom de l'état dans l'Animator
public int Priority; // Niveau de priorité
public object Source; // Source (pour validation lors du retrait, ex: instance de Buff)
}
// 2. Gestionnaire Unifié
public class AnimationStateManager : MonoBehaviour
{
private Animator _animator;
private List<AnimStateRequest> _activeStates = new List<AnimStateRequest>();
private string _currentPlayingState;
// Enregistrer un état
public void AddStateRequest(string stateName, int priority, object source)
{
_activeStates.Add(new AnimStateRequest { StateName = stateName, Priority = priority, Source = source });
EvaluateHighestPriorityState();
}
// Désenregistrer un état
public void RemoveStateRequest(object source)
{
_activeStates.RemoveAll(req => req.Source == source);
EvaluateHighestPriorityState();
}
// Évaluation dynamique de l'état de priorité maximale
private void EvaluateHighestPriorityState()
{
if (_activeStates.Count == 0)
{
PlayAnimation("Idle"); // État par défaut
return;
}
// Trier pour trouver la priorité la plus élevée
_activeStates.Sort((a, b) => b.Priority.CompareTo(a.Priority));
string targetState = _activeStates[0].StateName;
if (_currentPlayingState != targetState)
{
PlayAnimation(targetState);
}
}
// Piloter l'Animator Unity
private void PlayAnimation(string stateName)
{
_currentPlayingState = stateName;
// CrossFade pour une transition fluide, 0.1f est le temps de transition
_animator.CrossFade(stateName, 0.1f);
}
}
Extension de l'architecture : Mes idées pour des systèmes plus complexes
Cette architecture de Pool Actif et d'Évaluation de Priorité offre une excellente extensibilité. Pour la suite du développement, j'ai également envisagé quelques extensions :
- Gestion des conflits de priorité identique : Si deux demandes ont la même priorité (ex: 50), on peut étendre la logique pour donner la priorité à la plus récente, ou à celle ayant la durée restante la plus longue.
- Gestion indépendante de l'Avatar Mask : Dans les jeux 3D, il arrive souvent que le haut du corps doive tirer alors que le bas du corps subit un impact ou court. On peut créer deux gestionnaires indépendants (ex: UpperBodyPool et LowerBodyPool) et envoyer des index de Layer différents via
CrossFadepour un découplage parfait. - Adopter l'API Playable : Pour des mouvements avec un timing très strict, l'utilisation directe de l'Animator peut poser des problèmes de latence. Ici, ce gestionnaire d'états peut être couplé au Playable Graph d'Unity pour mixer les AnimationClips directement en C#, offrant ainsi un contrôle total.
Voilà mes réflexions sur la gestion des états d'action dans le cadre d'un projet réel. Objectivement, cette solution a réglé les points critiques de notre équipe, mais ce n'est pas forcément une solution miracle. Mon point de vue est forcément limité, donc si vous avez de meilleures approches ou si vous voyez des oublis dans ma logique, n'hésitez pas à en discuter. Merci de me corriger si je fais fausse route.
Cet article a été originalement créé par l'équipe iknowabit. Support technique : Exploration basée sur le moteur Unity et les modèles d'architecture avancés en C#.