La spillerne dine angre sine feil i spillet med kommandomodellen

Mange turnbaserte spill inkluderer en angre knappen for å la spillerne reversere feilene de gjør under spillet. Denne funksjonen blir spesielt relevant for mobil spillutvikling hvor kontakten kan ha klumpet berøringskjenning. Snarere enn å stole på et system der du spør brukeren "er du sikker på at du vil gjøre denne oppgaven?" For hver handling de tar, er det mye mer effektivt å la dem gjøre feil og få muligheten til å enkelt reversere sin handling. I denne opplæringen ser vi på hvordan du implementerer dette ved hjelp av Kommando Mønster, bruker eksempelet på et tic-tac-toe spill.

Merk: Selv om denne opplæringen er skrevet ved hjelp av Java, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø. (Det er ikke begrenset til tic-tac-toe-spill, heller!)


Endelig resultatforhåndsvisning

Det endelige resultatet av denne opplæringen er et tic-tac-toe-spill som tilbyr ubegrenset angre og gjenta operasjoner.

Denne demoen krever at Java kjører.

Kan ikke laste appleten? Se spillvideoen på YouTube:

Du kan også kjøre demoen på kommandolinjen ved hjelp av TicTacToeMain som hovedklassen å utføre fra. Etter å ha hentet kilden, kjør følgende kommandoer:

 javac * .java java TicTacToeMain

Trinn 1: Lag en grunnleggende implementering av Tic-Tac-Toe

For denne opplæringen skal du vurdere en implementering av tic-tac-toe. Selv om spillet er ekstremt trivielt, kan konseptene i denne opplæringen gjelde for mye mer komplekse spill.

Følgende nedlastning (som er forskjellig fra den endelige kildedownloaden) inneholder grunnleggende kode for en tic-tac-toe spillmodell som gjør ikke inneholde en angre eller gjenta funksjon. Det blir din jobb å følge denne opplæringen og legge til disse funksjonene. Last ned basen TicTacToeModel.java.

Du bør spesielt merke seg følgende metoder:

offentlig tomrom x (int rad, int kol) assert (playerXTurn); assert (mellomrom [rad] [col] == 0); mellomrom [rad] [col] = 1; playerXTurn = false; 
offentlig tomrom sted (int rad, int kol) assert (! playerXTurn); assert (mellomrom [rad] [col] == 0); mellomrom [rad] [col] = 2; playerXTurn = true; 

Disse metodene er de eneste metodene for dette spillet som endrer tilstanden til spillruten. De vil bli det du vil forandre.

