/** * 翻译力求准确,信达雅谈不上,如果有不准确的欢迎大家指出,以便修 * @see http://gamedev.tutsplus.com/tutorials/implementation/let-your-players-undo-their-in-game-mistakes-with-the-command-pattern/ */
许多回合制游戏包含一个撤销按钮让玩家在游戏过程中能返回之前的错误。此功能尤其适合手游开发,因为触屏有时候会产生失真的触摸识别。相比每次操作之前都问“确定执行这一步?”而言,如果在游戏中犯错后能够撤销到上一步操作之前的状态那可有效率的多。在本教程中,我们将学习如何使用命令模式来实现这种效果。我们以井字棋游戏为例。
注意:尽管该教程是用java写的,但你几乎能够在任何一种游戏开发环境中使用相同的技术和观念(当然也不只限于井字棋)
最终结果预览
本教程的最终结果是一个提供无限次撤销和重做功能的井字棋游戏,代码请到原网站下载
不能加载java applet?那就看youtube上面的视频吧(天朝老百姓就甭想了)
你可以将TicTacToeMain作为主类在命令行中执行该demo。把源码解压缩以后执行以下命令:
javac *.java java TicTacToeMain
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; }
/** The game logic for a Tic-Tac-Toe game. This model does not have * an associated User Interface: it is just the game logic. * * The game is represented by a simple 3x3 integer array. A value of * 0 means the space is empty, 1 means it is an X, 2 means it is an O. * * @author aarnott * */ public class TicTacToeModel { //True if it is the X player’s turn, false if it is the O player’s turn private boolean playerXTurn; //The set of spaces on the game grid private int[][] spaces; /** Initialize a new game model. In the traditional Tic-Tac-Toe * game, X goes first. * */ public TicTacToeModel() { spaces = new int[3][3]; playerXTurn = true; } /** Returns true if it is the X player's turn. * * @return */ public boolean isPlayerXTurn() { return playerXTurn; } /** Returns true if it is the O player's turn. * * @return */ public boolean isPlayerOTurn() { return !playerXTurn; } /** Places an X on a space specified by the row and column * parameters. * * Preconditions: * -> It must be the X player's turn * -> The space must be empty * * @param row The row to place the X on * @param col The column to place the X on */ public void placeX(int row, int col) { assert(playerXTurn); assert(spaces[row][col] == 0); spaces[row][col] = 1; playerXTurn = false; } /** Places an O on a space specified by the row and column * parameters. * * Preconditions: * -> It must be the O player's turn * -> The space must be empty * * @param row The row to place the O on * @param col The column to place the O on */ public void placeO(int row, int col) { assert(!playerXTurn); assert(spaces[row][col] == 0); spaces[row][col] = 2; playerXTurn = true; } /** Returns true if a space on the grid is empty (no Xs or Os) * * @param row * @param col * @return */ public boolean isSpaceEmpty(int row, int col) { return (spaces[row][col] == 0); } /** Returns true if a space on the grid is an X. * * @param row * @param col * @return */ public boolean isSpaceX(int row, int col) { return (spaces[row][col] == 1); } /** Returns true if a space on the grid is an O. * * @param row * @param col * @return */ public boolean isSpaceO(int row, int col) { return (spaces[row][col] == 2); } /** Returns true if the X player won the game. That is, if the * X player has completed a line of three Xs. * * @return */ public boolean hasPlayerXWon() { //Check rows 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) return true; if(spaces[2][0] == 1 && spaces[2][1] == 1 && spaces[2][2] == 1) return true; //Check columns if(spaces[0][0] == 1 && spaces[1][0] == 1 && spaces[2][0] == 1) return true; if(spaces[0][1] == 1 && spaces[1][1] == 1 && spaces[2][1] == 1) return true; if(spaces[0][2] == 1 && spaces[1][2] == 1 && spaces[2][2] == 1) return true; //Check diagonals if(spaces[0][0] == 1 && spaces[1][1] == 1 && spaces[2][2] == 1) return true; if(spaces[0][2] == 1 && spaces[1][1] == 1 && spaces[2][0] == 1) return true; //Otherwise, there is no line return false; } /** Returns true if the O player won the game. That is, if the * O player has completed a line of three Os. * * @return */ public boolean hasPlayerOWon() { //Check rows 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) return true; if(spaces[2][0] == 2 && spaces[2][1] == 2 && spaces[2][2] == 2) return true; //Check columns if(spaces[0][0] == 2 && spaces[1][0] == 2 && spaces[2][0] == 2) return true; if(spaces[0][1] == 2 && spaces[1][1] == 2 && spaces[2][1] == 2) return true; if(spaces[0][2] == 2 && spaces[1][2] == 2 && spaces[2][2] == 2) return true; //Check diagonals if(spaces[0][0] == 2 && spaces[1][1] == 2 && spaces[2][2] == 2) return true; if(spaces[0][2] == 2 && spaces[1][1] == 2 && spaces[2][0] == 2) return true; //Otherwise, there is no line return false; } /** Returns true if all the spaces are filled or one of the players has * won the game. * * @return */ public boolean isGameOver() { if(hasPlayerXWon() || hasPlayerOWon()) return true; //Check if all the spaces are filled. If one isn’t the game isn’t over for(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; } }
命令模式是设计模式的一种,它通常被用于分离用户界面上定义可视组件的代码(按钮,菜单等等可视组件)与操作这些组件后的执行代码。将操作代码分离开这个概念可以用于跟踪游戏状态的变化,你可以使用这些信息来执行撤销变化。
命令模式的最基本版本是下面的接口:
public interface Command { public void execute(); }
public interface Command { public void execute(); public void undo(); }
要添加撤销功能,你将要创建一个CommandManager类。CommandManager类负责跟踪,执行和撤销命令的实现。
回想一下命令接口提供了将程序从一个状态转换为另外一个状态的方法,也提供了从当前状态反转回上一个状态的方法.
public class CommandManager { private Command lastCommand; public CommandManager() {} public void executeCommand(Command c) { c.execute(); lastCommand = c; } ... }
public boolean isUndoAvailable() { return lastCommand != null; } public void undo() { assert(lastCommand != null); lastCommand.undo(); lastCommand = null; }
public class TicTacToeModel { ... private class PlaceXCommand implements Command { public void execute() { ... } public void undo() { ... } } private class PlaceOCommand implements Command { public void execute() { ... } public void undo() { ... } } }
//第一种:存储当前和下一状态 //Option 1: Storing the previous and next states private class PlaceXCommand implements Command { private TicTacToeModel model; // private int[][] previousGridState; private boolean previousTurnState; private int[][] nextGridState; private boolean nextTurnState; // private PlaceXCommand (TicTacToeModel model, int row, int col) { this.model = model; // previousTurnState = model.playerXTurn; //Copy the entire grid for both states 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; } }
//Option 2: Storing only the changes between states private class PlaceXCommand implements Command { private TicTacToeModel model; private int previousValue; private boolean previousTurn; private int row; private int col; // private PlaceXCommand(TicTacToeModel model, int row, int col) { this.model = model; this.row = row; this.col = col; //Copy the previous value from the grid 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; } }
第五步:将所有融合
为了使用Command的对应实现,PlaceXCommand和PlaceOCommand,你需要修改TicTacModel类。该类必须使用CommandManager而且必须使用Command实例而不是直接应用操作。
public class TicTacToeModel { private CommandManager commandManager; // ... // 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)); } // ... }
public class TicTacToeModel { // ... // public boolean canUndo() { return commandManager.isUndoAvailable(); } // public void undo() { commandManager.undo(); } }
对CommandManager稍作修改你就可以添加支持重做的功能以及不限次数的撤销与重做。
重做功能其背后的原理与撤销几乎一样,除了保存最后执行的Command,你也可以保存最后一个被撤销的命令。当撤销执行时你保存那个命令,当Command执行时则清除原先保存的命令
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; } }
public class CommandManager { private Stack<Command> undos = new Stack<Command>(); private Stack<Command> redos = new Stack<Command>(); public void executeCommand(Command c) { c.execute(); undos.push(c); redos.clear(); } public boolean isUndoAvailable() { return !undos.empty(); } public void undo() { assert(!undos.empty()); Command command = undos.pop(); command.undo(); redos.push(command); } public boolean isRedoAvailable() { return !redos.empty(); } public void redo() { assert(!redos.empty()); Command command = redos.pop(); command.execute(); undos.push(command); } }
如果你想看这一切是怎么结合在一块的下载最终的代码其中包含该教程的完整代码
结语
你或许已注意到你写的最终的CommandManager可以应用于任何Command实现。这意味着你可以用你最喜欢的编程语言来编写CommandManager,创建Command接口的一些实例,而且准备了一个完整的撤销/重做系统。撤销操作是一个很棒的功能,它能够允许用户体验你的游戏且在犯错是不必觉得决策失误。
感谢你对本教程能有兴趣。
作为一个课后思考题:思考如下:命令模式以及CommandManager允许你跟踪游戏时的每一个状态变化。如果你保存了这些信息,你可以创建程序执行的回放。