Laat uw spelers hun fouten in het spel ongedaan maken met het opdrachtpatroon

Veel turn-based spellen bevatten een ongedaan maken om de fouten die ze maken tijdens het spelen om te keren. Deze functie wordt vooral relevant voor de ontwikkeling van mobiele games, waarbij de aanraking onhandige aanrakingsherkenning kan hebben. In plaats van te vertrouwen op een systeem waarbij u de gebruiker vraagt: "Weet u zeker dat u deze taak wilt uitvoeren?" voor elke actie die ze ondernemen, is het veel efficiënter om fouten te maken en de mogelijkheid te hebben om eenvoudig hun actie om te keren. In deze zelfstudie zullen we kijken hoe we dit kunnen implementeren met behulp van de Commandopatroon, gebruikmakend van het voorbeeld van een boterwedstrijd.

Notitie: Hoewel deze tutorial geschreven is met behulp van Java, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken. (Het is niet beperkt tot tic-tac-toe games, ofwel!)


Eindresultaat voorbeeld

Het eindresultaat van deze tutorial is een tic-tac-toe-game die onbeperkte bewerkingen ongedaan maken en opnieuw uitvoeren biedt.

Voor deze demo moet Java worden uitgevoerd.

Kan de applet niet worden geladen? Bekijk de gameplay-video op YouTube:

U kunt de demo ook op de opdrachtregel uitvoeren met TicTacToeMain als de hoofdklasse om uit te voeren. Na het uitpakken van de bron voert u de volgende opdrachten uit:

 javac * .java java TicTacToeMain

Stap 1: Maak een basisimplementatie van Tic-Tac-Toe

Voor deze tutorial ga je een implementatie van tic-tac-toe overwegen. Hoewel de game extreem triviaal is, kunnen de concepten in deze tutorial van toepassing zijn op veel complexere games.

De volgende download (die verschilt van de uiteindelijke brondownload) bevat de basiscode voor een tic-tac-toe gamemodel dat wel niet bevatten een ongedaan maken of opnieuw uitvoeren functie. Het is jouw taak om deze tutorial te volgen en deze functies toe te voegen. Download de basis TicTacToeModel.java.

U moet vooral letten op de volgende methoden:

public void placeX (int row, int col) assert (playerXTurn); beweren (spaties [rij] [col] == 0); spaties [rij] [col] = 1; playerXTurn = false; 
public void placeO (int row, int col) assert (! playerXTurn); beweren (spaties [rij] [col] == 0); spaties [rij] [col] = 2; playerXTurn = true; 

Deze methoden zijn de enige methoden voor dit spel die de status van het spelraster wijzigen. Ze zullen zijn wat je gaat veranderen.