Hvis du ikke er en Java-utvikler, vil du nok fortsatt kunne forstå koden. Det er kopiert her hvis du bare vil referere til det:

 / ** Spilllogikken for et Tic-Tac-Toe-spill. Denne modellen har ikke * et tilhørende brukergrensesnitt: det er bare spilllogikken. * * Spillet er representert av et enkelt 3x3 heltall array. En verdi på * 0 betyr at rommet er tomt, 1 betyr at det er en X, 2 betyr at det er en O. * * @author aarnott * * / public class TicTacToeModel // True hvis det er X-spilleren snu, falsk hvis det er O-spilleren sin private boolske spillerXTurn; // Settet med mellomrom på spillruten private int [] [] mellomrom; / ** Initialiser en ny spillmodell. I det tradisjonelle Tic-Tac-Toe * -spillet går X først. * * / public TicTacToeModel () spaces = new int [3] [3]; playerXTurn = true;  / ** Returnerer sant hvis det er X-spilleren snu. * * @return * / offentlig boolean isPlayerXTurn () return playerXTurn;  / ** Returnerer sant hvis det er O-spillerens sving. * * @return * / offentlig boolean isPlayerOTurn () return! playerXTurn;  / ** Plasser en X på et mellomrom angitt av rad og kolonne * parametere. * * Forutsetninger: * -> Det må være X-spillerens sving * -> Plassen må være tom * * @param rad Rækken for å plassere X på * @param kol Kolonnen for å plassere X på * / offentlig tomrom (int rad, int col) assert (playerXTurn); assert (mellomrom [rad] [col] == 0); mellomrom [rad] [col] = 1; playerXTurn = false;  / ** Plasser en O på et mellomrom angitt av rad og kolonne * parametere. * * Forutsetninger: * -> Det må være O-spillerens sving * -> Plassen må være tom * * @param rad Rækken for å plassere O på * @param col Kolonnen for å plassere O på * / offentlig tomrom (int rad, int col) assert (! playerXTurn); assert (mellomrom [rad] [col] == 0); mellomrom [rad] [col] = 2; playerXTurn = true;  / ** Returnerer sant hvis et mellomrom på rutenettet er tomt (nei Xs eller Os) * * @param rad * @param col * @return * / offentlig boolean isSpaceEmpty (int rad, int col) return ] [col] == 0);  / ** Returnerer sant hvis en plass på rutenettet er en X. * * @param rad * @param col * @return * / offentlig boolean isSpaceX (int rad, int col) retur (mellomrom [rad] == 1);  / ** Returnerer sant hvis en plass på rutenettet er en O. * * @param rad * @param col * @return * / offentlig boolean isSpaceO (int rad, int col) retur (mellomrom [rad] == 2);  / ** Returnerer sant hvis X-spilleren vant spillet. Det vil si at hvis * X-spilleren har fullført en linje på tre Xs. * * @return * / offentlig booleansk harPlayerXWon () // Kontroller rader hvis (mellomrom [0] [0] == 1 && mellomrom [0] [1] == 1 && mellomrom [0] [2] == 1 ) returnere sant; hvis (mellomrom [1] [0] == 1 && mellomrom [1] [1] == 1 && mellomrom [1] [2] == 1) returner sann; hvis (mellomrom [2] [0] == 1 && mellomrom [2] [1] == 1 && mellomrom [2] [2] == 1) returner sann; // Sjekk kolonner hvis (mellomrom [0] [0] == 1 && mellomrom [1] [0] == 1 && mellomrom [2] [0] == 1) returner sann; hvis (mellomrom [0] [1] == 1 && mellomrom [1] [1] == 1 && mellomrom [2] [1] == 1) returner sann; hvis (mellomrom [0] [2] == 1 && mellomrom [1] [2] == 1 && mellomrom [2] [2] == 1) returner sann; // Kontroller diagonaler hvis (mellomrom [0] [0] == 1 && mellomrom [1] [1] == 1 && mellomrom [2] [2] == 1) returner sann; hvis (mellomrom [0] [2] == 1 && mellomrom [1] [1] == 1 && mellomrom [2] [0] == 1) returner sann; // Ellers er det ingen returlinje på linjen.  / ** Returnerer sant hvis O-spilleren vant spillet. Det vil si at hvis * O-spilleren har fullført en linje på tre Os. * * @return * / offentlig boolsk harPlayerOWon () // Kontroller rader hvis (mellomrom [0] [0] == 2 && mellomrom [0] [1] == 2 && mellomrom [0] [2] == 2 ) returnere sant; hvis (mellomrom [1] [0] == 2 && mellomrom [1] [1] == 2 && mellomrom [1] [2] == 2) returner sann; hvis (mellomrom [2] [0] == 2 && mellomrom [2] [1] == 2 && mellomrom [2] [2] == 2) returner sant; // Sjekk kolonner hvis (mellomrom [0] [0] == 2 && mellomrom [1] [0] == 2 && mellomrom [2] [0] == 2) returner sann; hvis (mellomrom [0] [1] == 2 && mellomrom [1] [1] == 2 && mellomrom [2] [1] == 2) returner sann; hvis (mellomrom [0] [2] == 2 && mellomrom [1] [2] == 2 && mellomrom [2] [2] == 2) returner sann; // Kontroller diagonaler hvis (mellomrom [0] [0] == 2 && mellomrom [1] [1] == 2 && mellomrom [2] [2] == 2) returner sann; hvis (mellomrom [0] [2] == 2 && mellomrom [1] [1] == 2 && mellomrom [2] [0] == 2) returner sann; // Ellers er det ingen returlinje på linjen.  / ** Returnerer sant hvis alle mellomrom er fylt eller en av spillerne har * vant spillet. * * @return * / offentlig boolean isGameOver () hvis (harPlayerXWon () || harPlayerOWon ()) returnere true; // Sjekk om alle mellomrom er fylt. Hvis man ikke er spillet, er ikke over for (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;  

