Laissez vos joueurs annuler les erreurs du jeu avec le motif de commande

Beaucoup de jeux au tour comprennent un annuler bouton pour permettre aux joueurs d’annuler les erreurs qu’ils commettent pendant le jeu. Cette fonctionnalité devient particulièrement pertinente pour le développement de jeux sur mobiles où le toucher peut avoir une reconnaissance tactile maladroite. Plutôt que de compter sur un système dans lequel vous demandez à l'utilisateur "êtes-vous sûr de vouloir effectuer cette tâche?" pour chaque action entreprise, il est beaucoup plus efficace de les laisser faire des erreurs et d’avoir la possibilité d’inverser facilement leur action. Dans ce tutoriel, nous verrons comment implémenter cela en utilisant le Modèle de commande, en utilisant l'exemple d'un jeu de tic-tac-toe.

Remarque: Bien que ce tutoriel ait été écrit en Java, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux. (Ce n'est pas limité aux jeux de tic-tac-toe, non plus!)


Aperçu du résultat final

Le résultat final de ce tutoriel est un jeu de tic-tac-toe qui offre des opérations illimitées d'annulation et de restauration..

Cette démo nécessite Java pour fonctionner.

Impossible de charger l'applet? Regardez la vidéo de gameplay sur YouTube:

Vous pouvez également exécuter la démonstration sur la ligne de commande en utilisant TicTacToeMain en tant que classe principale à partir de laquelle exécuter. Après avoir extrait la source, exécutez les commandes suivantes:

 javac * .java java TicTacToeMain

Étape 1: Créer une implémentation de base de Tic-Tac-Toe

Pour ce tutoriel, vous allez envisager une implémentation de tic-tac-toe. Bien que le jeu soit extrêmement trivial, les concepts fournis dans ce didacticiel peuvent s’appliquer à des jeux beaucoup plus complexes..

Le téléchargement suivant (qui diffère du téléchargement de la source finale) contient le code de base d’un modèle de jeu de tic-tac-toe qui ne pas contient une fonction annuler ou rétablir. Ce sera votre travail de suivre ce tutoriel et d’ajouter ces fonctionnalités. Télécharger la base TicTacToeModel.java.

Vous devriez prendre note, en particulier, des méthodes suivantes:

public void placeX (int row, int col) assert (playerXTurn); assert (espaces [rangée] [col] == 0); espaces [ligne] [col] = 1; playerXTurn = false; 
public void placeO (int row, int col) assert (! playerXTurn); assert (espaces [rangée] [col] == 0); espaces [ligne] [col] = 2; playerXTurn = true; 

Ces méthodes sont les seules méthodes pour ce jeu qui changent l’état de la grille de jeu. Ils seront ce que tu changeras.

Si vous n'êtes pas un développeur Java, vous pourrez probablement toujours comprendre le code. Il est copié ici si vous voulez juste y faire référence:

 / ** La logique de jeu pour un jeu de Tic-Tac-Toe. Ce modèle n'a pas * d'interface utilisateur associée: c'est juste la logique du jeu. * * Le jeu est représenté par un simple tableau d'entiers 3x3. Une valeur de * 0 signifie que l'espace est vide, 1 signifie que c'est un X, 2 signifie que c'est un O. * * @author aarnott * * / public class TicTacToeModel // Vrai si c'est le tour du joueur X, faux si c'est le tour du joueur booléen privéXTurn du joueur O; // L'ensemble des espaces sur la grille de jeux private int [] [] spaces; / ** Initialise un nouveau modèle de jeu. Dans le jeu traditionnel Tic-Tac-Toe *, X est le premier. * * / public TicTacToeModel () spaces = new int [3] [3]; playerXTurn = true;  / ** Renvoie true si c'est le tour du joueur X. * * @return * / public boolean isPlayerXTurn () return playerXTurn;  / ** Retourne vrai si c'est le tour du joueur O. * * @return * / public boolean isPlayerOTurn () return! playerXTurn;  / ** Place un X sur un espace spécifié par les paramètres row et column *. * * Conditions préalables: * -> Ce doit être le tour du joueur X * -> L'espace doit être vide * * @param row La ligne pour placer le X sur * @param col La colonne pour placer le X sur * / public void placeX (int row, int col) assert (playerXTurn); assert (espaces [rangée] [col] == 0); espaces [ligne] [col] = 1; playerXTurn = false;  / ** Place un O sur un espace spécifié par les paramètres row et column *. * * Conditions préalables: * -> Ce doit être le tour du joueur O * -> L'espace doit être vide * * @param row La ligne pour placer le O sur * @param col La colonne pour placer le O sur * / public void placeO (int row, int col) assert (! playerXTurn); assert (espaces [rangée] [col] == 0); espaces [ligne] [col] = 2; playerXTurn = true;  / ** Renvoie true si un espace de la grille est vide (pas de X ni d'Os) * * @param row * @param col * @return * / public boolean isSpaceEmpty (int row, int col) return (spaces [row ] [col] == 0);  / ** Renvoie true si un espace de la grille est un X. * * @param row * @param col * @return * / public boolean isSpaceX (int row, int col) return (espaces [row] [col] == 1);  / ** Renvoie true si un espace de la grille est un O. * * @param row * @param col * @return * / public boolean isSpaceO (int row, int col) return (espaces [row] [col] == 2);  / ** Retourne vrai si le joueur X a gagné la partie. En d’autres termes, si le joueur * X a complété une ligne de trois X. * * @return * / public booléen hasPlayerXWon () // Vérifier les lignes si (espaces [0] [0] == 1 && spaces [0] [1] == 1 && espaces [0] [2] == 1 ) retourne vrai; if (espaces [1] [0] == 1 && espaces [1] [1] == 1 && espaces [1] [2] == 1) renvoie la valeur true; if (espaces [2] [0] == 1 && espaces [2] [1] == 1 && espaces [2] [2] == 1) renvoie la valeur true; // Vérifie les colonnes si (espaces [0] [0] == 1 && espaces [1] [0] == 1 && espaces [2] [0] == 1) renvoie la valeur true; if (espaces [0] [1] == 1 && espaces [1] [1] == 1 && espaces [2] [1] == 1) renvoie la valeur true; if (espaces [0] [2] == 1 && espaces [1] [2] == 1 && espaces [2] [2] == 1) renvoie la valeur true; // Vérifie les diagonales si (espaces [0] [0] == 1 && espaces [1] [1] == 1 && espaces [2] [2] == 1) return true; if (espaces [0] [2] == 1 && espaces [1] [1] == 1 && espaces [2] [0] == 1) renvoie la valeur true; // Sinon, il n'y a pas de retour de ligne faux;  / ** Retourne vrai si le joueur O a gagné la partie. Autrement dit, si le joueur * O a complété une ligne de trois os. * * @return * / public booléen hasPlayerOWon () // Vérifier les lignes si (espaces [0] [0] == 2 && espaces [0] [1] == 2 && espaces [0] [2] == 2 ) retourne vrai; if (espaces [1] [0] == 2 && espaces [1] [1] == 2 && espaces [1] [2] == 2) renvoie la valeur true; if (espaces [2] [0] == 2 && espaces [2] [1] == 2 && espaces [2] [2] == 2) renvoie la valeur true; // Vérifie les colonnes si (espaces [0] [0] == 2 && espaces [1] [0] == 2 && espaces [2] [0] == 2) renvoie la valeur true; if (espaces [0] [1] == 2 && espaces [1] [1] == 2 && espaces [2] [1] == 2) renvoie la valeur true; if (espaces [0] [2] == 2 && espaces [1] [2] == 2 && espaces [2] [2] == 2) renvoie la valeur true; // Vérifie les diagonales si (espaces [0] [0] == 2 && espaces [1] [1] == 2 && espaces [2] [2] == 2) return true; if (espaces [0] [2] == 2 && espaces [1] [1] == 2 && espaces [2] [0] == 2) renvoie la valeur true; // Sinon, il n'y a pas de retour de ligne faux;  / ** Retourne vrai si tous les espaces sont remplis ou si l'un des joueurs a * gagné la partie. * * @return * / public boolean isGameOver () if (hasPlayerXWon () || hasPlayerOWon ()) return true; // Vérifie si tous les espaces sont remplis. Si ce n'est pas le cas, la partie n'est pas terminée pour (int row = 0; row < 3; row++)  for(int col = 0; col < 3; col++)  if(spaces[row][col] == 0) return false;   //Otherwise, it is a “cat's game” return true;  

Étape 2: Comprendre le modèle de commande

le Commander motif est un motif de conception couramment utilisé avec les interfaces utilisateur pour séparer les actions effectuées par les boutons, menus ou autres widgets des définitions de code d'interface utilisateur de ces objets. Ce concept de code d'action séparant peut être utilisé pour suivre chaque changement qui se produit dans l'état d'un jeu, et vous pouvez utiliser cette information pour inverser les changements..

La version la plus basique du Commander motif est l'interface suivante:

interface publique Command public void execute (); 

Tout Une action entreprise par le programme qui modifie l’état du jeu - telle que placer un X dans un espace spécifique - implémentera la Commander interface. Lorsque l'action est prise, le exécuter() la méthode s'appelle.

Maintenant, vous avez probablement remarqué que cette interface n'offre pas la possibilité d'annuler des actions; tout ce qu’il fait, c’est faire passer le jeu d’un État à un autre. L'amélioration suivante permettra aux actions de mise en œuvre d'offrir une capacité d'annulation.

interface publique Command public void execute (); public void undo (); 

L'objectif lors de la mise en œuvre d'un Commander sera d'avoir le annuler() méthode inverse chaque action prise par le exécuter méthode. En conséquence, le exécuter() méthode sera également en mesure de fournir la possibilité de refaire une action.

C'est l'idée de base. Cela deviendra plus clair lorsque nous implémenterons des commandes spécifiques pour ce jeu..


Étape 3: Créer un gestionnaire de commandes

Pour ajouter une fonction d'annulation, vous créerez un CommandManager classe. le CommandManager est responsable du suivi, de l'exécution et de l'annulation Commander implémentations.

(Rappelons que le Commander interface fournit les méthodes pour effectuer des modifications d’un état d’un programme à un autre et l’inverser également.)

Classe publique CommandManager private Command lastCommand; public CommandManager ()  public void executeCommand (Commande c) c.execute (); lastCommand = c; …

Pour exécuter un Commander, la CommandManager est passé un Commander par exemple, et il exécutera le Commander puis stocker le dernier exécuté Commander pour référence ultérieure.

Ajout de la fonction d'annulation à la CommandManager exige simplement de lui dire de défaire le plus récent Commander qui a exécuté.

public boolean isUndoAvailable () return lastCommand! = null;  public void undo () assert (lastCommand! = null); lastCommand.undo (); lastCommand = null; 

Ce code est tout ce qui est nécessaire pour avoir un fonctionnel CommandManager. Pour qu’il fonctionne correctement, vous devez créer certaines implémentations de la Commander interface.


Étape 4: Créer des implémentations du Commander Interface

Le but de la Commander Le modèle de ce didacticiel consiste à déplacer tout code qui modifie l’état du jeu de tic-tac-toe dans un Commander exemple. À savoir, le code dans les méthodes placeX () et placeO () êtes ce que vous allez changer.

À l'intérieur de TicTacToeModel classe, ajoutez deux nouvelles classes internes appelées PlaceXCommand et PlaceOCommand, respectivement, qui mettent en œuvre les Commander interface.

classe publique TicTacToeModel … classe privée PlaceXCommand implémente la commande public void execute () … publique void undo () … classe privée PlaceOCommand implémente la commande public void execute () … public void undo () … 

Le travail d'un Commander L’implémentation consiste à stocker un état et à avoir une logique pour le passage à un nouvel état résultant de l’exécution de la Commander ou pour revenir à l'état initial avant la Commander a été exécuté. Il existe deux manières simples de réaliser cette tâche..

  1. Stocker tout l'état précédent et l'état suivant. Définit l’état actuel du jeu sur l’état suivant lorsque exécuter() est appelé et définit l'état actuel du jeu à l'état précédent stocké lorsque annuler() est appelé.
  2. Stocker uniquement les informations qui changent entre les états. Ne changez que ces informations stockées lorsque exécuter() ou annuler() est appelé.
// Option 1: Stockage de la classe privée d'états précédent et suivant PlaceXCommand implémente la commande modèle privé TicTacToeModel; // private int [] [] previousGridState; private boolean previousTurnState; int privée [] [] nextGridState; privé booléen nextTurnState; // PlaceXCommand privé (modèle TicTacToeModel, int row, int col) this.model = model; // previousTurnState = model.playerXTurn; // Copier la grille entière pour les deux états previousGridState = new int [3] [3]; nextGridState = new int [3] [3]; pour (int i = 0; i < 3; i++)  for(int j = 0; j < 3; j++)  //This is allowed because this class is an inner //class. Otherwise, the model would need to //provide array access somehow. previousGridState[i][j] = m.spaces[i][j]; nextGridState[i][j] = m.spaces[i][j];   //Figure out the next state by applying the placeX logic nextGridState[row][col] = 1; nextTurnState = false;  // public void execute()  model.spaces = nextGridState; model.playerXTurn = nextTurnState;  // public void undo()  model.spaces = previousGridState; model.playerXTurn = previousTurnState;  

La première option est un peu inutile, mais cela ne signifie pas qu’elle est mal conçue. Le code est simple et, à moins que les informations d'état soient extrêmement volumineuses, la quantité de déchets ne sera pas un sujet de préoccupation..

Vous verrez que, dans le cas de ce tutoriel, la deuxième option est préférable, mais que cette approche ne sera pas toujours la meilleure pour tous les programmes. Le plus souvent, cependant, la deuxième option sera la voie à suivre.

// Option 2: Stocker uniquement les modifications entre les états La classe privée PlaceXCommand implémente la commande modèle privé TicTacToeModel; private int previousValue; private boolean previousTurn; privé int row; privé int col; // PlaceXCommand privé (modèle TicTacToeModel, int row, int col) this.model = model; this.row = rangée; this.col = col; // Copie la valeur précédente de la grille this.previousValue = model.spaces [row] [col]; this.previousTurn = model.playerXTurn;  // public void execute () model.spaces [row] [col] = 1; model.playerXTurn = false;  // public void undo () model.spaces [row] [col] = previousValue; model.playerXTurn = previousTurn; 

La deuxième option ne stocke que les changements qui surviennent, plutôt que l'état entier. Dans le cas de tic-tac-toe, il est plus efficace et pas particulièrement complexe d’utiliser cette option..

le PlaceOCommand la classe intérieure est écrite de la même manière - essayez de l'écrire vous-même!


Étape 5: Tout mettre ensemble

Afin de faire usage de votre Commander implémentations, PlaceXCommand et PlaceOCommand, vous devrez modifier le TicTacToeModel classe. La classe doit utiliser un CommandManager et il doit utiliser Commander instances au lieu d'appliquer des actions directement.

Classe publique TicTacToeModel private CommandManager commandManager; //… // public TicTacToeModel () … // commandManager = new CommandManager ();  //… // public void placeX (int row, int col) assert (playerXTurn); assert (espaces [rangée] [col] == 0); commandManager.executeCommand (nouveau PlaceXCommand (this, row, col));  // public void placeO (int row, int col) assert (! playerXTurn); assert (espaces [rangée] [col] == 0); commandManager.executeCommand (nouveau PlaceOCommand (this, row, col));  //…

le TicTacToeModel La classe fonctionnera exactement comme avant vos modifications, mais vous pouvez également exposer la fonction d'annulation. Ajouter un annuler() méthode au modèle et aussi ajouter une méthode de contrôle peutUndo pour l'interface utilisateur à utiliser à un moment donné.

classe publique TicTacToeModel //… // public boolean canUndo () return commandManager.isUndoAvailable ();  // public void undo () commandManager.undo (); 

Vous avez maintenant un modèle de jeu de tic-tac-toe complètement fonctionnel qui prend en charge l'annulation!


Étape 6: allez plus loin

Avec quelques petites modifications à la CommandManager, vous pouvez ajouter un support pour les opérations de rétablissement ainsi qu'un nombre illimité d'annulations et de restaurations.

Le concept d'une fonctionnalité de rétablissement est à peu près identique à celui d'une fonctionnalité d'annulation. En plus de stocker le dernier Commander exécuté, vous stockez également le dernier Commander c'était défait. Vous stockez cela Commander quand un undo est appelé et effacer quand un Commander est exécuté.

classe publique CommandManager private Command lastCommandUndone;… public void executeCommand (Commande c) c.execute (); lastCommand = c; lastCommandUndone = null;  public void undo () assert (lastCommand! = null); lastCommand.undo (); lastCommandUndone = lastCommand; lastCommand = null;  public boolean isRedoAvailable () return lastCommandUndone! = null;  public void redo () assert (lastCommandUndone! = null); lastCommandUndone.execute (); lastCommand = lastCommandUndone; lastCommandUndone = null; 

Ajouter plusieurs annulations et répétitions est une question de stockage d’une empiler des actions annulables et refaisables. Lorsqu'une nouvelle action est exécutée, elle est ajoutée à la pile d'annulation et la pile de restauration est effacée. Lorsqu'une action est annulée, elle est ajoutée à la pile de restauration et supprimée de la pile d'annulation. Lorsqu'une action est refaite, elle est retirée de la pile de restauration et ajoutée à la pile d'annulation..

L'image ci-dessus montre un exemple des piles en action. La pile de restauration contient deux éléments issus de commandes déjà annulées. Quand de nouvelles commandes, PlaceX (0,0) et PlaceO (0,1), sont exécutés, la pile de restauration est effacée et ils sont ajoutés à la pile d'annulation. Lorsqu'un PlaceO (0,1) est annulé, il est retiré du haut de la pile d'annulation et placé sur la pile de rétablissement.

Voici à quoi ça ressemble dans le code:

Classe publique CommandManager pile privée undos = nouvelle pile(); pile privée redos = nouvelle pile(); public void executeCommand (Commande c) c.execute (); undos.push (c); redos.clear ();  public boolean isUndoAvailable () return! undos.empty ();  public void undo () assert (! undos.empty ()); Commande commande = undos.pop (); command.undo (); redos.push (commande);  public boolean isRedoAvailable () return! redos.empty ();  public void redo () assert (! redos.empty ()); Commande commande = redos.pop (); command.execute (); undos.push (commande); 

Maintenant, vous avez un modèle de jeu de tic-tac-toe qui peut annuler des actions jusqu'au début du jeu et les refaire à nouveau.

Si vous souhaitez voir comment tout cela s’agence, récupérez le dernier téléchargement source, qui contient le code complet de ce didacticiel..


Conclusion

Vous avez peut-être remarqué que la finale CommandManager vous avez écrit va travailler pour tout Commander mises en œuvre. Cela signifie que vous pouvez coder une CommandManager dans votre langue préférée, créez des exemples de Commander interface, et avoir un système complet préparé pour annuler / rétablir. La fonction d'annulation peut être un excellent moyen de permettre aux utilisateurs d'explorer votre jeu et de faire des erreurs sans se sentir engagé dans de mauvaises décisions..

Merci de vous intéresser à ce tutoriel!

Pensez à ce qui suit: Commander motif avec le CommandManager vous permettent de suivre chaque changement d'état pendant l'exécution de votre partie. Si vous enregistrez ces informations, vous pouvez créer des replays de l'exécution du programme..