Als u geen Java-ontwikkelaar bent, kunt u de code waarschijnlijk nog steeds begrijpen. Het is hier gekopieerd als je er gewoon naar wilt verwijzen:

 / ** De spellogica voor een Tic-Tac-Toe-spel. Dit model heeft geen * een bijbehorende gebruikersinterface: het is gewoon de spellogica. * * Het spel wordt voorgesteld door een eenvoudige 3x3 integer array. Een waarde van * 0 betekent dat de spatie leeg is, 1 betekent dat het een X is, 2 betekent dat het een O is. * * @Author aarnott * * / public class TicTacToeModel // Waar als het de beurt aan de X-speler is, false als het is de beurt private Boolean playerXTurn; // De verzameling spaties in de private int [] [] spaties van het spelraster; / ** Initialiseer een nieuw gamemodel. In de traditionele Tic-Tac-Toe * -game gaat X eerst. * * / public TicTacToeModel () spaces = new int [3] [3]; playerXTurn = true;  / ** Geeft true als het de beurt aan de X-speler is. * * @return * / public boolean isPlayerXTurn () return playerXTurn;  / ** Geeft true als het de beurt aan de O-speler is. * * @return * / public boolean isPlayerOTurn () return! playerXTurn;  / ** Plaatst een X op een spatie gespecificeerd door de rij- en kolomparameters *. * * Voorwaarden: * -> Het moet de beurt zijn van de X-speler * -> De spatie moet leeg zijn * * @param row De rij om de X op te zetten * @param col De kolom om de X op * / public void placeX te plaatsen (int row, int col) assert (playerXTurn); beweren (spaties [rij] [col] == 0); spaties [rij] [col] = 1; playerXTurn = false;  / ** Plaatst een O op een spatie gespecificeerd door de rij- en kolomparameters *. * * Voorwaarden: * -> Het moet de beurt van de O-speler zijn * -> De spatie moet leeg zijn * * @param row De rij om de O on * @param col te plaatsen De kolom om de O on * / public void placeO te plaatsen (int row, int col) assert (! playerXTurn); beweren (spaties [rij] [col] == 0); spaties [rij] [col] = 2; playerXTurn = true;  / ** Geeft true terug als een spatie op het raster leeg is (geen Xs of Os) * * @param row * @param col * @return * / public boolean isSpaceEmpty (int row, int col) return (spaties [rij ] [col] == 0);  / ** Geeft true als een spatie in het raster een X is. * * @Param row * @param col * @return * / public boolean isSpaceX (int row, int col) return (spaties [rij] [col] == 1);  / ** Retourneert true als een spatie in het raster een O. is * * @param row * @param col * @return * / public boolean isSpaceO (int row, int col) return (spaties [rij] [col] == 2);  / ** Geeft true als de X-speler de game heeft gewonnen. Dat wil zeggen, als de * X-speler een rij van drie X's heeft voltooid. * * @return * / public boolean hasPlayerXWon () // Controleer rijen if (spaties [0] [0] == 1 && spaties [0] [1] == 1 && spaties [0] [2] == 1 ) retourneer waar; if (spaties [1] [0] == 1 && spaties [1] [1] == 1 && spaties [1] [2] == 1) retourneer waar; if (spaties [2] [0] == 1 && spaties [2] [1] == 1 && spaties [2] [2] == 1) retourneer waar; // Controleer de kolommen als (spaties [0] [0] == 1 && spaties [1] [0] == 1 && spaties [2] [0] == 1) waar retourneren; if (spaties [0] [1] == 1 && spaties [1] [1] == 1 && spaties [2] [1] == 1) retourneer waar; if (spaties [0] [2] == 1 && spaties [1] [2] == 1 && spaties [2] [2] == 1) retourneer waar; // Controleer diagonalen als (spaties [0] [0] == 1 && spaties [1] [1] == 1 && spaties [2] [2] == 1) retourneer waar; if (spaties [0] [2] == 1 && spaties [1] [1] == 1 && spaties [2] [0] == 1) retourneren waar; // Anders is er geen regelterugloop false;  / ** Geeft true als de O-speler de game heeft gewonnen. Dat wil zeggen, als de * O-speler een regel van drie Os heeft voltooid. * * @return * / public boolean hasPlayerOWon () // Controleer rijen if (spaties [0] [0] == 2 && spaties [0] [1] == 2 && spaties [0] [2] == 2 ) retourneer waar; if (spaties [1] [0] == 2 && spaties [1] [1] == 2 && spaties [1] [2] == 2) retourneer waar; if (spaties [2] [0] == 2 && spaties [2] [1] == 2 && spaties [2] [2] == 2) retourneren waar; // Controleer kolommen als (spaties [0] [0] == 2 && spaties [1] [0] == 2 && spaties [2] [0] == 2) waar retourneren; if (spaties [0] [1] == 2 && spaties [1] [1] == 2 && spaties [2] [1] == 2) retourneren waar; if (spaties [0] [2] == 2 && spaties [1] [2] == 2 && spaties [2] [2] == 2) retourneren waar; // Controleer diagonalen als (spaties [0] [0] == 2 && spaties [1] [1] == 2 && spaties [2] [2] == 2) retourneer waar; if (spaties [0] [2] == 2 && spaties [1] [1] == 2 && spaties [2] [0] == 2) retourneren waar; // Anders is er geen regelterugloop false;  / ** Geeft true als alle spaties zijn gevuld of een van de spelers * het spel heeft gewonnen. * * @return * / public boolean isGameOver () if (hasPlayerXWon () || hasPlayerOWon ()) return true; // Controleer of alle spaties zijn gevuld. Als dat niet het geval is, is het spel nog niet voorbij (int row = 0; rij < 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;  

