Låt dina spelare ångra sina in-game-misstag med kommandotypen

Många turnbaserade spel inkluderar en ångra knappen för att låta spelare vända om misstag de gör under spel. Denna funktion blir särskilt relevant för mobilspelutveckling där kontakten kan ha klumpigt beröringsigenkänning. Snarare än att lita på ett system där du frågar användaren "är du säker på att du vill göra den här uppgiften?" För varje åtgärd som de tar är det mycket effektivare att låta dem göra misstag och ha möjlighet att enkelt vända sin handling. I denna handledning ser vi på hur man implementerar detta med hjälp av Kommando Mönster, med exempel på ett tic-tac-toe-spel.

Notera: Även om denna handledning skrivs med Java, bör du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst. (Det är inte begränsat till tic-tac-toe-spel, heller!)


Slutresultatförhandsvisning

Slutresultatet av denna handledning är ett tic-tac-toe-spel som erbjuder obegränsad ångra och omdirigera operationer.

Den här demo kräver att Java körs.

Kan inte ladda appleten? Titta på gameplay-videon på YouTube:

Du kan också köra demo på kommandoraden med TicTacToeMain som huvudklasset att utföra från. Efter att extrahera källan kör följande kommandon:

 javac * .java java TicTacToeMain

Steg 1: Skapa en grundläggande implementering av Tic-Tac-Toe

För denna handledning kommer du att överväga en implementering av tic-tac-toe. Trots att spelet är extremt trivialt kan koncepten i denna handledning gälla för mycket mer komplexa spel.

Nedladdningen (som skiljer sig från den slutliga källnedladdningen) innehåller den grundläggande koden för en tic-tac-toe-spelmodell som gör inte innehålla en ångra eller omkastningsfunktion. Det blir ditt jobb att följa denna handledning och lägga till dessa funktioner. Ladda ner basen TicTacToeModel.java.

Du bör särskilt notera följande metoder:

public void placeX (int rad, int col) assert (playerXTurn); assert (mellanslag [rad] [col] == 0); mellanslag [rad] [col] = 1; playerXTurn = false; 
public void placeO (int rad, int col) assert (! playerXTurn); assert (mellanslag [rad] [col] == 0); mellanslag [rad] [col] = 2; playerXTurn = true; 

Dessa metoder är de enda metoderna för detta spel som ändrar spelets tillstånd. De kommer att bli vad du kommer att ändra.

