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.