Implémentation du modèle de statut dans Unity

image

Dans le processus de programmation des entités dans le jeu, des situations surviennent lorsqu'elles doivent agir dans différentes conditions de différentes manières, ce qui suggère l'utilisation d' états .

Mais si vous décidez d'utiliser la force brute, le code se transformera rapidement en chaos emmêlé avec de nombreuses instructions imbriquées if-else.

Pour une solution élégante à ce problème, vous pouvez utiliser le modèle de conception État. Nous lui dédierons ce tutoriel!

À partir du didacticiel, vous:

  • Apprenez les bases du modèle State dans Unity.
  • Vous apprendrez ce qu'est une machine d'état et quand l'utiliser.
  • Apprenez à utiliser ces concepts pour contrôler les mouvements de votre personnage.

Remarque : ce tutoriel est destiné aux utilisateurs avancés; il est supposé que vous savez déjà comment travailler dans Unity et que vous avez un niveau moyen de connaissance de C #. De plus, ce didacticiel utilise Unity 2019.2 et C # 7.

Se rendre au travail


Téléchargez le matériel du projet . Décompressez le fichier zip et ouvrez le projet de démarrage dans Unity.

Il existe plusieurs dossiers dans le projet qui vous aideront à démarrer. Le dossier Assets / RW contient les dossiers Animations , Matériaux , Modèles , Préfabriques , Ressources , Scènes , Scripts et Sons , nommés en fonction des ressources qu'ils contiennent.

Pour terminer le didacticiel, nous ne travaillerons qu'avec des scènes et des scripts .

Allez dans RW / Scenes et ouvrez Main . En mode Jeu, vous verrez un personnage dans une cagoule à l'intérieur d'un château médiéval.


Cliquez sur Play et notez comment la caméra se déplace pour s'adapter au cadre du personnage . Pour le moment, dans notre petit jeu il n'y a pas d'interactions, nous allons y travailler dans le tutoriel.


Explorez le personnage


Dans la hiérarchie, sélectionnez Caractère . Découvrez l' inspecteur . Vous verrez un composant du même nom contenant la logique de contrôle des caractères .


Ouvrez Character.cs situé dans RW / Scripts .

Le script effectue de nombreuses actions, mais la plupart d'entre elles ne sont pas importantes pour nous. Pour l'instant, prêtons attention aux méthodes suivantes.

  • Move : il déplace le personnage, recevant des valeurs de type speed flottante comme vitesse de déplacement et vitesse de rotationSpeed comme vitesse angulaire.
  • ResetMoveParams : cette méthode réinitialise les paramètres utilisés pour animer le mouvement et la vitesse angulaire du personnage. Il est utilisé uniquement pour le nettoyage.
  • SetAnimationBool : il définit le paramètre d'animation param de type Bool sur valeur.
  • CheckCollisionOverlap : il reçoit un type Vector3 et renvoie un bool qui détermine s'il y a des collisionneurs dans le rayon spécifié à partir du .
  • TriggerAnimation : TriggerAnimation le paramètre d'animation de param entrée.
  • ApplyImpulse : ApplyImpulse impulsion au caractère égale à la force paramètre d'entrée force type Vector3 .

Ci-dessous, vous verrez ces méthodes. Dans notre tutoriel, leur contenu et leur travail interne ne sont pas importants.

Que sont les machines à états


Une machine d'état est un concept dans lequel un conteneur stocke l'état de quelque chose à un moment donné. Sur la base des données d'entrée, il peut fournir une conclusion en fonction de l'état actuel, en passant dans ce processus à un nouvel état. Les machines à états peuvent être représentées sous forme de diagramme d'état . La préparation d'un diagramme d'état vous permet de réfléchir à tous les états possibles du système et aux transitions entre eux.

Machines d'état


Les machines à états finis ou FSM (machines à états finis) sont l'une des quatre principales familles de machines . Les automates sont des modèles abstraits de machines simples. Ils sont étudiés dans le cadre de la théorie des automates - la branche théorique de l'informatique.

En bref:

  • FSM se compose d'une quantité limitée de condition . À un moment donné , un seul de ces états est actif .
  • Chaque état détermine dans quel état il entrera en sortie sur la base de la séquence reçue d' informations entrantes .
  • L'état de sortie devient le nouvel état actif. En d'autres termes, il y a une transition entre les États .