Om du inte är en Java-utvecklare, kommer du förmodligen fortfarande att kunna förstå koden. Den kopieras här om du bara vill hänvisa till den:

 / ** Spellogiken för ett Tic-Tac-Toe-spel. Denna modell har inte * ett associerat användargränssnitt: det är bara spellogiken. * * Spelet representeras av en enkel 3x3 heltal. Ett värde av * 0 betyder att tomt är tomt, 1 betyder att det är en X, 2 betyder att det är en O. * * @author aarnott * * / public class TicTacToeModel // True om det är X-spelarens tur, falskt om det är O-spelarens tur privat boolean playerXTurn; // Satsen av mellanslag på spelrutan privata int [] [] utrymmen; / ** Initiera en ny spelmodell. I det traditionella Tic-Tac-Toe * -spelet går X först. * * / public TicTacToeModel () spaces = new int [3] [3]; playerXTurn = true;  / ** Returnerar sant om det är X-spelarens tur. * * @return * / public boolean isPlayerXTurn () return playerXTurn;  / ** Returnerar sant om det är O-spelareens tur. * * @return * / public boolean isPlayerOTurn () return! playerXTurn;  / ** Placerar en X på ett mellanslag som anges av rad och kolumn * parametrar. * * Förutsättningar: * -> Det måste vara X-spelarens tur * -> Utrymmet måste vara tomt * * @param row Raden för att placera X på * @param col Kolumnen för att placera X på * / offentligt tomrum (int rad, int col) assert (playerXTurn); assert (mellanslag [rad] [col] == 0); mellanslag [rad] [col] = 1; playerXTurn = false;  / ** Placerar en O på ett mellanslag som anges i rad och kolumn * parametrar. * * Förutsättningar: * -> Det måste vara O-spelarens tur * -> Utrymmet måste vara tomt * * @param row Raden för att placera O på * @param col Kolumnen för att placera O på * / public void placeO (int rad, int col) assert (! playerXTurn); assert (mellanslag [rad] [col] == 0); mellanslag [rad] [col] = 2; playerXTurn = true;  / ** Returnerar sant om ett mellanslag i rutnätet är tomt (nej Xs eller Os) * * @param rad * @param col * @return * / offentliga boolean isSpaceEmpty (int rad, int col) retur ] [col] == 0);  / ** Returnerar sant om ett mellanslag i rutnätet är en X. * * @param-rad * @param col * @return * / offentlig boolean isSpaceX (int rad, int col) retur (mellanslag [rad] == 1);  / ** Returnerar sant om ett mellanslag i rutnätet är en O. * * @paramrad * @param col * @return * / offentlig boolean isSpaceO (int rad, int col) return (mellanslag [rad] == 2);  / ** Returnerar sant om X-spelaren vann spelet. Det vill säga, om * X-spelaren har slutfört en rad på tre Xs. * * @return * / public boolean hasPlayerXWon () // Kontrollera rader om (mellanslag [0] [0] == 1 && mellanslag [0] [1] == 1 && mellanslag [0] [2] == 1 ) returnera sant; om (mellanslag [1] [0] == 1 && mellanslag [1] [1] == 1 && mellanslag [1] [2] == 1) returnera sant; om (mellanslag [2] [0] == 1 && mellanslag [2] [1] == 1 && mellanslag [2] [2] == 1) returnera sant; // Kolla kolumner om (mellanslag [0] [0] == 1 && mellanslag [1] [0] == 1 && mellanslag [2] [0] == 1) returnera true; om (mellanslag [0] [1] == 1 && mellanslag [1] [1] == 1 && mellanslag [2] [1] == 1) returnera sant; om (mellanslag [0] [2] == 1 && mellanslag [1] [2] == 1 && mellanslag [2] [2] == 1) returnera true; // Kontrollera diagonaler om (mellanslag [0] [0] == 1 && mellanslag [1] [1] == 1 && mellanslag [2] [2] == 1) returnera true; om (mellanslag [0] [2] == 1 && mellanslag [1] [1] == 1 && mellanslag [2] [0] == 1) returnera true; // Annars finns det ingen linje retur falsk;  / ** Returnerar sant om O-spelaren vann spelet. Det vill säga, om * O-spelaren har gjort en linje med tre Os. * * @return * / public boolean hasPlayerOWon () // Kontrollera rader om (mellanslag [0] [0] == 2 && mellanslag [0] [1] == 2 && mellanslag [0] [2] == 2 ) returnera sant; om (mellanslag [1] [0] == 2 && mellanslag [1] [1] == 2 && mellanslag [1] [2] == 2) returnera sant; om (mellanslag [2] [0] == 2 && mellanslag [2] [1] == 2 && mellanslag [2] [2] == 2) returnera sant; // Kolla kolumner om (mellanslag [0] [0] == 2 && mellanslag [1] [0] == 2 && mellanslag [2] [0] == 2) returnera true; om (mellanslag [0] [1] == 2 && mellanslag [1] [1] == 2 && mellanslag [2] [1] == 2) returnera true; om (mellanslag [0] [2] == 2 && mellanslag [1] [2] == 2 && mellanslag [2] [2] == 2) returnera sant; // Kontrollera diagonaler om (mellanslag [0] [0] == 2 && mellanslag [1] [1] == 2 && mellanslag [2] [2] == 2) returnera true; om (mellanslag [0] [2] == 2 && mellanslag [1] [1] == 2 && mellanslag [2] [0] == 2) returnera true; // Annars finns det ingen linje retur falsk;  / ** Returnerar sant om alla blanketter är fyllda eller en av spelarna har * vunnit spelet. * * @return * / public boolean isGameOver () om (hasPlayerXWon () || hasPlayerOWon ()) returnera true; // Kontrollera om alla mellanslag är fyllda. Om man inte är spelet är inte över för (int rad = 0; rad < 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;  

Steg 2: Förstå kommandomönstret

De Kommando mönstret är ett mönster som brukar användas med användargränssnitt för att separera de åtgärder som utförs av knappar, menyer eller andra widgets från användargränssnittskoddefinitionerna för dessa objekt. Detta begrepp med åtskillnad av åtgärdskod kan användas för att spåra varje ändring som händer med läget i ett spel, och du kan använda denna information för att vända om ändringarna.

Den mest grundläggande versionen av Kommando mönstret är följande gränssnitt:

offentligt gränssnitt Kommando public void execute (); 

Några Åtgärd som tas av programmet som ändrar spelets tillstånd - till exempel att placera en X i ett visst utrymme - kommer att genomföra Kommando gränssnitt. När åtgärden tas, Kör() Metoden heter.

Nu märkte du sannolikt att det här gränssnittet inte ger möjlighet att ångra handlingar. allt det gör är att ta spelet från ett tillstånd till ett annat. Följande förbättringar möjliggör genomförandeåtgärder för att erbjuda ångra kapacitet.

offentligt gränssnitt Kommando public void execute (); offentligt ogiltigt ångra (); 