Stap 2: Begrijp het opdrachtpatroon

De Commando patroon is een ontwerppatroon dat gewoonlijk wordt gebruikt met gebruikersinterfaces om acties die worden uitgevoerd door knoppen, menu's of andere widgets te scheiden van de codeformalisaties van de gebruikersinterface voor deze objecten. Dit concept van het scheiden van actiecode kan worden gebruikt om elke wijziging bij te houden die met de status van een spel gebeurt, en u kunt deze informatie gebruiken om de wijzigingen ongedaan te maken.

De meest eenvoudige versie van de Commando patroon is de volgende interface:

openbare interface Command public void execute (); 

Ieder actie die wordt ondernomen door het programma dat de status van het spel verandert - zoals het plaatsen van een X in een specifieke ruimte - zal het Commando interface. Wanneer de actie wordt ondernomen, de execute () methode wordt genoemd.

U hebt waarschijnlijk opgemerkt dat deze interface niet de mogelijkheid biedt om acties ongedaan te maken; het enige dat het doet is het spel van de ene staat naar de andere brengen. Met de volgende verbetering kunnen acties worden uitgevoerd om de mogelijkheid tot ongedaan maken aan te bieden.

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

Het doel bij het implementeren van een Commando zal zijn om de undo () methode reverse elke actie die door de uitvoeren methode. Dientengevolge, de execute () methode zal ook de mogelijkheid bieden om een ​​actie opnieuw uit te voeren.

Dat is het basisidee. Het wordt duidelijker als we specifieke opdrachten voor dit spel implementeren.


Stap 3: Maak een Command Manager

Om een ​​undo-functie toe te voegen, maakt u een CommandManager klasse. De CommandManager is verantwoordelijk voor het volgen, uitvoeren en ongedaan maken Commando implementaties.

(Bedenk dat de Commando interface biedt de methoden om wijzigingen aan te brengen van de ene status van een programma naar een ander en omgekeerd.)

public class CommandManager private Command lastCommand; public CommandManager ()  public void executeCommand (Command c) c.execute (); lastCommand = c;  ...

Om een ​​uit te voeren Commando, de CommandManager is geslaagd a Commando Bijvoorbeeld, en het zal de Commando en vervolgens de laatst uitgevoerde opslaan Commando voor latere referentie.

Toevoegen van de functie Ongedaan maken aan de CommandManager vereist gewoon dat je het vertelt om de meest recente ongedaan te maken Commando dat is uitgevoerd.

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

Deze code is alles wat nodig is om een ​​functioneel te hebben CommandManager. Om ervoor te zorgen dat het goed werkt, moet u enkele implementaties van de. Maken Commando interface.


Stap 4: Implementaties van de Commando Interface

Het doel van de Commando patroon voor deze tutorial is om elke code te verplaatsen die de status van het spel met de tic-tac-teen verandert in een Commando aanleg. Namelijk, de code in de methoden placeX () en placeO () zijn wat je gaat veranderen.

Binnen in de TicTacToeModel klasse, voeg twee nieuwe innerlijke klassen toe genaamd PlaceXCommand en PlaceOCommand, respectievelijk, die elk het Commando interface.

public class TicTacToeModel ... private class PlaceXCommand implementeert Command public void execute () ... public void undo () ... private class PlaceOCommand implementements Command public void execute () ... public void undo () ... 

