Implémentation du modèle de conception de commande dans Unity

image

Vous êtes-vous déjà demandé comment dans des jeux comme Super Meat Boy la fonction de relecture est implémentée? L'une des façons de le mettre en œuvre consiste à effectuer l'entrée de la même manière que le joueur, ce qui, à son tour, signifie que l'entrée doit être stockée d'une manière ou d'une autre. Vous pouvez utiliser le modèle de commande pour cela et bien plus encore.

Le modèle de commande est également utile pour créer des fonctions d'annulation et de rétablissement dans un jeu de stratégie.

Dans ce didacticiel, nous implémentons le modèle de commande en C # et l'utilisons pour guider le personnage du bot à travers un labyrinthe en trois dimensions. À partir du didacticiel, vous apprendrez:

  • Les bases du modèle de commande.
  • Comment implĂ©menter le modèle de commande
  • Comment crĂ©er une file d'attente de commandes d'entrĂ©e et retarder leur exĂ©cution.

Remarque : il est supposé que vous connaissez déjà Unity et que vous avez une connaissance moyenne de C #. Dans ce didacticiel, nous travaillerons avec Unity 2019.1 et C # 7 .

Se rendre au travail


Pour commencer, téléchargez le matériel du projet . Décompressez le fichier et ouvrez le projet Starter dans Unity.

Allez dans RW / Scenes et ouvrez la scène principale . La scène se compose d'un bot et d'un labyrinthe, ainsi que d'une interface utilisateur terminale qui affiche des instructions. Le level design est réalisé sous la forme d'une grille, ce qui est utile lorsque l'on déplace visuellement le bot à travers le labyrinthe.


Si vous cliquez sur Play , nous verrons que les instructions ne fonctionnent pas. Ceci est normal car nous ajouterons cette fonctionnalité au didacticiel.

La partie la plus intéressante de la scène est le Bot GameObject. Sélectionnez-le dans la fenêtre Hiérarchie en cliquant dessus.


Dans l'inspecteur, vous pouvez voir qu'il a un composant Bot . Nous utiliserons ce composant en émettant des commandes d'entrée.


Nous comprenons la logique du bot


Accédez à RW / Scripts et ouvrez le script Bot dans l'éditeur de code. Vous n'avez pas besoin de savoir ce qui se passe dans le script Bot . Mais jetez un œil à deux méthodes: Move et Shoot . Encore une fois, vous n'avez pas à comprendre ce qui se passe à l'intérieur de ces méthodes, mais vous devez comprendre comment les utiliser.

Notez que la méthode Move reçoit un paramètre d'entrée CardinalDirection . CardinalDirection est une énumération. Un élément d'énumération de type CardinalDirection peut être Up , Down , Right ou Left . Selon la CardinalDirection sélectionnée CardinalDirection bot se déplace exactement d'un carré le long de la grille dans la direction correspondante.


La méthode Shoot oblige le bot à tirer des obus qui détruisent les murs jaunes , mais sont inutiles contre les autres murs.


Enfin, jetez un œil à la méthode ResetToLastCheckpoint ; pour comprendre ce qu'il fait, regardez le labyrinthe. Il y a des points dans le labyrinthe appelés checkpoint . Pour passer le labyrinthe, le bot doit se rendre au point de contrôle vert .


Lorsqu'un bot marche sur un nouveau point de contrôle, il devient le dernier pour lui. ResetToLastCheckpoint réinitialise la position du bot, le déplaçant vers le dernier point de contrôle.


Bien que nous ne puissions pas utiliser ces méthodes, nous le corrigerons bientôt. Pour commencer, vous devez vous renseigner sur le modèle de conception de commande .

Qu'est-ce que le modèle de conception de commande?


Le modèle de commande est l'un des 23 modèles de conception décrits dans le livre Design Patterns: Elements of Reusable Object-Oriented Software écrit par le Gang of Four par Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides ( GoF , Gang of Four).

Les auteurs rapportent que «le modèle de commande encapsule la demande en tant qu'objet, nous permettant ainsi de paramétrer d'autres objets avec des demandes différentes, des demandes de file d'attente ou de journal et de prendre en charge des opérations réversibles».

