Molti giochi a turni includono un disfare pulsante per permettere ai giocatori di invertire gli errori che fanno durante il gioco. Questa funzione diventa particolarmente rilevante per lo sviluppo di giochi per dispositivi mobili in cui il tocco potrebbe avere un riconoscimento tattile maldestro. Piuttosto che affidarsi a un sistema in cui chiedi all'utente "sei sicuro di voler svolgere questo compito?" per ogni azione che intraprendono, è molto più efficiente lasciarli fare errori e avere la possibilità di invertire facilmente la loro azione. In questo tutorial, vedremo come implementare questo utilizzando il Modello di comando, usando l'esempio di un gioco tic-tac-toe.
Nota: Sebbene questo tutorial sia scritto usando Java, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi. (Non è limitato ai giochi tic-tac-toe, neanche!)
Il risultato finale di questo tutorial è un gioco tic-tac-toe che offre operazioni illimitate di annullamento e ripetizione.
Impossibile caricare l'applet? Guarda il video del gameplay su YouTube:
È anche possibile eseguire la demo sulla riga di comando utilizzando TicTacToeMain
come la classe principale da cui eseguire. Dopo aver estratto l'origine, esegui i seguenti comandi:
javac * .java java TicTacToeMain
Per questo tutorial, considererai un'implementazione di tic-tac-toe. Sebbene il gioco sia estremamente banale, i concetti forniti in questo tutorial possono essere applicati a giochi molto più complessi.
Il seguente download (che è diverso dal download della fonte finale) contiene il codice di base per un modello di gioco tic-tac-toe che fa non contenere una funzione di annullamento o ripetizione. Sarà tuo compito seguire questo tutorial e aggiungere queste funzionalità. Scarica la base TicTacToeModel.java.
Dovresti prendere nota, in particolare, dei seguenti metodi:
public void placeX (int row, int col) assert (playerXTurn); assert (spaces [row] [col] == 0); spaces [row] [col] = 1; playerXTurn = false;
public void placeO (int row, int col) assert (! playerXTurn); assert (spaces [row] [col] == 0); spaces [row] [col] = 2; playerXTurn = true;
Questi metodi sono gli unici metodi per questo gioco che cambiano lo stato della griglia di gioco. Saranno ciò che cambierai.
Se non sei uno sviluppatore Java, probabilmente sarai ancora in grado di capire il codice. È copiato qui se vuoi solo farvi riferimento:
/ ** La logica di gioco per un gioco Tic-Tac-Toe. Questo modello non ha * un'interfaccia utente associata: è solo la logica del gioco. * * Il gioco è rappresentato da un semplice array intero 3x3. Un valore di * 0 significa che lo spazio è vuoto, 1 significa che è una X, 2 significa che è un O. * * @author aarnott * * / public class TicTacToeModel // Vero se è il turno del giocatore X, falso se è il turno del giocatore booleano privato del turno O giocatoreXTurn; // L'insieme di spazi sulla griglia di gioco int privato [] [] spazi; / ** Inizializza un nuovo modello di gioco. Nel tradizionale gioco Tic-Tac-Toe *, X va per primo. * * / public TicTacToeModel () spaces = new int [3] [3]; playerXTurn = true; / ** Restituisce vero se è il turno del giocatore X. * * @return * / public boolean isPlayerXTurn () return playerXTurn; / ** Restituisce vero se è il turno del giocatore O. * * @return * / public boolean isPlayerOTurn () return! playerXTurn; / ** Posiziona una X su uno spazio specificato dai parametri row e column *. * * Precondizioni: * -> Deve essere il turno del giocatore X * -> Lo spazio deve essere vuoto * * @param row La riga per posizionare la X su * @param col La colonna per posizionare la X su * / public void placeX (int row, int col) assert (playerXTurn); assert (spaces [row] [col] == 0); spaces [row] [col] = 1; playerXTurn = false; / ** Posiziona una O in uno spazio specificato dai parametri row e column *. * * Precondizioni: * -> Deve essere il turno del giocatore O * -> Lo spazio deve essere vuoto * * @param row La riga per posizionare l'O su * @param col La colonna per posizionare l'O su * / public void placeO (int row, int col) assert (! playerXTurn); assert (spaces [row] [col] == 0); spaces [row] [col] = 2; playerXTurn = true; / ** Restituisce true se uno spazio sulla griglia è vuoto (senza Xs o Os) * * @param row * @param col * @return * / public boolean isSpaceEmpty (int row, int col) return (spaces [row ] [col] == 0); / ** Restituisce true se uno spazio sulla griglia è un X. * * @param row * @param col * @return * / public boolean isSpaceX (int row, int col) return (spaces [row] [col] == 1); / ** Restituisce true se uno spazio sulla griglia è una O. * * @param row * @param col * @return * / public boolean isSpaceO (int row, int col) return (spaces [row] [col] == 2); / ** Restituisce vero se il giocatore X ha vinto la partita. Cioè, se il giocatore * X ha completato una linea di tre X. * * @return * / public boolean hasPlayerXWon () // Controlla le righe if (spaces [0] [0] == 1 && spaces [0] [1] == 1 && spaces [0] [2] == 1 ) return true; if (spaces [1] [0] == 1 && spaces [1] [1] == 1 && spaces [1] [2] == 1) restituisce true; if (spaces [2] [0] == 1 && spaces [2] [1] == 1 && spaces [2] [2] == 1) restituisce true; // Controlla colonne se (spazi [0] [0] == 1 && spazi [1] [0] == 1 && spazi [2] [0] == 1) restituisce true; if (spaces [0] [1] == 1 && spaces [1] [1] == 1 && spaces [2] [1] == 1) restituisce true; if (spaces [0] [2] == 1 && spaces [1] [2] == 1 && spaces [2] [2] == 1) restituisce true; // Verifica diagonali if (spazi [0] [0] == 1 && spazi [1] [1] == 1 && spazi [2] [2] == 1) restituisce true; if (spaces [0] [2] == 1 && spaces [1] [1] == 1 && spaces [2] [0] == 1) restituisce true; // Altrimenti, non c'è nessun ritorno di riga falso; / ** Restituisce vero se il giocatore O ha vinto la partita. Cioè, se il giocatore * O ha completato una linea di tre Os. * * @return * / public boolean hasPlayerOWon () // Controlla le righe if (spaces [0] [0] == 2 && spaces [0] [1] == 2 && spaces [0] [2] == 2 ) return true; if (spaces [1] [0] == 2 && spaces [1] [1] == 2 && spaces [1] [2] == 2) restituisce true; if (spaces [2] [0] == 2 && spaces [2] [1] == 2 && spaces [2] [2] == 2) restituisce true; // Controlla colonne se (spazi [0] [0] == 2 && spazi [1] [0] == 2 && spazi [2] [0] == 2) restituisce true; if (spaces [0] [1] == 2 && spaces [1] [1] == 2 && spaces [2] [1] == 2) restituisce true; if (spaces [0] [2] == 2 && spaces [1] [2] == 2 && spaces [2] [2] == 2) restituisce true; // Verifica diagonali if (spazi [0] [0] == 2 && spazi [1] [1] == 2 && spazi [2] [2] == 2) restituisce true; if (spaces [0] [2] == 2 && spaces [1] [1] == 2 && spaces [2] [0] == 2) restituisce true; // Altrimenti, non c'è nessun ritorno di riga falso; / ** Restituisce vero se tutti gli spazi sono occupati o uno dei giocatori ha * vinto la partita. * * @return * / public boolean isGameOver () if (hasPlayerXWon () || hasPlayerOWon ()) restituisce true; // Controlla se tutti gli spazi sono pieni. Se uno non è il gioco non è finito per (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;
Il Comando
pattern è un modello di progettazione comunemente utilizzato con le interfacce utente per separare le azioni eseguite da pulsanti, menu o altri widget dalle definizioni di codice dell'interfaccia utente per questi oggetti. Questo concetto di separazione del codice di azione può essere utilizzato per tenere traccia di ogni cambiamento che si verifica nello stato di un gioco e puoi utilizzare queste informazioni per invertire le modifiche.
La versione più semplice di Comando
pattern è la seguente interfaccia:
comando di interfaccia pubblica public void execute ();
Qualunque l'azione intrapresa dal programma che cambia lo stato del gioco - come posizionare una X in uno spazio specifico - implementerà il Comando
interfaccia. Quando l'azione è presa, il eseguire()
il metodo è chiamato.
Ora, probabilmente hai notato che questa interfaccia non offre la possibilità di annullare le azioni; tutto ciò che fa è prendere il gioco da uno stato all'altro. Il seguente miglioramento consentirà di implementare azioni per offrire funzionalità di annullamento.
comando di interfaccia pubblica public void execute (); public void undo ();
L'obiettivo quando si implementa a Comando
sarà per avere il disfare()
metodo invertire ogni azione intrapresa dal eseguire
metodo. Di conseguenza, il eseguire()
il metodo sarà anche in grado di fornire la capacità di ripetere un'azione.
Questa è l'idea di base. Diventerà più chiaro mentre implementiamo comandi specifici per questo gioco.
Per aggiungere una funzione di annullamento, creerai un CommandManager
classe. Il CommandManager
è responsabile per il tracciamento, l'esecuzione e l'annullamento Comando
implementazioni.
(Ricorda che il Comando
l'interfaccia fornisce i metodi per apportare modifiche da uno stato di un programma a un altro e anche invertirlo.)
public class commandManager private Command lastCommand; public CommandManager () public void executeCommand (Command c) c.execute (); lastCommand = c; ...
Per eseguire a Comando
, il CommandManager
è passato a Comando
istanza e eseguirà il Comando
e quindi memorizzare l'ultimo eseguito Comando
per riferimento futuro.
Aggiunta della funzione di annullamento a CommandManager
richiede semplicemente di dirgli di annullare la più recente Comando
quello eseguito.
public boolean isUndoAvailable () return lastCommand! = null; public void undo () assert (lastCommand! = null); lastCommand.undo (); lastCommand = null;
Questo codice è tutto ciò che è necessario per avere un funzionale CommandManager
. Affinché funzioni correttamente, è necessario creare alcune implementazioni di Comando
interfaccia.
Comando
InterfacciaL'obiettivo del Comando
il modello per questo tutorial è quello di spostare qualsiasi codice che modifichi lo stato del gioco tic-tac-toe in a Comando
esempio. Vale a dire, il codice nei metodi PlaceX ()
e placeO ()
sono ciò che cambierai.
Dentro il TicTacToeModel
classe, aggiungi due nuove classi interne chiamate PlaceXCommand
e PlaceOCommand
, rispettivamente, che ciascuno implementa il Comando
interfaccia.
public class TicTacToeModel ... classe privata PlaceXCommand implements Command public void execute () ... public void undo () ... classe privata PlaceOCommand implementa Command public void execute () ... public void undo () ...
Il lavoro di a Comando
l'implementazione è di memorizzare uno stato e avere la logica per passare a uno stato nuovo risultante dall'esecuzione del Comando
o per tornare allo stato iniziale prima del Comando
è stato eseguito. Ci sono due modi semplici per raggiungere questo compito.
eseguire()
viene chiamato e imposta lo stato corrente del gioco allo stato precedente memorizzato quando disfare()
è chiamato.eseguire()
o disfare()
è chiamato.// Opzione 1: memorizzazione della classe privata degli stati precedente e successivo PlaceXCommand implementa il comando modello privato TicTacToeModel; // private int [] [] previousGridState; private boolean previousTurnState; private int [] [] nextGridState; private boolean nextTurnState; // private PlaceXCommand (modello TicTacToeModel, int row, int col) this.model = model; // previousTurnState = model.playerXTurn; // Copia l'intera griglia per entrambi gli stati previousGridState = new int [3] [3]; nextGridState = new int [3] [3]; per (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 prima opzione è un po 'dispendiosa, ma ciò non significa che sia un cattivo design. Il codice è semplice e, a meno che le informazioni di stato siano estremamente grandi, la quantità di rifiuti non sarà qualcosa di cui preoccuparsi.
Vedrai che, nel caso di questo tutorial, la seconda opzione è migliore, ma questo approccio non sarà sempre il migliore per ogni programma. Più spesso che no, tuttavia, la seconda opzione sarà la strada da percorrere.
// Opzione 2: Archivia solo le modifiche tra stati private class PlaceXCommand implementa Command private TicTacToeModel model; private int previousValue; private boolean previousTurn; fila int privata; privato int col; // private PlaceXCommand (modello TicTacToeModel, int row, int col) this.model = model; this.row = row; this.col = col; // Copia il valore precedente dalla griglia 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 seconda opzione memorizza solo le modifiche che si verificano, piuttosto che l'intero stato. Nel caso di tic-tac-toe, è più efficiente e non particolarmente complicato usare questa opzione.
Il PlaceOCommand
la classe interiore è scritta in modo simile - provate a scriverla da soli!
Per utilizzare il tuo Comando
implementazioni, PlaceXCommand
e PlaceOCommand
, sarà necessario modificare il TicTacToeModel
classe. La classe deve fare uso di a CommandManager
e deve usare Comando
istanze invece di applicare direttamente le azioni.
public class TicTacToeModel commandManager commandManager privato; // ... // public TicTacToeModel () ... // commandManager = new CommandManager (); // ... // public void placeX (int row, int col) assert (playerXTurn); assert (spaces [row] [col] == 0); commandManager.executeCommand (new PlaceXCommand (this, row, col)); // public void placeO (int row, int col) assert (! playerXTurn); assert (spaces [row] [col] == 0); commandManager.executeCommand (new PlaceOCommand (this, row, col)); // ...
Il TicTacToeModel
la classe funzionerà esattamente come prima delle modifiche, ma puoi anche esporre la funzione di annullamento. Aggiungi un disfare()
metodo per il modello e anche aggiungere un metodo di controllo canUndo
per l'interfaccia utente da utilizzare a un certo punto.
public class TicTacToeModel // ... // public booleano canUndo () return commandManager.isUndoAvailable (); // public void undo () commandManager.undo ();
Ora hai un modello di gioco tic-tac-toe completamente funzionale che supporta l'annullamento!
Con alcune piccole modifiche al CommandManager
, è possibile aggiungere il supporto per le operazioni di ripetizione e un numero illimitato di annullamenti e ripristini.
Il concetto alla base di una funzione di ripetizione è praticamente la stessa di una funzione di annullamento. Oltre a memorizzare l'ultimo Comando
eseguito, memorizzi anche l'ultimo Comando
è stato annullato. Lo conservi Comando
quando viene chiamato un annullamento e lo si cancella quando a Comando
viene eseguito.
public class commandManager comando privato 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;
Aggiungere in più annullamenti e ripristini è una questione di memorizzazione di pila di azioni annullabili e redoable. Quando viene eseguita una nuova azione, viene aggiunta allo stack di annullamento e lo stack di ripetizione viene cancellato. Quando un'azione viene annullata, viene aggiunta allo stack di ripetizione e rimossa dallo stack di annullamento. Quando un'azione viene ripristinata, viene rimossa dallo stack di ripetizione e aggiunta allo stack di annullamento.
L'immagine sopra mostra un esempio degli stack in azione. Lo stack di ripetizione contiene due elementi di comandi che sono già stati annullati. Quando nuovi comandi, PlaceX (0,0)
e PlaceO (0,1)
, vengono eseguiti, lo stack di ripetizione viene cancellato e vengono aggiunti allo stack di annullamento. Quando un PlaceO (0,1)
viene annullato, viene rimosso dalla parte superiore dello stack di annullamento e inserito nella pila di ripetizione.
Ecco come appare nel codice:
CommandManager di classe pubblica Stack privatoundos = new Stack (); Stack privato redos = new Stack (); public void executeCommand (Command c) c.execute (); undos.push (c); redos.clear (); public boolean isUndoAvailable () return! undos.empty (); public void undo () assert (! undos.empty ()); Comando command = undos.pop (); command.undo (); redos.push (comando); public boolean isRedoAvailable () return! redos.empty (); public void redo () assert (! redos.empty ()); Comando command = redos.pop (); command.execute (); undos.push (comando);
Ora hai un modello di gioco tic-tac-toe che può annullare le azioni fino all'inizio del gioco e rifarlo di nuovo.
Se vuoi vedere come tutto questo combacia, prendi il download del codice sorgente finale, che contiene il codice completato da questo tutorial.
Potresti aver notato che il finale CommandManager
per la quale hai scritto lavorerai qualunque Comando
implementazioni. Ciò significa che è possibile codificare a CommandManager
nella tua lingua preferita, crea alcune istanze di Comando
interfaccia e avere un sistema completo preparato per annullare / ripristinare. La funzione di annullamento può essere un ottimo modo per consentire agli utenti di esplorare il tuo gioco e commettere errori senza sentirti impegnato in decisioni sbagliate.
Grazie per esserti interessato a questo tutorial!
Come ulteriori spunti di riflessione, considera quanto segue: il Comando
modello insieme al CommandManager
ti permettono di tracciare ogni cambio di stato durante l'esecuzione del tuo gioco. Se si salvano queste informazioni, è possibile creare replay dell'esecuzione del programma.