Trinn 2: Forstå kommandomodelet

De Kommando mønster er et mønster som vanligvis brukes med brukergrensesnitt for å skille mellom handlinger utført av knapper, menyer eller andre widgets fra definisjonene for brukergrensesnittkoden for disse objektene. Dette konseptet med separerende handlingskode kan brukes til å spore alle endringer som skjer med tilstanden til et spill, og du kan bruke denne informasjonen til å reversere endringene.

Den mest grunnleggende versjonen av Kommando mønster er følgende grensesnitt:

offentlige grensesnitt kommando public void execute (); 

Noen handling som tas av programmet som endrer tilstanden til spillet - for eksempel å plassere en X i en bestemt plass - vil implementere Kommando grensesnitt. Når handlingen er tatt, vil henrette() Metoden kalles.

Nå har du sannsynligvis lagt merke til at dette grensesnittet ikke gir mulighet til å angre handlinger. alt det gjør er å ta spillet fra en stat til en annen. Følgende forbedringer vil tillate implementeringshandlinger å tilby angre evnen.

offentlige grensesnitt kommando public void execute (); offentlig ugyldig angre (); 

Målet når man implementerer en Kommando vil være å ha angre () metode reversere alle handlinger tatt av henrette metode. Som en følge av dette henrette() Metoden vil også kunne gi muligheten til å gjenta en handling.

Det er den grunnleggende ideen. Det blir tydeligere da vi implementerer bestemte kommandoer for dette spillet.


Trinn 3: Opprett en kommandolinje

For å legge til en angrepsfunksjon, vil du opprette en Command klasse. De Command er ansvarlig for å spore, utføre og angre Kommando implementeringer.

(Husk at Kommando grensesnitt gir metoder for å gjøre endringer fra en tilstand av et program til et annet og også reversere det.)

offentlig klasse CommandManager private Command lastCommand; offentlig CommandManager ()  public void executeCommand (Command c) c.execute (); siste kommando = c;  ...

For å utføre en Kommando, de Command er bestått a Kommando eksempel, og det vil utføre Kommando og lagre deretter den nylig utførte Kommando for senere referanse.

Legge til å angre funksjonen til Command krever bare å fortelle det å angre det siste Kommando som henrettet.

offentlig boolean erUndoAvailable () return lastCommand! = null;  offentlig ugyldig angre () assert (lastCommand! = null); lastCommand.undo (); lastCommand = null; 

Denne koden er alt som kreves for å være funksjonell Command. For at det skal fungere ordentlig, må du opprette noen implementeringer av Kommando grensesnitt.


Trinn 4: Opprett implementeringer av Kommando Interface

Målet med Kommando mønster for denne opplæringen er å flytte noen kode som endrer tilstanden til tic-tac-toe-spillet i en Kommando forekomst. Nemlig koden i metodene placeX () og placeO () er det du vil endre.

Inne i TicTacToeModel klasse, legg til to nye indre klasser kalt PlaceXCommand og PlaceOCommand, henholdsvis, som hver implementerer Kommando grensesnitt.

offentlig klasse TicTacToeModel ... privat klasse PlaceXCommand implementerer Command public void execute () ... offentlig ugyldig angre () ... privat klasse PlaceCommand implementerer Command public void execute () ... offentlig ugyldig ugyldig () ... 