Ouah! Comment est-ce?

Je comprends que cette définition n'est pas très simple, alors analysons-la.

L'encapsulation signifie qu'un appel de méthode peut être encapsulé en tant qu'objet.


La méthode encapsulée peut affecter de nombreux objets en fonction du paramètre d'entrée. C'est ce qu'on appelle le paramétrage d' autres objets.

La «commande» résultante peut être enregistrée avec d'autres équipes jusqu'à ce qu'elle soit exécutée. Il s'agit de la file d'attente des demandes.


File d'attente d'équipe

Enfin, la réversibilité signifie que les opérations peuvent être annulées à l'aide de la fonction Annuler.

OK, mais comment cela se reflète-t-il dans le code?

La classe Command aura une méthode Execute , qui reçoit en paramètre d'entrée l'objet (par lequel la commande est exécutée) appelé Receiver . En fait, la méthode Execute est encapsulée par la classe Command.

De nombreuses instances de la classe Command peuvent être passées en tant qu'objets ordinaires, c'est-à-dire qu'elles peuvent être stockées dans des structures de données telles qu'une file d'attente, une pile, etc.

Pour exécuter une commande, vous devez appeler sa méthode Execute. La classe qui démarre l'exécution est appelée Invoker .

Le projet contient actuellement une classe vide appelée BotCommand . Dans la section suivante, nous allons implémenter l'implémentation de ce qui précède pour permettre au bot d'effectuer des actions à l'aide du modèle de commande.

Déplacez le bot


Implémentation du modèle de commande


Dans cette section, nous implémentons le modèle de commande. Il existe de nombreuses façons de le mettre en œuvre. Dans ce tutoriel, nous allons couvrir l'un d'entre eux.

Pour commencer, allez dans RW / Scripts et ouvrez le script BotCommand dans l'éditeur. La classe BotCommand toujours vide, mais pas pour longtemps.

Insérez le code suivant dans la classe:

  //1 private readonly string commandName; //2 public BotCommand(ExecuteCallback executeMethod, string name) { Execute = executeMethod; commandName = name; } //3 public delegate void ExecuteCallback(Bot bot); //4 public ExecuteCallback Execute { get; private set; } //5 public override string ToString() { return commandName; } 

Que se passe-t-il ici?

  1. La variable commandName utilisée simplement pour stocker le nom de commande lisible par l'homme. Il n'est pas nécessaire de l'utiliser dans le modèle, mais nous en aurons besoin plus tard dans le tutoriel.
  2. Le constructeur de BotCommand reçoit une fonction et une chaîne. Cela nous aidera à configurer la méthode Execute de l'objet Command et son name .
  3. Le délégué ExecuteCallback définit le type de méthode encapsulée. La méthode encapsulée retournera vide et acceptera comme paramètre d'entrée un objet de type Bot (composant Bot ).
  4. La propriété Execute fera référence à la méthode encapsulée. Nous l'utiliserons pour appeler la méthode encapsulée.
  5. La méthode ToString est remplacée pour renvoyer la chaîne commandName . C'est pratique, par exemple, pour une utilisation dans l'interface utilisateur.

Enregistrez les modifications et c'est tout! Nous avons réussi à implémenter le modèle de commande.

Reste Ă  l'utiliser.

Constitution d'équipe


Ouvrez le BotInputHandler dans le dossier RW / Scripts .

Ici, nous allons créer cinq instances de BotCommand . Ces instances encapsuleront des méthodes pour déplacer le GameObject Bot vers le haut, le bas, la gauche et la droite, ainsi que pour le tir.

Pour implémenter cela, insérez ce qui suit dans cette classe:

  //1 private static readonly BotCommand MoveUp = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Up); }, "moveUp"); //2 private static readonly BotCommand MoveDown = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Down); }, "moveDown"); //3 private static readonly BotCommand MoveLeft = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Left); }, "moveLeft"); //4 private static readonly BotCommand MoveRight = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Right); }, "moveRight"); //5 private static readonly BotCommand Shoot = new BotCommand(delegate (Bot bot) { bot.Shoot(); }, "shoot"); 