Pour mieux comprendre cela, considérez le caractère d'un jeu de plateforme qui est sur le terrain. Le personnage est à l'état permanent . Ce sera son état actif jusqu'à ce que le joueur appuie sur le bouton pour que le personnage saute.

L'état permanent identifie une pression sur un bouton comme une entrée importante et, en tant que sortie , passe à l'état de saut .

Supposons qu'il existe un certain nombre de tels états de mouvement et qu'un personnage ne puisse être que dans l'un des états à la fois. Ceci est un exemple de FSM.

Machines à états hiérarchiques


Considérons un jeu de plateforme utilisant FSM, dans lequel plusieurs états partagent une logique physique commune. Par exemple, vous pouvez vous déplacer et sauter dans les états accroupi et debout . Dans ce cas, plusieurs variables entrantes conduisent au même comportement et à la même sortie d'informations pour deux états différents.

Dans une telle situation, il serait logique de déléguer le comportement général à un autre état. Heureusement, cela peut être réalisé en utilisant des machines à états hiérarchiques .

Dans un FSM hiérarchique, certains sous - états délèguent des informations entrantes brutes à leurs sous-états . Cela vous permet à son tour de réduire gracieusement la taille et la complexité du FSM, tout en conservant sa logique.

Modèle de statut


Dans leur livre Design Patterns: Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissidis ( The Gang of Four ) ont défini la tâche du modèle State comme suit:

«Il doit permettre à l'objet de changer de comportement lorsque son état interne change. Dans ce cas, il semblerait que l'objet ait changé de classe. »

Pour mieux comprendre cela, considérons l'exemple suivant:

  • Un script qui reçoit des informations entrantes pour la logique de mouvement est attaché à une entité en jeu.
  • Cette classe stocke une variable d' état actuelle qui fait simplement référence à une instance de la classe d' état .
  • Les informations entrantes sont déléguées à cet état actuel, qui les traite et crée un comportement défini en lui-même. Il gère également les transitions d'état requises.

Par conséquent, étant donné qu'à différents moments la variable d' état actuelle fait référence à différents états, il semblera que la même classe de script se comporte différemment. C'est l'essence même du modèle «Statut».

Dans notre projet, la classe de caractères susmentionnée se comportera différemment selon les différents états. Mais nous avons besoin de lui pour se comporter!


Dans le cas général, il y a trois points clés pour chaque classe d'état qui permettent le comportement de l'état dans son ensemble:

  • Entrée : c'est le moment où une entité entre dans un état et effectue des actions qui ne doivent être effectuées qu'une seule fois lors de l'entrée dans l'état.
  • Sortie : similaire à l'entrée - toutes les opérations de réinitialisation sont effectuées ici, qui doivent être effectuées uniquement avant que l'état ne change.
  • Boucle de mise à jour : voici la logique de mise à jour de base qui s'exécute dans chaque trame. Il peut être divisé en plusieurs parties, par exemple, un cycle de mise à jour de la physique et un cycle de traitement des entrées des joueurs.


Définition d'un état et d'une machine à états


Accédez à RW / Scripts et ouvrez StateMachine.cs .

La State Machine , comme vous pouvez le deviner, fournit une abstraction pour la State Machine. Notez que CurrentState correctement situé à l'intérieur de cette classe. Il stockera un lien vers l'état actuel de la machine à états actifs.

Maintenant, pour définir le concept de l' état , allons dans RW / Scripts et ouvrons le script State.cs dans l'EDI.

State est une classe abstraite que nous utiliserons comme modèle à partir duquel toutes les classes d'états de projet sont dérivées. Une partie du code dans les documents du projet est déjà prête.

DisplayOnUI affiche uniquement le nom de l'état actuel dans l'interface utilisateur à l'écran. Vous n'avez pas besoin de connaître son périphérique interne, comprenez simplement qu'il reçoit un énumérateur du type UIManager.Alignment tant que paramètre d'entrée, qui peut être Left ou Right . L'affichage du nom de l'état dans la partie inférieure gauche ou droite de l'écran en dépend.

De plus, il existe deux variables protégées, character et stateMachine . La variable de character fait référence à une instance de la classe Character et stateMachine fait référence à une instance de la machine à états associée à l'état.

Lors de la création d'une instance d'état, le constructeur lie character et stateMachine .

Chacune des nombreuses instances de Character dans une scène peut avoir son propre ensemble d'états et de machines à états.

Ajoutez maintenant les méthodes suivantes à State.cs et enregistrez le fichier:

 public virtual void Enter() { DisplayOnUI(UIManager.Alignment.Left); } public virtual void HandleInput() { } public virtual void LogicUpdate() { } public virtual void PhysicsUpdate() { } public virtual void Exit() { } 