Målet när man genomför en Kommando kommer att vara att ha ångra() metod omvänd varje åtgärd som tas av Kör metod. Som en följd av detta Kör() Metoden kommer också att kunna ge möjlighet att göra om en åtgärd.

Det är den grundläggande idén. Det blir tydligare när vi implementerar specifika kommandon för det här spelet.


Steg 3: Skapa en Command Manager

För att lägga till en ångrafunktion skapar du en Command klass. De Command ansvarar för att spåra, exekvera och ångra Kommando implementeringar.

(Minns att Kommando gränssnittet tillhandahåller metoderna för att göra ändringar från ett tillstånd av ett program till ett annat och även omvända det.)

Kommunalbefolkningens offentliga klass privata kommandot sista kommandot; public CommandManager ()  public void executeCommand (Command c) c.execute (); sistaCommand = c;  ...

Att utföra en Kommando, de Command passeras a Kommando exempel, och det kommer att exekvera Kommando och lagra sedan den senast utförda Kommando för senare referens.

Lägga till funktionen Ångra till Command helt enkelt kräver att man talar om att ångra det senaste Kommando som exekveras.

offentliga booleanska isUndoAvailable () return lastCommand! = null;  public void ångra () assert (lastCommand! = null); lastCommand.undo (); lastCommand = null; 

Denna kod är allt som krävs för att ha en funktionell Command. För att det ska fungera korrekt måste du skapa några implementeringar av Kommando gränssnitt.


Steg 4: Skapa implementeringar av Kommando Gränssnitt

Målet för Kommando mönstret för denna handledning är att flytta någon kod som ändrar tillståndet för tic-tac-toe-spelet i en Kommando exempel. Namnlösa: Koden i metoderna placeX () och placeO () är vad du kommer att ändra.

Inuti TicTacToeModel klass, lägg till två nya inre klasser som heter PlaceXCommand och PlaceOCommand, respektive, vilka varje implementerar Kommando gränssnitt.

public class TicTacToeModel ... privat klass PlaceXCommand implementerar Command public void execute () ... offentligt ogiltigt ångra () ... privatklass PlaceCommand implementerar Command public void execute () ... offentligt ogiltigt ångra () ... 

Arbetet med a Kommando genomförandet är att lagra en stat och ha logik för att antingen övergå till ett nytt tillstånd som härrör från genomförandet av Kommando eller att övergå tillbaka till det ursprungliga tillståndet före Kommando blev avrättad. Det finns två enkla sätt att uppnå denna uppgift.

  1. Spara hela tidigare tillstånd och nästa status. Ställ in spelets nuvarande tillstånd till nästa tillstånd när Kör() kallas och ställer spelets nuvarande tillstånd till det lagrade tidigare tillståndet när ångra() kallas.
  2. Spara endast informationen som ändras mellan stater. Ändra endast den här lagrade informationen när Kör() eller ångra() kallas.
// Alternativ 1: Lagring av föregående och nästa tillstånd Privat klass PlaceXCommand implementerar kommandot privat TicTacToeModel-modell; // private int [] [] previousGridState; privata booleanska tidigareTurnState; privat int [] [] nextGridState; privat booleansk nextTurnState; // privat PlaceXCommand (TicTacToeModel modell, int rad, int col) this.model = model; // previousTurnState = model.playerXTurn; // Kopiera hela rutnätet för båda staterna previousGridState = new int [3] [3]; nextGridState = new int [3] [3]; för (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;  

Det första alternativet är lite slöseri, men det betyder inte att det är dålig design. Koden är enkel och om inte statsinformationen är extremt stor kommer mängden avfall inte att vara något att oroa sig för.

Du kommer att se att när det gäller denna handledning är det andra alternativet bättre, men det här tillvägagångssättet kommer inte alltid att vara det bästa för varje program. Oftast är dock det andra alternativet vägen att gå.

// Alternativ 2: Förvarar endast ändringarna mellan staterna privatklass PlaceXCommand implementerar kommandot privat TicTacToeModel-modell; privat int tidigareValue; privat booleansk tidigare tur; privat int rad; privat int col; // privat PlaceXCommand (TicTacToeModel modell, int rad, int col) this.model = model; this.row = row; this.col = col; // Kopiera föregående värde från rutnätet this.previousValue = model.spaces [row] [col]; this.previousTurn = model.playerXTurn;  // public void execute () model.spaces [rad] [col] = 1; model.playerXTurn = false;  // public void undo () model.spaces [rad] [col] = previousValue; model.playerXTurn = previousTurn; 

Det andra alternativet lagrar bara de förändringar som händer, snarare än hela tillståndet. När det gäller tic-tac-toe, är det mer effektivt och inte särskilt mer komplext att använda detta alternativ.

De PlaceOCommand inre klassen är skriven på ett liknande sätt - ta en gå på att skriva det själv!