Dans chacune de ces instances, une méthode anonyme est transmise au constructeur. Cette méthode anonyme sera encapsulée dans l'objet de commande correspondant. Comme vous pouvez le voir, la signature de chacune des méthodes anonymes répond aux exigences spécifiées par le délégué ExecuteCallback .

De plus, le deuxième paramètre du constructeur est une chaîne indiquant le nom de la commande. Ce nom sera retourné par la méthode ToString de l'instance de commande. Plus tard, nous l'appliquerons pour l'interface utilisateur.

Dans les quatre premières instances, les méthodes anonymes appellent la méthode Move sur l'objet bot . Cependant, leurs paramètres d'entrée sont différents.

Les commandes MoveUp , MoveDown , MoveLeft et MoveRight transmettent les paramètres de Move CardinalDirection.Up , CardinalDirection.Down , CardinalDirection.Left et CardinalDirection.Right . Comme mentionné dans la section Qu'est-ce que le modèle de conception de commande , ils indiquent différentes directions de déplacement du Bot GameObject.

Dans la cinquième instance, la méthode anonyme appelle la méthode Shoot pour l'objet bot . Grâce à cela, le bot tirera un obus lors de l'exécution de la commande.

Maintenant que nous avons créé les commandes, nous devons en quelque sorte y accéder lorsque l'utilisateur fait une entrée.

Pour ce faire, BotInputHandler code suivant dans le BotInputHandler , immédiatement après les instances de commande:

  public static BotCommand HandleInput() { if (Input.GetKeyDown(KeyCode.W)) { return MoveUp; } else if (Input.GetKeyDown(KeyCode.S)) { return MoveDown; } else if (Input.GetKeyDown(KeyCode.D)) { return MoveRight; } else if (Input.GetKeyDown(KeyCode.A)) { return MoveLeft; } else if (Input.GetKeyDown(KeyCode.F)) { return Shoot; } return null; } 

La méthode HandleInput renvoie une instance de la commande en fonction de la touche enfoncée par l'utilisateur. Enregistrez vos modifications avant de continuer.

Application de commandes


Super, maintenant il est temps d'utiliser les équipes que nous avons créées. Accédez à nouveau à RW / Scripts et ouvrez le script SceneManager dans l'éditeur. Dans cette classe, vous remarquerez un lien vers une variable uiManager de type UIManager .

La classe UIManager fournit des méthodes d'aide utiles pour l' interface utilisateur du terminal que nous utilisons dans cette scène. Si la méthode d' UIManager est utilisée, alors le tutoriel expliquera ce qu'elle fait, mais en général pour nos besoins il n'est pas nécessaire de connaître sa structure interne.

De plus, la variable bot fait référence au composant bot attaché au GameObject Bot .

Ajoutez maintenant le code suivant à la classe SceneManager , en le remplaçant par comment //1 :

  //1 private List<BotCommand> botCommands = new List<BotCommand>(); private Coroutine executeRoutine; //2 private void Update() { if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else { CheckForBotCommands(); } } //3 private void CheckForBotCommands() { var botCommand = BotInputHandler.HandleInput(); if (botCommand != null && executeRoutine == null) { AddToCommands(botCommand); } } //4 private void AddToCommands(BotCommand botCommand) { botCommands.Add(botCommand); //5 uiManager.InsertNewText(botCommand.ToString()); } //6 private void ExecuteCommands() { if (executeRoutine != null) { return; } executeRoutine = StartCoroutine(ExecuteCommandsRoutine()); } private IEnumerator ExecuteCommandsRoutine() { Debug.Log("Executing..."); //7 uiManager.ResetScrollToTop(); //8 for (int i = 0, count = botCommands.Count; i < count; i++) { var command = botCommands[i]; command.Execute(bot); //9 uiManager.RemoveFirstTextLine(); yield return new WaitForSeconds(CommandPauseTime); } //10 botCommands.Clear(); bot.ResetToLastCheckpoint(); executeRoutine = null; } 

Wow, combien de code! Mais ne vous inquiétez pas; nous sommes enfin prêts pour le premier vrai lancement du projet dans la fenêtre de jeu.

J'expliquerai le code plus tard. N'oubliez pas d'enregistrer les modifications.