Ces méthodes virtuelles définissent les points d'état clés décrits ci-dessus. Lorsque la machine d'état effectue une transition entre les états, nous appelons Exit pour l'état précédent et Enter nouvel état actif .

HandleInput , LogicUpdate et PhysicsUpdate définissent ensemble une boucle de mise à jour . HandleInput gère l'entrée du lecteur. LogicUpdate traite la logique de base, tandis que PhyiscsUpdate traite les calculs de logique et de physique.

Maintenant, ouvrez à nouveau StateMachine.cs , ajoutez les méthodes suivantes et enregistrez le fichier:

 public void Initialize(State startingState) { CurrentState = startingState; startingState.Enter(); } public void ChangeState(State newState) { CurrentState.Exit(); CurrentState = newState; newState.Enter(); } 

Initialize configure la machine à états en définissant CurrentState sur startingState et en appelant Enter pour cela. Cela initialise la machine d'état, définissant pour la première fois l'état actif.

ChangeState gère les transitions d' état . Il appelle Exit pour l'ancien CurrentState avant de remplacer sa référence par newState . À la fin, il appelle Enter pour newState .

Ainsi, nous définissons l' état et la machine d'état .

Création d'états de mouvement


Jetez un œil au diagramme d'état suivant, qui montre les différents états de mouvement de l' essence du joueur dans le jeu. Dans cette section, nous implémentons le modèle «Status» pour le mouvement montré dans la figure FSM :


Faites attention aux états de mouvement, à savoir la position debout , le canardage et le saut , ainsi que la façon dont les données entrantes provoquent des transitions entre les états. Il s'agit d'un FSM hiérarchique dans lequel Grounded est un sous-état pour les sous-états Ducking et Standing .

Revenez à Unity et accédez à RW / Scripts / States . Vous y trouverez plusieurs fichiers C # dont les noms se terminent par State .

Chacun de ces fichiers définit une classe, dont chacune est héritée de State . Par conséquent, ces classes définissent les états que nous utiliserons dans le projet.

Ouvrez maintenant Character.cs à partir du dossier RW / Scripts .

Faites défiler au-dessus du fichier #region Variables et ajoutez le code suivant:

 public StateMachine movementSM; public StandingState standing; public DuckingState ducking; public JumpingState jumping; 

Ce movementSM fait référence à une machine à états qui traite la logique de mouvement pour l'instance de Character . Nous avons également ajouté des liens vers trois états que nous mettons en œuvre pour chaque type de mouvement.

Accédez à #region MonoBehaviour Callbacks dans le même fichier. Ajoutez les méthodes MonoBehaviour suivantes, puis enregistrez

 private void Start() { movementSM = new StateMachine(); standing = new StandingState(this, movementSM); ducking = new DuckingState(this, movementSM); jumping = new JumpingState(this, movementSM); movementSM.Initialize(standing); } private void Update() { movementSM.CurrentState.HandleInput(); movementSM.CurrentState.LogicUpdate(); } private void FixedUpdate() { movementSM.CurrentState.PhysicsUpdate(); } 

  • Dans Start code crée une instance de State Machine et l'affecte à movementSM , et instancie également divers états de mouvement. Lors de la création de chacun des états de mouvement, nous transmettons des références à l'instance de Character à l'aide du this , ainsi qu'à l'instance de movementSM . Au final, nous appelons Initialize pour movementSM et passons Standing comme état initial.
  • Dans la méthode Update , nous appelons HandleInput et LogicUpdate pour le CurrentState de la machine movementSM . De même, dans FixedUpdate nous appelons PhysicsUpdate pour le CurrentState de la machine movementSM . En substance, cela délègue les tâches à un état actif; c'est la signification du modèle «Statut».

Maintenant, nous devons définir le comportement à l'intérieur de chacun des états de mouvement. Préparez-vous, il y aura beaucoup de code!

Entreprise ferme


Revenez à RW / Scripts / States dans la fenêtre Project.

Ouvrez Grounded.cs et notez que cette classe a un constructeur qui correspond au constructeur State . C'est logique car cette classe en hérite. Vous verrez la même chose dans toutes les autres classes d' état .