Jobben av a Kommando implementering er å lagre en stat og ha logikk til enten overgang til en ny stat som følge av utførelsen av Kommando eller å overgå tilbake til opprinnelig tilstand før Kommando ble henrettet. Det er to enkle måter å oppnå denne oppgaven på.

  1. Lagre hele forrige tilstand og neste tilstand. Angi spillets nåværende tilstand til neste tilstand når henrette() kalles og angir spillets nåværende tilstand til den lagrede tidligere tilstanden når angre () er kalt.
  2. Bare lagre informasjonen som endres mellom tilstandene. Endre kun denne lagrede informasjonen når henrette() eller angre () er kalt.
// Alternativ 1: Lagre forrige og neste tilstander privat klasse PlaceXCommand implementerer kommando privat TicTacToeModel-modell; // private int [] [] previousGridState; privat boolsk tidligereTurnState; privat int [] [] nextGridState; privat booleansk nextTurnState; // privat PlaceXCommand (TicTacToeModel modell, int rad, int col) this.model = model; // previousTurnState = model.playerXTurn; // Kopier hele rutenettet for begge statene previousGridState = new int [3] [3]; nextGridState = new int [3] [3]; for (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ørste alternativet er litt sløsing, men det betyr ikke at det er dårlig design. Koden er enkel og med mindre statlig informasjon er ekstremt stor, vil mengden avfall ikke være noe å bekymre seg for.

Du vil se at i tilfelle denne opplæringen er det andre alternativet bedre, men denne tilnærmingen vil ikke alltid være best for hvert program. Oftere enn ikke, det andre alternativet vil imidlertid være veien å gå.

// Alternativ 2: Lagrer bare endringene mellom tilstandene privat klasse PlaceXCommand implementerer kommandoen private TicTacToeModel-modellen; privat int tidligereValue; privat boolsk forrige tur; privat int rad; privat int col; // privat PlaceXCommand (TicTacToeModel modell, int rad, int col) this.model = model; this.row = rad; this.col = col; // Kopier den forrige verdien fra rutenettet this.previousValue = model.spaces [rad] [col]; this.previousTurn = model.playerXTurn;  // public void execute () model.spaces [rad] [col] = 1; model.playerXTurn = false;  // offentlig ugyldig angre () model.spaces [rad] [col] = previousValue; model.playerXTurn = previousTurn; 

Det andre alternativet lagrer bare endringene som skjer, i stedet for hele staten. Når det gjelder tic-tac-toe, er det mer effektivt og ikke spesielt mer komplekst å bruke dette alternativet.

De PlaceOCommand indre klasse er skrevet på en lignende måte - ta en tur til å skrive det selv!


Trinn 5: Sett alt sammen

For å gjøre bruk av din Kommando implementeringer, PlaceXCommand og PlaceOCommand, du må endre TicTacToeModel klasse. Klassen må gjøre bruk av a Command og den må bruke Kommando forekomster i stedet for å søke handlinger direkte.

offentlig klasse TicTacToeModel private CommandManager commandManager; // ... // offentlige TicTacToeModel () ... // commandManager = ny CommandManager ();  // ... // offentlig tomt stedX (int rad, int kol) assert (playerXTurn); assert (mellomrom [rad] [col] == 0); commandManager.executeCommand (nytt stedXCommand (dette, rad, kol));  // offentlig tomrom sted (int rad, int kol) assert (! playerXTurn); assert (mellomrom [rad] [col] == 0); commandManager.executeCommand (nytt stedOCommand (dette, rad, kol));  // //

De TicTacToeModel klassen vil fungere akkurat som det gjorde før endringene dine nå, men du kan også avsløre å angre funksjonen. Legg til en angre () metode til modellen og også legge til en sjekk metode canUndo for brukergrensesnittet å bruke på et tidspunkt.