Exécution du jeu pour tester le modèle de commande


Il est donc temps de construire; Cliquez sur Lecture dans l'éditeur Unity.

Vous devriez pouvoir entrer des commandes de déplacement à l'aide des touches WASD . Pour entrer la commande de prise de vue, appuyez sur la touche F. Pour exécuter des commandes, appuyez sur Entrée .

Remarque : tant que le processus d'exécution n'est pas terminé, la saisie de nouvelles commandes n'est pas possible.



Notez que des lignes sont ajoutées à l'interface utilisateur du terminal. Les équipes dans l'interface utilisateur sont indiquées par leurs noms. Ceci est rendu possible grâce à la variable commandName .

Notez également comment l'interface utilisateur défile vers le haut avant l'exécution et comment les lignes sont supprimées pendant l'exécution.

Nous étudions les équipes de plus près


Il est temps d'apprendre le code que nous avons ajouté dans la section "Application des commandes":

  1. La liste botCommands stocke des liens vers des instances de BotCommand . N'oubliez pas que pour économiser de la mémoire, nous ne pouvons créer que cinq instances de commandes, mais il peut y avoir plusieurs références à une commande. De plus, la variable executeCoroutine fait référence à ExecuteCommandsRoutine , qui contrôle l'exécution de la commande.
  2. Update vérifie si l'utilisateur a appuyé sur la touche Entrée; si c'est le cas, il appelle ExecuteCommands , sinon CheckForBotCommands est CheckForBotCommands .
  3. CheckForBotCommands utilise la méthode statique HandleInput du BotInputHandler pour vérifier si l'utilisateur a terminé l'entrée, et si c'est le cas, la commande est renvoyée . La commande retournée est transmise à AddToCommands . Cependant, si les commandes sont exécutées, c'est-à-dire si executeRoutine pas null, il reviendra sans rien transmettre à AddToCommands . Autrement dit, l'utilisateur doit attendre la fin.
  4. AddToCommands ajoute un nouveau lien vers l'instance retournée de la commande dans botCommands .
  5. La méthode InsertNewText de la classe InsertNewText ajoute une nouvelle ligne de texte à l'interface utilisateur du terminal. Une chaîne de texte est une chaîne passée en paramètre d'entrée. Dans ce cas, nous lui passons commandName.
  6. La méthode ExecuteCommandsRoutine lance ExecuteCommandsRoutine .
  7. ResetScrollToTop de UIManager défiler l'interface utilisateur du terminal vers le haut. Cela se fait juste avant le début de l'exécution.
  8. ExecuteCommandsRoutine contient une boucle for qui itère sur les commandes à l'intérieur de la liste botCommands et les exécute une par une, en passant l'objet bot à la méthode renvoyée par la propriété Execute . Après chaque exécution, une pause est ajoutée en secondes CommandPauseTime .
  9. La méthode RemoveFirstTextLine de UIManager supprime la toute première ligne de texte dans l'interface utilisateur du terminal, si elle existe. Autrement dit, lorsqu'une commande est exécutée, son nom est supprimé de l'interface utilisateur.
  10. Une fois toutes les commandes botCommands est effacé et le bot est réinitialisé au dernier point d'arrêt à l'aide de ResetToLastCheckpoint . À la fin, executeRoutine null et l'utilisateur peut continuer à entrer des commandes.

Implémentation des fonctionnalités d'annulation et de rétablissement


Exécutez à nouveau la scène et essayez d'atteindre le point de contrôle vert.

Vous remarquerez que nous ne pouvons pas annuler la commande entrée. Cela signifie que si vous faites une erreur, vous ne pouvez pas revenir en arrière avant d'avoir terminé toutes les commandes que vous entrez. Vous pouvez résoudre ce problème en ajoutant les fonctionnalités Annuler et Rétablir .

Revenez à SceneManager.cs et ajoutez la déclaration de variable suivante immédiatement après la déclaration List pour botCommands :

  private Stack<BotCommand> undoStack = new Stack<BotCommand>(); 

La variable undoStack est une pile (de la famille Collections) qui stockera toutes les références aux commandes qui peuvent être annulées.