De taak van een Commando implementatie is om een ​​staat op te slaan en logica te hebben om ofwel over te gaan naar een nieuwe staat die resulteert uit de uitvoering van de Commando of om terug te gaan naar de oorspronkelijke staat vóór de Commando is geëxecuteerd. Er zijn twee eenvoudige manieren om deze taak te bereiken.

  1. Bewaar de volledige vorige staat en volgende staat. Stel de huidige status van de game in op de volgende status wanneer execute () wordt aangeroepen en stelt de huidige staat van het spel in op de opgeslagen vorige staat wanneer undo () wordt genoemd.
  2. Bewaar alleen de informatie die tussen de staten verandert. Wijzig alleen deze opgeslagen informatie wanneer execute () of undo () wordt genoemd.
// Optie 1: opslaan van de vorige en volgende privé-klasse PlaceXCommand implementeert Command private TicTacToeModel-model; // private int [] [] previousGridState; private boolean vorigeTurnState; private int [] [] nextGridState; private boolean nextTurnState; // private PlaceXCommand (TicTacToeModel-model, int row, int col) this.model = model; // vorigeTurnState = model.playerXTurn; // Kopieer het volledige raster voor beide staten previousGridState = new int [3] [3]; nextGridState = nieuwe int [3] [3]; voor (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;  

De eerste optie is een beetje verkwistend, maar dat betekent niet dat het een slecht ontwerp is. De code is eenvoudig en tenzij de statusinformatie extreem groot is, hoeft u zich geen zorgen te maken over de hoeveelheid afval.

Je zult zien dat, in het geval van deze tutorial, de tweede optie beter is, maar deze aanpak zal niet altijd de beste zijn voor elk programma. Vaker wel dan niet, echter, is de tweede optie de manier om te gaan.

// Optie 2: alleen de wijzigingen opslaan tussen staten private class PlaceXCommand implementeert Command private TicTacToeModel-model; private int vorigeValue; private boolean vorigeTurn; privé int rij; privé int col; // private PlaceXCommand (TicTacToeModel-model, int row, int col) this.model = model; this.row = rij; this.col = col; // Kopieer de vorige waarde uit het raster 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] = vorigeWaarde; model.playerXTurn = vorigeTurn; 

De tweede optie slaat alleen de wijzigingen op die plaatsvinden, in plaats van de volledige status. In het geval van tic-tac-toe is het efficiënter en niet merkbaar complexer om deze optie te gebruiken.

De PlaceOCommand innerlijke klasse is op een vergelijkbare manier geschreven - probeer het zelf te schrijven!


Stap 5: doe alles samen

Om gebruik te maken van uw Commando implementaties, PlaceXCommand en PlaceOCommand, u moet het wijzigen TicTacToeModel klasse. De klas moet gebruik maken van een CommandManager en het moet gebruiken Commando instanties in plaats van acties rechtstreeks toe te passen.

public class TicTacToeModel private CommandManager commandManager; // ... // public TicTacToeModel () ... // commandManager = new CommandManager ();  // ... // public void placeX (int row, int col) assert (playerXTurn); beweren (spaties [rij] [col] == 0); commandManager.executeCommand (nieuwe PlaceXCommand (this, row, col));  // public void placeO (int row, int col) assert (! playerXTurn); beweren (spaties [rij] [col] == 0); commandManager.executeCommand (nieuwe PlaceOCommand (this, row, col));  // ...

De TicTacToeModel De klas zal precies hetzelfde werken als vóór uw wijzigingen nu, maar u kunt ook de functie Ongedaan maken blootleggen. Voeg een toe undo () methode aan het model en voeg ook een controlemethode toe canUndo voor de gebruikersinterface om op een bepaald moment te gebruiken.

public class TicTacToeModel // ... // public boolean canUndo () return commandManager.isUndoAvailable ();  // public void undo () commandManager.undo (); 