Ajoutez le code suivant:

 public override void Enter() { base.Enter(); horizontalInput = verticalInput = 0.0f; } public override void Exit() { base.Exit(); character.ResetMoveParams(); } public override void HandleInput() { base.HandleInput(); verticalInput = Input.GetAxis("Vertical"); horizontalInput = Input.GetAxis("Horizontal"); } public override void PhysicsUpdate() { base.PhysicsUpdate(); character.Move(verticalInput * speed, horizontalInput * rotationSpeed); } 

Voici ce qui se passe ici:

  • Nous redéfinissons l'une des méthodes virtuelles définies dans la classe parente. Pour conserver toutes les fonctionnalités qui peuvent exister dans le parent, nous appelons la méthode de base avec le même nom de chaque méthode substituée. Il s'agit d'un modèle important que nous continuerons à utiliser.
  • La ligne suivante, Enter définit horizontalInput et verticalInput leurs valeurs par défaut.
  • Dans Exit nous appelons, comme mentionné ci-dessus, la méthode ResetMoveParams pour réinitialiser lors du passage à un autre état.
  • Dans la méthode HandleInput , les variables horizontalInput et verticalInput HandleInput valeurs des axes d'entrée horizontal et vertical. Grâce à cela, le joueur peut contrôler le personnage à l'aide des touches W , A , S et D.
  • À PhysicsUpdate nous effectuons un appel Move , en passant les variables horizontalInput et verticalInput multipliées par les vitesses correspondantes. Dans la speed variable speed la vitesse de déplacement est stockée, et dans rotationSpeed , la vitesse angulaire.

Maintenant, ouvrez Standing.cs et faites attention au fait qu'il hérite de Grounded . Cela s'est produit parce que, comme nous l'avons dit plus haut, Standing est un sous-état de Grounded . Il existe différentes façons de mettre en œuvre cette relation, mais dans ce didacticiel, nous utilisons l'héritage.

Ajoutez les méthodes de override suivantes et enregistrez le script:

 public override void Enter() { base.Enter(); speed = character.MovementSpeed; rotationSpeed = character.RotationSpeed; crouch = false; jump = false; } public override void HandleInput() { base.HandleInput(); crouch = Input.GetButtonDown("Fire3"); jump = Input.GetButtonDown("Jump"); } public override void LogicUpdate() { base.LogicUpdate(); if (crouch) { stateMachine.ChangeState(character.ducking); } else if (jump) { stateMachine.ChangeState(character.jumping); } } 

  • Dans Enter nous configurons les variables héritées de Grounded . Appliquez la speed rotationSpeed et la speed rotationSpeed RotationSpeed personnage à la speed et à la speed rotationSpeed . Ensuite, ils se rapportent, respectivement, à la vitesse normale de mouvement et à la vitesse angulaire destinée à l'essence du personnage.

    De plus, les variables de stockage des entrées crouch et crouch sont réinitialisées à faux.
  • À l'intérieur de HandleInput , les variables HandleInput et jump stockent les entrées du joueur pour les squats et les sauts. Si dans la scène principale le joueur appuie sur la touche Maj, le squat est réglé sur vrai. De même, un joueur peut utiliser la touche Espace pour jump .
  • Dans LogicUpdate nous vérifions les variables LogicUpdate et jump de type bool . Si crouch est vrai, alors movementSM.CurrentState transforme en character.ducking . Si jump est vrai, l'état change en character.jumping .

Enregistrez et assemblez le projet, puis cliquez sur Lecture . Vous pouvez vous déplacer dans la scène à l'aide des touches W , A , S et D. Si vous essayez d'appuyer sur Maj ou Espace , un comportement inattendu se produira, car les états correspondants ne sont pas encore implémentés.


Essayez de vous déplacer sous les objets de la table. Vous verrez qu'en raison de la hauteur du collisionneur du personnage, ce n'est pas possible. Pour que le personnage le fasse, vous devez ajouter un comportement accroupi.

On grimpe sous la table