Nous ajoutons maintenant deux méthodes UndoCommandEntry et RedoCommandEntry qui exécuteront Undo et Redo. Dans la classe SceneManager , SceneManager code suivant après ExecuteCommandsRoutine :

  private void UndoCommandEntry() { //1 if (executeRoutine != null || botCommands.Count == 0) { return; } undoStack.Push(botCommands[botCommands.Count - 1]); botCommands.RemoveAt(botCommands.Count - 1); //2 uiManager.RemoveLastTextLine(); } private void RedoCommandEntry() { //3 if (undoStack.Count == 0) { return; } var botCommand = undoStack.Pop(); AddToCommands(botCommand); } 

Analysons le code:

  1. Si des commandes sont exécutées ou si la liste botCommands vide, la méthode UndoCommandEntry rien. Sinon, il écrit un lien vers la dernière commande entrée dans la pile undoStack . Cela supprime également le lien vers la commande de la liste botCommands .
  2. La méthode RemoveLastTextLine de UIManager supprime la dernière ligne de texte de l'interface utilisateur du terminal afin que l'interface utilisateur corresponde au contenu de botCommands .
  3. Si la pile undoStack vide, alors RedoCommandEntry ne fait rien. Sinon, il extrait la dernière commande du haut de undoStack et l'ajoute à la liste des AddToCommands utilisant AddToCommands .

Nous allons maintenant ajouter une entrée au clavier pour utiliser ces fonctions. Dans la classe SceneManager remplacez le corps de la méthode Update par le code suivant:

  if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else if (Input.GetKeyDown(KeyCode.U)) //1 { UndoCommandEntry(); } else if (Input.GetKeyDown(KeyCode.R)) //2 { RedoCommandEntry(); } else { CheckForBotCommands(); } 

  1. Lorsque vous appuyez sur la touche U , la méthode UndoCommandEntry est UndoCommandEntry .
  2. Lorsque vous appuyez sur la touche R , la méthode RedoCommandEntry est RedoCommandEntry .

Gestion des cas de bord


Super, nous avons presque fini! Mais d'abord, nous devons faire ce qui suit:

  1. Lorsque vous entrez une nouvelle commande, la pile undoStack doit être effacée.
  2. Avant d'exécuter des commandes, la pile undoStack doit être effacée.

Pour implémenter cela, nous devons d'abord ajouter une nouvelle méthode à SceneManager . Insérez la méthode suivante après CheckForBotCommands :

  private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); } 

Cette méthode efface undoStack et appelle ensuite la méthode AddToCommands .

Remplacez maintenant l'appel Ă  AddToCommands dans CheckForBotCommands par le code suivant:

  AddNewCommand(botCommand); 

Insérez ensuite la ligne suivante après l' if dans la méthode ExecuteCommands à effacer avant d'exécuter les commandes undoStack :

  undoStack.Clear(); 

Et nous avons enfin terminé!

Sauvegardez votre travail. Générez le projet et cliquez sur dans l'éditeur de lecture . Saisissez les commandes comme précédemment. Appuyez sur U pour annuler les commandes. Appuyez sur R pour répéter les commandes annulées.


Essayez d'arriver au point de contrĂ´le vert.

OĂą aller ensuite?


Pour en savoir plus sur les modèles de conception utilisés dans la programmation de jeux, je vous recommande d'étudier les modèles de programmation de jeux de Robert Nystrom.

Pour en savoir plus sur les techniques C # avancées, suivez le cours Collections C #, Lambdas et LINQ .

Tâche


En tant que tâche, essayez d'arriver au point de contrôle vert à la fin du labyrinthe. J'ai caché une des solutions sous le spoiler.

Solution
  • moveUp Ă— 2
  • moveRight Ă— 3
  • moveUp Ă— 2
  • moveLeft
  • tirer
  • moveLeft Ă— 2
  • moveUp Ă— 2
  • moveLeft Ă— 2
  • moveDown Ă— 5
  • moveLeft
  • tirer
  • moveLeft
  • moveUp Ă— 3
  • tirer Ă— 2
  • moveUp Ă— 5
  • moveRight Ă— 3

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


All Articles