Je hebt nu een volledig functioneel tic-tac-teen gamemodel dat ongedaan maken ondersteunt!


Stap 6: Neem het verder

Met een paar kleine aanpassingen aan de CommandManager, je kunt ondersteuning toevoegen voor redo-bewerkingen en een onbeperkt aantal ongedaan maken en opnieuw uitvoeren.

Het concept achter een opnieuw-functie is vrijwel hetzelfde als een functie voor ongedaan maken. Naast het opslaan van de laatste Commando uitgevoerd, slaat u ook de laatste op Commando dat was ongedaan gemaakt. Je bewaart dat Commando wanneer een ongedaan maken wordt aangeroepen en wis het wanneer a Commando is geëxecuteerd.

public class CommandManager private Command lastCommandUndone; ... public void executeCommand (Command 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; 

Het toevoegen van meerdere undos en redos is een kwestie van het opslaan van een stack van ongedaan maken en opnieuw in te zetten acties. Wanneer een nieuwe actie wordt uitgevoerd, wordt deze aan de stapel voor ongedaan maken toegevoegd en wordt de opnieuw uitgevoerde stapel gewist. Wanneer een actie ongedaan wordt gemaakt, wordt deze aan de nieuwe stapel toegevoegd en uit de stapel ongedaan maken verwijderd. Wanneer een actie opnieuw wordt gedaan, wordt deze verwijderd uit de nieuwe stapel en toegevoegd aan de stapel voor ongedaan maken.

De bovenstaande afbeelding toont een voorbeeld van de stapels in actie. De opnieuw stapelen heeft twee items uit opdrachten die al ongedaan zijn gemaakt. Wanneer nieuwe opdrachten, PlaceX (0,0) en PlaceO (0,1), worden uitgevoerd, de opnieuw stapelen wordt gewist en ze worden toegevoegd aan de stapel ongedaan maken. Wanneer een PlaceO (0,1) is ongedaan gemaakt, wordt het van de bovenkant van de stapel ongedaan maken verwijderd en op de nieuwe stapel geplaatst.

Dit is hoe dat eruit ziet in de code:

openbare klasse CommandManager private Stack undos = nieuwe stapel(); private Stack redos = nieuwe stapel(); public void executeCommand (Command c) c.execute (); undos.push (c); redos.clear ();  public boolean isUndoAvailable () return! undos.empty ();  public void undo () assert (! undos.empty ()); Opdrachtopdracht = undos.pop (); command.undo (); redos.push (commando);  public boolean isRedoAvailable () return! redos.empty ();  public void redo () assert (! redos.empty ()); Opdrachtopdracht = redos.pop (); command.execute (); undos.push (commando); 

Nu heb je een spel-en-leg-ten spelmodel dat acties ongedaan kan maken tot aan het begin van het spel en ze opnieuw kan overdoen.

Als je wilt zien hoe dit allemaal in elkaar past, kun je de laatste bron downloaden, die de voltooide code uit deze zelfstudie bevat.


Conclusie

Je hebt misschien gemerkt dat de finale CommandManager je schreef zal werken voor ieder Commando implementaties. Dit betekent dat je een code kunt coderen CommandManager in uw favoriete taal, maak enkele exemplaren van de Commando interface, en hebben een volledig systeem voorbereid voor ongedaan maken / opnieuw uitvoeren. De functie Ongedaan maken kan een geweldige manier zijn om gebruikers in staat te stellen uw spel te verkennen en fouten te maken zonder zich te willen inzetten voor slechte beslissingen.

Bedankt dat je interesse hebt getoond in deze zelfstudie!

Houd rekening met het volgende als iets nader nadenken: het Commando patroon samen met de CommandManager kunt u elke statusverandering volgen tijdens de uitvoering van uw spel. Als u deze informatie opslaat, kunt u herhalingen van de uitvoering van het programma maken.