Développer une base de code modulaire et flexible avec le modèle de programmation d'état
En implémentant des modèles de conception de programmation de jeux courants dans votre projet Unity, vous pouvez créer et maintenir efficacement une base de code propre, organisée et lisible. Les modèles de conception réduisent non seulement le refactoring et le temps passé aux tests, mais ils accélèrent également les processus d'intégration et de développement, contribuant ainsi à une base solide pour la croissance de votre jeu, de votre équipe de développement et de votre entreprise.
Considérez les modèles de conception non pas comme des solutions finies que vous pouvez copier et coller dans votre code, mais comme des outils supplémentaires qui peuvent vous aider à créer des applications plus volumineuses et évolutives lorsqu'elles sont utilisées correctement.
Cette page examine le modèle State et comment il peut faciliter la gestion de votre base de code.
Le contenu ici est basé sur le livre électronique gratuit, Améliorez votre code avec des modèles de programmation de jeux, qui explique les modèles de conception bien connus et partage des exemples pratiques pour les utiliser dans votre projet Unity.
D'autres articles de la série de modèles de conception de programmation de jeux Unity sont disponibles sur le hub des meilleures pratiques Unity ou cliquez sur les liens suivants :
Imaginez construire un personnage jouable. A un moment donné, le personnage peut se trouver au sol. Déplacez le contrôleur et il semble courir ou marcher. Appuyez sur le bouton de saut et le personnage saute dans les airs. Quelques images plus tard, il atterrit et reprend sa position debout et inactive.
L'interactivité des jeux informatiques nécessite le suivi et la gestion de nombreux systèmes qui changent au moment de l'exécution. Si vous dessinez un diagramme qui représente les différents états de votre personnage, vous pourriez obtenir quelque chose comme l'image ci-dessus :
Il ressemble à un organigramme, avec quelques différences :
- Il se compose d'un certain nombre d'états (ralenti/debout, marche, course, saut, etc.), et un seul état actuel est actif à un moment donné.
- Chaque état peut déclencher une transition vers un autre état en fonction des conditions au moment de l'exécution.
- Lorsqu'une transition se produit, l'état de sortie devient le nouvel état actif.
Ce diagramme illustre ce qu'on appelle une machine à états finis (FSM). Dans le développement de jeux, un cas d'utilisation typique d'un FSM consiste à suivre l'état interne d'un accessoire ou d'un « acteur du jeu » comme le personnage jouable. Il existe de nombreux cas d'utilisation d'un FSM dans le développement de jeux, et si vous avez une certaine expérience dans le développement d'un projet dans Unity, vous avez probablement déjà utilisé un FSM dans le contexte des machines à états d'animation dans Unity.
Un FSM est défini par une liste de ses états. Il possède un état initial avec des conditions pour chaque transition. Un FSM peut se trouver exactement dans un état parmi un nombre fini d’états à un moment donné, avec la possibilité de passer d’un état à un autre en réponse à des entrées externes entraînant une transition.
Le modèle de conception State, quant à lui, définit une interface qui représente un état et une classe qui implémente cette interface pour chaque état. Le contexte, ou la classe, qui doit modifier son comportement en fonction de l'état contient une référence à l'objet d'état actuel. Lorsque l'état interne du contexte change, il met simplement à jour la référence à l'objet d'état pour pointer vers un objet différent, ce qui modifie ensuite le comportement du contexte.
Le modèle State est similaire au FSM dans le sens où il permet également la gestion de différents états et la transition entre eux. Cependant, un FSM est généralement implémenté à l'aide d'une instruction switch, tandis que le modèle de conception State définit une interface qui représente un état et une classe qui implémente cette interface pour chaque état.
Le modèle d'état est largement utilisé dans le développement de jeux et peut constituer un moyen efficace de gérer les différents états d'un jeu, tels qu'un menu principal, un état de jeu et un état de fin de partie.
Voyons le modèle d'état en action avec l'exemple de la section suivante.
Un projet de démonstration est disponible sur Github qui fournit l' exemple de code dans cette section.
Une manière simplifiée de décrire un FSM de base dans le code pourrait ressembler à l'exemple ci-dessous qui utilise une énumération et une instruction switch .
Tout d’abord, vous définissez une énumération PlayerControllerState composée de trois états : Ralenti, marchez et sautez.
Ensuite, switch est utilisé comme instruction conditionnelle dans la boucle Update pour tester dans quel état vous vous trouvez actuellement. En fonction de l'état, vous pouvez appeler les fonctions appropriées pour exécuter le comportement spécifique qui s'applique.
Cela peut fonctionner, mais le script PlayerController peut rapidement devenir compliqué, d'autant plus que vous devez formuler les conditions de transition entre les états. L'utilisation d'une instruction switch pour gérer l'état d'un jeu avec un seul script n'est pas considérée comme la meilleure pratique car elle peut conduire à un code complexe et difficile à maintenir. L'instruction switch peut devenir volumineuse et difficile à comprendre à mesure que le nombre d'états et de transitions augmente.
De plus, cela rend plus difficile l’ajout de nouveaux états ou transitions car des modifications doivent être apportées à l’instruction switch . Le modèle State, quant à lui, permet une conception plus modulaire et extensible, facilitant l'ajout de nouveaux états ou transitions.
Réimplémentons le modèle d'état pour réorganiser la logique de PlayerController. Cet exemple de code est également disponible dans le projet de démonstration hébergé sur Github.
Selon le Gang of Fouroriginal, le modèle de conception de l'État résout deux problèmes :
- Un objet doit changer de comportement lorsque son état interne change.
- Le comportement spécifique à l'état est défini indépendamment. L'ajout de nouveaux états n'a pas d'impact sur le comportement des états existants.
Dans l’exemple de code précédent, la classe UnrefactoredPlayerController peut suivre les changements d’état, mais elle ne répond pas au deuxième problème. Vous souhaitez minimiser l’impact sur les états existants lorsque vous en ajoutez de nouveaux. Au lieu de cela, vous pouvez encapsuler un état en tant qu’objet.
Imaginez structurer chacun des états de votre exemple comme le diagramme ci-dessus. Ici, vous entrez dans l'état approprié et bouclez chaque image jusqu'à ce qu'une condition provoque la sortie du flux de contrôle. En d’autres termes, vous encapsulez l’état spécifique avec une entrée, une mise à jour et une sortie.
Pour implémenter le modèle ci-dessus, créez une interface appelée IState. Chaque état concret de votre jeu implémentera alors l'interface en suivant cette convention :
- Une entrée: Cette logique s’exécute lors de la première entrée dans l’état.
- Mise à jour: Cette logique exécute chaque image (parfois appelée Execute ou Tick). Vous pouvez segmenter davantage la méthode Update comme le fait MonoBehaviour, en utilisant un FixedUpdate pour la physique, un LateUpdate, etc. Toute fonctionnalité de la mise à jour exécute chaque image jusqu'à ce qu'une condition soit détectée qui déclenche un changement d'état.
- Une sortie : Le code ici s'exécute avant de quitter l'état et de passer à un nouvel état.
Vous devrez créer une classe pour chaque état qui implémente IState. Dans l'exemple de projet, une classe distincte a été configurée pour WalkState, IdleStateet JumpState.
Une autre classe, StateMachine.cs, gérera ensuite la façon dont le flux de contrôle entre et sort des états. Avec les trois exemples d’états, la machine à états pourrait ressembler à l’exemple de code ci-dessous.
Pour suivre le modèle, la machine à états fait référence à un objet public pour chaque état sous sa gestion (dans ce cas, walkState, jumpStateet ralentiState). Étant donné que la machine à états n'hérite pas de MonoBehaviour, utilisez un constructeur pour configurer chaque instance.
Vous pouvez transmettre tous les paramètres nécessaires au constructeur. Dans l’exemple de projet, un PlayerController est référencé dans chaque état. Vous l'utilisez ensuite pour mettre à jour chaque état par image (voir l'exemple IdleState ci-dessous).
Notez les points suivants concernant le concept de machine à états :
- L'attribut Serialisable vous permet d'afficher le StateMachine.cs (et ses champs publics) dans l'Inspecteur. Un autre MonoBehaviour (par exemple, un PlayerController ou un EnemyController) peut alors utiliser la machine à états comme champ.
- La propriété CurrentState est en lecture seule. Le StateMachine.cs lui-même ne définit pas explicitement ce champ. Un objet externe tel que PlayerController peut ensuite appeler la méthode Initialize pour définir l'état par défaut.
- Chaque objet d'état détermine ses propres conditions d'appel de la méthode TransitionTo pour modifier l'état actuellement actif. Vous pouvez transmettre toutes les dépendances nécessaires (y compris la machine à états elle-même) à chaque état lors de la configuration de l'instance StateMachine.
Dans l'exemple de projet, le PlayerController inclut déjà une référence au StateMachine, vous ne transmettez donc qu'un seul paramètre de joueur .
Chaque objet d'état gérera sa propre logique interne et vous pourrez créer autant d'états que nécessaire pour décrire votre GameObject ou votre composant. Chacun a sa propre classe qui implémente IState. Conformément aux principes SOLID , l'ajout d'états supplémentaires a un impact minimal sur les états créés précédemment.
Voici un exemple de IdleState.
Semblable au script StateMachine.cs , le constructeur est utilisé pour transmettre l'objet PlayerController. Ce lecteur contient une référence à la State Machine et à tout ce qui est nécessaire à la logique de mise à jour. IdleState surveille la vitesse ou l'état de saut du contrôleur de personnage, puis appelle la méthode TransitionTo de la machine à états de manière appropriée.
Consultez également l’exemple de projet pour l’implémentation de WalkState et JumpState . Plutôt que d’avoir une grande classe qui change de comportement, chaque état possède sa propre logique de mise à jour, leur permettant de fonctionner indépendamment les uns des autres.
Le modèle d'état peut vous aider à respecter les principes SOLID lors de la configuration de la logique interne d'un objet. Chaque État est relativement petit et ne suit que les conditions de transition vers un autre État. Conformément au principe ouvert-fermé, vous pouvez ajouter plus d'états sans affecter ceux existants et éviter les commutateurs fastidieux ou les instructions if dans un seul script monolithique.
Vous pouvez également étendre ses fonctionnalités pour communiquer les changements d'état aux objets extérieurs. Vous souhaiterez peut-être ajouter des événements (voir le modèle d'observateur). Le fait d'avoir un événement lors de l'entrée ou de la sortie d'un état peut avertir les auditeurs concernés et leur faire répondre au moment de l'exécution.
D’un autre côté, si vous n’avez que quelques états à suivre, la structure supplémentaire peut s’avérer excessive. Ce modèle n’a de sens que si vous vous attendez à ce que vos États atteignent une certaine complexité. Comme pour tout autre modèle de conception, vous devrez évaluer les avantages et les inconvénients en fonction des besoins de votre jeu particulier.
Ressources plus avancées pour la programmation dans Unity
Le livre électronique Améliorez votre code avec des modèles de programmation de jeuxfournit plus d'exemples sur la façon d'utiliser les modèles de conception dans Unity.
Tous les livres électroniques et articles techniques avancés sur Unity sont disponibles sur le hub des meilleures pratiques . Les livres électroniques sont également disponibles sur la page des meilleures pratiques avancées de la documentation.