Ouvrez le script Ducking.cs . Notez que Ducking hérite également de la classe Grounded pour les mêmes raisons que Standing . Ajoutez les méthodes de override suivantes et enregistrez le script:

 public override void Enter() { base.Enter(); character.SetAnimationBool(character.crouchParam, true); speed = character.CrouchSpeed; rotationSpeed = character.CrouchRotationSpeed; character.ColliderSize = character.CrouchColliderHeight; belowCeiling = false; } public override void Exit() { base.Exit(); character.SetAnimationBool(character.crouchParam, false); character.ColliderSize = character.NormalColliderHeight; } public override void HandleInput() { base.HandleInput(); crouchHeld = Input.GetButton("Fire3"); } public override void LogicUpdate() { base.LogicUpdate(); if (!(crouchHeld || belowCeiling)) { stateMachine.ChangeState(character.standing); } } public override void PhysicsUpdate() { base.PhysicsUpdate(); belowCeiling = character.CheckCollisionOverlap(character.transform.position + Vector3.up * character.NormalColliderHeight); } 

  • Dans Enter paramètre qui provoque le basculement de l'animation de squat est réglé sur accroupi, ce qui active l'animation de squat. Les propriétés character.CrouchSpeed et character.CrouchRotationSpeed les valeurs de speed et de rotation , qui renvoient le mouvement et la vitesse angulaire du personnage lorsqu'il se déplace dans un squat .

    character.CrouchColliderHeight suivant.CrouchColliderHeight définit la taille du collisionneur du personnage, qui renvoie la hauteur de collision souhaitée lors de l'accroupissement. À la fin, belowCeiling réinitialisé sur false.
  • Dans Exit le paramètre d'animation squat est défini sur false. Cela désactive l'animation de squat. Ensuite, la hauteur normale du collisionneur est définie, renvoyée par character.NormalColliderHeight .
  • À l'intérieur de HandleInput variable crouchHeld définit la valeur d'entrée du lecteur. Dans la scène principale , maintenir Shift définit crouchHeld sur true.
  • Dans PhysicsUpdate variable belowCeiling attribuer une valeur en passant un point au format Vector3 avec la tête de l'objet de jeu du personnage à la méthode CheckCollisionOverlap . S'il y a une collision près de ce point, cela signifie que le personnage est sous une sorte de plafond.
  • En interne, LogicUpdate vérifie si crouchHeld ou belowCeiling est vrai. Si aucune d'entre elles n'est vraie, alors movementSM.CurrentState devient character.standing .

Générez le projet et cliquez sur Play . Vous pouvez maintenant vous déplacer dans la scène. Si vous appuyez sur Shift , le personnage s'assoit et vous pouvez vous déplacer dans le squat.

Vous pouvez également monter sous la plate-forme. Si vous relâchez Shift sous les plates-formes, le personnage sera toujours dans un squat jusqu'à ce qu'il quitte son abri.


Envolez-vous!


Ouvrez Jumping.cs . Vous verrez une méthode appelée Jump . Ne vous inquiétez pas de son fonctionnement; il suffit de comprendre qu'il est utilisé pour que le personnage puisse sauter en tenant compte de la physique et de l'animation.

Ajoutez maintenant les méthodes de override habituelles et enregistrez le script

 public override void Enter() { base.Enter(); SoundManager.Instance.PlaySound(SoundManager.Instance.jumpSounds); grounded = false; Jump(); } public override void LogicUpdate() { base.LogicUpdate(); if (grounded) { character.TriggerAnimation(landParam); SoundManager.Instance.PlaySound(SoundManager.Instance.landing); stateMachine.ChangeState(character.standing); } } public override void PhysicsUpdate() { base.PhysicsUpdate(); grounded = character.CheckCollisionOverlap(character.transform.position); } 

  • À l'intérieur d' Enter singleton SoundManager joue le son du saut. La grounded réinitialisée à sa valeur par défaut. À la fin, Jump est appelé.
  • Dans PhysicsUpdate point PhysicsUpdate côté des jambes du personnage est envoyé à CheckCollisionOverlap , ce qui signifie que lorsque le personnage est au sol, la grounded sera définie sur true.
  • Dans LogicUpdate , si la grounded est vraie, nous appelons TriggerAnimation pour activer l'animation d'atterrissage, un son d'atterrissage est joué et le movementSM.CurrentState change en character.standing .

Ainsi, sur ce point, nous avons achevé la mise en œuvre complète du déplacement des FSM en utilisant le modèle «État» . Générez le projet et exécutez-le. Appuyez sur Espace pour faire sauter le personnage.


Où aller ensuite?


Les matériaux du projet ont un projet de projet et un projet fini.

Malgré son utilité, les machines à états ont des limites. Les machines à états simultanés et les automates à refoulement peuvent gérer certaines de ces limitations. Vous pouvez les lire dans le livre de Robert Nystrom Game Programming Patterns .

De plus, le sujet peut être approfondi en examinant les arbres de comportement utilisés pour créer des entités plus complexes dans le jeu.

Source: https://habr.com/ru/post/fr484176/


All Articles