offentlig klasse TicTacToeModel // ... // offentlige boolean canUndo () return commandManager.isUndoAvailable ();  // offentlig ugyldig angre () commandManager.undo (); 

Du har nå en helt funksjonell tic-tac-toe spillmodell som støtter angre!


Trinn 6: Ta det videre

Med noen få små modifikasjoner til Command, Du kan legge til støtte for gjenta operasjoner, samt et ubegrenset antall undos og redos.

Konseptet bak en omformingsfunksjon er stort sett det samme som en angrepsfunksjon. I tillegg til å lagre sist Kommando utført, lagrer du også sist Kommando det var utelatt. Du lagrer det Kommando når et angrepet kalles og slettes når a Kommando er utført.

offentlig klasse CommandManager Private Command lastCommandUndone; ... public void executeCommand (Command c) c.execute (); siste kommando = c; lastCommandUndone = null;  offentlig ugyldig angre () assert (lastCommand! = null); lastCommand.undo (); lastCommandUndone = lastCommand; lastCommand = null;  offentlige boolean isRedoAvailable () return lastCommandUndone! = null;  offentlig ugyldig gjeld () assert (lastCommandUndone! = null); lastCommandUndone.execute (); lastCommand = lastCommandUndone; lastCommandUndone = null; 

Å legge til i flere undos og redos er et spørsmål om lagring av en stable av uopprettelige og omsettelige handlinger. Når en ny handling utføres, legges den til å angre stabelen, og gjenopprettingsstakken slettes. Når en handling er slettet, legges den til gjenta stakken og fjernes fra stryk av stakkord. Når en handling er omgjort, blir den fjernet fra gjenta-stakken og lagt til for å angre stakken.

Ovennevnte bilde viser et eksempel på stablene i aksjon. Gjenopprettingsstakken har to elementer fra kommandoer som allerede er utelatt. Når nye kommandoer, PlaceX (0,0) og PlaceO (0,1), blir utført, gjenta stakken blir ryddet og de legges til å angre stakken. Når en PlaceO (0,1) er utelatt, blir den fjernet fra toppen av å angre stakken og plassert på gjenta stakken.

Slik ser det ut i kode:

offentlig klasse CommandManager private Stack undos = ny Stack(); privat stabling redos = ny Stack(); Offentlig ugyldig utfør kommandoen (Kommando c) c.execute (); undos.push (c); redos.clear ();  offentlige boolean erUndoAvailable () return! undos.empty ();  offentlig ugyldig angre () assert (! undos.empty ()); Kommando kommando = undos.pop (); command.undo (); redos.push (kommando);  offentlig boolsk isRedoAvailable () return! redos.empty ();  offentlig ugyldig omgang () assert (! redos.empty ()); Kommando kommando = redos.pop (); command.execute (); undos.push (kommando); 

Nå har du en tic-tac-toe spillmodell som kan angre handlinger helt tilbake til begynnelsen av spillet og gjenta dem igjen.

Hvis du vil se hvordan alt dette passer sammen, ta den endelige kildedownloaden, som inneholder den fullførte koden fra denne opplæringen.


Konklusjon

Du har kanskje lagt merke til at den endelige Command du skrev vil jobbe for noen Kommando implementeringer. Dette betyr at du kan kode opp en Command i favorittspråket ditt, opprett noen forekomster av Kommando grensesnitt, og har et fullt system forberedt for å angre / gjenta. Å angre funksjonen kan være en fin måte å tillate brukere å utforske spillet ditt og gjøre feil uten å være forpliktet til dårlige beslutninger.

Takk for at du har interesse for denne opplæringen!

Som noen ytterligere mat for tanker, vurder følgende: Kommando mønster sammen med Command lar deg spore alle statlige endringer under utførelsen av spillet ditt. Hvis du lagrer denne informasjonen, kan du opprette replays av utførelsen av programmet.