Steg 5: Sätt allt tillsammans

För att kunna utnyttja din Kommando implementeringar, PlaceXCommand och PlaceOCommand, du måste ändra TicTacToeModel klass. Klassen måste använda sig av a Command och det måste använda Kommando instanser istället för att tillämpa åtgärder direkt.

offentlig klass TicTacToeModel privat CommandManager commandManager; // ... // public TicTacToeModel () ... // commandManager = ny CommandManager ();  // ... // public void placeX (int rad, int col) assert (playerXTurn); assert (mellanslag [rad] [col] == 0); commandManager.executeCommand (ny PlaceXCommand (detta, rad, col));  // public void placeO (int rad, int col) assert (! playerXTurn); assert (mellanslag [rad] [col] == 0); commandManager.executeCommand (nytt PlaceOCommand (detta, rad, col));  // //

De TicTacToeModel klassen kommer att fungera exakt som före dina ändringar nu, men du kan också avslöja funktionen Ångra. Lägg till en ångra() metod till modellen och även lägga till en kontrollmetod canUndo för användargränssnittet att använda vid någon tidpunkt.

offentlig klass TicTacToeModel // ... // offentliga boolean canUndo () return commandManager.isUndoAvailable ();  // public void undo () commandManager.undo (); 

Du har nu en helt funktionell tic-tac-toe-spelmodell som stöder ångra!


Steg 6: Ta det vidare

Med några små modifieringar till Command, Du kan lägga till support för redo-funktioner samt ett obegränsat antal undos och redos.

Konceptet bakom en redo-funktion är ungefär samma som en ångrafunktion. Förutom att lagra den sista Kommando exekveras, lagrar du också den sista Kommando det var ångrat. Du lagrar det Kommando när en ångra heter och rensas när a Kommando exekveras.

public class CommandManager privat kommando sistCommandUndone; ... public void executeCommand (Command c) c.execute (); sistaCommand = c; lastCommandUndone = null;  public void ångra () assert (lastCommand! = null); lastCommand.undo (); lastCommandUndone = lastCommand; lastCommand = null;  offentliga booleanska isRedoAvailable () return lastCommandUndone! = null;  public void redo () assert (lastCommandUndone! = null); lastCommandUndone.execute (); lastCommand = lastCommandUndone; lastCommandUndone = null; 

Lägga till i flera undos och redos handlar om att lagra a stack av oåterkalleliga och redoåtbara åtgärder. När en ny åtgärd exekveras läggs den till i ångra stapeln och återställningsstapeln raderas. När en åtgärd är borttagen läggs den till redo-stapeln och tas bort från ångerstapeln. När en åtgärd är omformad, tas den bort från redo-stapeln och läggs till till ångra stapeln.

Ovanstående bild visar ett exempel på staplarna i åtgärd. Återställningsstacken har två objekt från kommandon som redan har blivit omöjliga. När nya kommandon, PlaceX (0,0) och PlaceO (0,1), exekveras, återställs stapeln raderas och de läggs till i ångra stapeln. När en PlaceO (0,1) är borttagen, det tas bort från toppen av ångerstacken och placeras på omställningsstacken.

Så här ser det ut i kod:

offentlig klass CommandManager privatstack undos = ny stapel(); privat stack redos = ny stapel(); public void executeCommand (Command c) c.execute (); undos.push (c); redos.clear ();  offentliga boolean ärUndoAvailable () return! undos.empty ();  public void ångra () assert (! undos.empty ()); Kommandokommando = undos.pop (); command.undo (); redos.push (kommando);  offentliga booleanska isRedoAvailable () return! redos.empty ();  public void redo () assert (! redos.empty ()); Kommandokommando = redos.pop (); command.execute (); undos.push (kommando); 

Nu har du en tic-tac-toe spelmodell som kan ångra handlingar hela vägen tillbaka till början av spelet och redo dem igen.

Om du vill se hur allt detta passar ihop, ta tag i den slutliga källnedladdningen, som innehåller den färdiga koden från den här handledningen.


Slutsats

Du kanske har märkt att finalen Command du skrev kommer att fungera för några Kommando implementeringar. Det betyder att du kan koda upp en Command i ditt favorit språk, skapa några instanser av Kommando gränssnitt, och ha ett komplett system förberett för att ångra / återställa. Ångrafunktionen kan vara ett bra sätt att låta användare utforska ditt spel och göra misstag utan att känna sig skyldig till dåliga beslut.

Tack för att du är intresserad av denna handledning!

Som en del ytterligare tankar för att tänka, överväga följande: Kommando mönster tillsammans med Command tillåter dig att spåra varje statlig förändring under genomförandet av ditt spel. Om du sparar den här informationen kan du skapa repliker av programmets utförande.