Advertisement
  1. Game Development
  2. Programming

Biarkan Pemain Anda Membatalkan Kesalahan Dalam Game Mereka Dengan Pola Perintah

Scroll to top
Read Time: 15 min

() translation by (you can also view the original English article)

Banyak game berbasis giliran menyertakan tombol undo untuk membiarkan pemain membalikkan kesalahan yang mereka buat selama bermain. Fitur ini menjadi sangat relevan untuk pengembangan game mobile di mana sentuhan mungkin memiliki pengenalan sentuhan yang ceroboh. Daripada mengandalkan sistem di mana Anda bertanya kepada pengguna, "Anda yakin ingin melakukan tugas ini?" untuk setiap tindakan yang mereka lakukan, jauh lebih efisien untuk membiarkan mereka melakukan kesalahan dan memiliki opsi untuk dengan mudah membalikkan tindakan mereka. Dalam tutorial ini, kita akan melihat bagaimana mengimplementasikan ini menggunakan Command Pattern, menggunakan contoh permainan tic-tac-toe.

Catatan: Meskipun tutorial ini ditulis menggunakan Java, Anda harus dapat menggunakan teknik dan konsep yang sama di hampir semua lingkungan pengembangan game. (Ini tidak terbatas pada permainan tic-tac-toe, baik!)


Pratinjau Hasil Akhir

Hasil akhir dari tutorial ini adalah permainan tic-tac-toe yang menawarkan operasi undo and redo tanpa batas.

Demo ini membutuhkan Java untuk dijalankan.

Tidak dapat memuat applet? Tonton video gameplay di YouTube:

Anda juga dapat menjalankan demo di baris perintah menggunakan TicTacToeMain sebagai kelas utama untuk dieksekusi. Setelah mengekstrak sumber, jalankan perintah berikut:

1
javac *.java
2
java TicTacToeMain

Langkah 1: Buat Implementasi Dasar Tic-Tac-Toe

Untuk tutorial ini, Anda akan mempertimbangkan implementasi tic-tac-toe. Meskipun gim ini sangat sepele, konsep yang diberikan dalam tutorial ini dapat diterapkan pada game yang jauh lebih kompleks.

Unduhan berikut (yang berbeda dari unduhan sumber final) berisi kode dasar untuk model permainan tic-tac-toe yang tidak mengandung fitur undo atau redo. Ini akan menjadi tugas Anda untuk mengikuti tutorial ini dan menambahkan fitur-fitur ini. Unduh basis TicTacToeModel.java.

Anda harus memperhatikan, khususnya, metode berikut ini:

1
public void placeX(int row, int col) {
2
    assert(playerXTurn);
3
    assert(spaces[row][col] == 0);
4
    spaces[row][col] = 1;
5
    playerXTurn = false;
6
}
1
public void placeO(int row, int col) {
2
  assert(!playerXTurn);
3
	assert(spaces[row][col] == 0);
4
	spaces[row][col] = 2;
5
	playerXTurn = true;
6
}

Metode-metode ini adalah satu-satunya metode untuk game ini yang mengubah keadaan grid game. Mereka akan menjadi apa yang akan diubah.

Jika Anda bukan pengembang Java, Anda mungkin masih dapat memahami kode tersebut. Ini disalin di sini jika Anda hanya ingin merujuknya:

1
/** The game logic for a Tic-Tac-Toe game. This model does not have

2
 * an associated User Interface: it is just the game logic.

3
 * 

4
 * The game is represented by a simple 3x3 integer array. A value of

5
 * 0 means the space is empty, 1 means it is an X, 2 means it is an O. 

6
 * 

7
 * @author aarnott

8
 *

9
 */
10
public class TicTacToeModel {
11
	//True if it is the X player’s turn, false if it is the O player’s turn

12
	private boolean playerXTurn;
13
	//The set of spaces on the game grid

14
	private int[][] spaces;
15
	
16
	/** Initialize a new game model. In the traditional Tic-Tac-Toe

17
	 * game, X goes first.

18
	 * 

19
	 */
20
	public TicTacToeModel() {
21
		spaces = new int[3][3]; 
22
		playerXTurn = true;
23
	}
24
	
25
	/** Returns true if it is the X player's turn.

26
	 * 

27
	 * @return

28
	 */
29
	public boolean isPlayerXTurn() {
30
		return playerXTurn;
31
	}
32
	
33
	/** Returns true if it is the O player's turn.

34
	 * 

35
	 * @return

36
	 */
37
	public boolean isPlayerOTurn() {
38
		return !playerXTurn;
39
	}
40
	
41
	/** Places an X on a space specified by the row and column

42
	 * parameters.

43
	 * 

44
	 * Preconditions:

45
	 * -> It must be the X player's turn

46
	 * -> The space must be empty

47
	 * 

48
	 * @param row The row to place the X on

49
	 * @param col The column to place the X on

50
	 */
51
	public void placeX(int row, int col) {
52
		assert(playerXTurn);
53
		assert(spaces[row][col] == 0);
54
		spaces[row][col] = 1;
55
		playerXTurn = false;
56
	}
57
	
58
	/** Places an O on a space specified by the row and column

59
	 * parameters.

60
	 * 

61
	 * Preconditions:

62
	 * -> It must be the O player's turn

63
	 * -> The space must be empty

64
	 * 

65
	 * @param row The row to place the O on

66
	 * @param col The column to place the O on

67
	 */	
68
	public void placeO(int row, int col) {
69
		assert(!playerXTurn);
70
		assert(spaces[row][col] == 0);
71
		spaces[row][col] = 2;
72
		playerXTurn = true;
73
	}
74
	
75
	/** Returns true if a space on the grid is empty (no Xs or Os)

76
	 * 	

77
	 * @param row

78
	 * @param col

79
	 * @return

80
	 */
81
	public boolean isSpaceEmpty(int row, int col) {
82
		return (spaces[row][col] == 0);
83
	}
84
	
85
	/** Returns true if a space on the grid is an X.

86
	 * 	

87
	 * @param row

88
	 * @param col

89
	 * @return

90
	 */
91
	public boolean isSpaceX(int row, int col) {
92
		return (spaces[row][col] == 1);
93
	}
94
	
95
	/** Returns true if a space on the grid is an O.

96
	 * 	

97
	 * @param row

98
	 * @param col

99
	 * @return

100
	 */
101
	public boolean isSpaceO(int row, int col) {
102
		return (spaces[row][col] == 2);
103
	}
104
	
105
	/** Returns true if the X player won the game. That is, if the

106
	 * X player has completed a line of three Xs.

107
	 * 

108
	 * @return

109
	 */
110
	public boolean hasPlayerXWon() {
111
		//Check rows

112
		if(spaces[0][0] == 1 && spaces[0][1] == 1 && spaces[0][2] == 1) return true;
113
		if(spaces[1][0] == 1 && spaces[1][1] == 1 && spaces[1][2] == 1) return true;
114
		if(spaces[2][0] == 1 && spaces[2][1] == 1 && spaces[2][2] == 1) return true;
115
		//Check columns

116
		if(spaces[0][0] == 1 && spaces[1][0] == 1 && spaces[2][0] == 1) return true;
117
		if(spaces[0][1] == 1 && spaces[1][1] == 1 && spaces[2][1] == 1) return true;
118
		if(spaces[0][2] == 1 && spaces[1][2] == 1 && spaces[2][2] == 1) return true;
119
		//Check diagonals

120
		if(spaces[0][0] == 1 && spaces[1][1] == 1 && spaces[2][2] == 1) return true;
121
		if(spaces[0][2] == 1 && spaces[1][1] == 1 && spaces[2][0] == 1) return true;
122
		//Otherwise, there is no line

123
		return false;
124
	}
125
	
126
	/** Returns true if the O player won the game. That is, if the

127
	 * O player has completed a line of three Os.

128
	 * 

129
	 * @return

130
	 */	
131
	public boolean hasPlayerOWon() {
132
		//Check rows

133
		if(spaces[0][0] == 2 && spaces[0][1] == 2 && spaces[0][2] == 2) return true;
134
		if(spaces[1][0] == 2 && spaces[1][1] == 2 && spaces[1][2] == 2) return true;
135
		if(spaces[2][0] == 2 && spaces[2][1] == 2 && spaces[2][2] == 2) return true;
136
		//Check columns

137
		if(spaces[0][0] == 2 && spaces[1][0] == 2 && spaces[2][0] == 2) return true;
138
		if(spaces[0][1] == 2 && spaces[1][1] == 2 && spaces[2][1] == 2) return true;
139
		if(spaces[0][2] == 2 && spaces[1][2] == 2 && spaces[2][2] == 2) return true;
140
		//Check diagonals

141
		if(spaces[0][0] == 2 && spaces[1][1] == 2 && spaces[2][2] == 2) return true;
142
		if(spaces[0][2] == 2 && spaces[1][1] == 2 && spaces[2][0] == 2) return true;
143
		//Otherwise, there is no line

144
		return false;
145
	}
146
	
147
	/** Returns true if all the spaces are filled or one of the players has

148
	 * won the game.

149
	 * 

150
	 * @return

151
	 */
152
	public boolean isGameOver() {
153
		if(hasPlayerXWon() || hasPlayerOWon()) return true;
154
		//Check if all the spaces are filled. If one isn’t the game isn’t over

155
		for(int row = 0; row < 3; row++) {
156
			for(int col = 0; col < 3; col++) {
157
				if(spaces[row][col] == 0) return false;
158
			}
159
		}
160
		//Otherwise, it is a “cat’s game”

161
		return true;
162
	}	
163
164
}

Langkah 2: Memahami Pola Perintah

Pola Command adalah pola desain yang umum digunakan dengan antarmuka pengguna untuk memisahkan tindakan yang dilakukan oleh tombol, menu, atau widget lain dari definisi kode interface pengguna untuk objek-objek ini. Konsep pemisahan kode tindakan ini dapat digunakan untuk melacak setiap perubahan yang terjadi pada kondisi permainan, dan Anda dapat menggunakan informasi ini untuk membalikkan perubahan.

Versi paling dasar dari pola Command adalah interface berikut:

1
public interface Command {
2
	public void execute();
3
}

Setiap tindakan yang diambil oleh program yang mengubah keadaan permainan - seperti menempatkan X di ruang tertentu - akan mengimplementasikan interface Command. Ketika tindakan diambil, metode execute() dipanggil.

Sekarang, Anda mungkin memperhatikan bahwa antarmuka ini tidak menawarkan kemampuan untuk membatalkan tindakan; yang dilakukannya hanyalah membawa game dari satu negara ke negara lain. Peningkatan berikut ini akan memungkinkan tindakan implementasi untuk menawarkan kemampuan undo.

1
public interface Command {
2
	public void execute();
3
	public void undo();
4
}

Tujuan ketika mengimplementasikan suatu Command adalah untuk memiliki metode undo() membalikkan setiap tindakan yang diambil oleh metode execute. Sebagai konsekuensinya, metode execute() juga akan dapat memberikan kemampuan untuk mengulang suatu tindakan.

Itu ide dasarnya. Ini akan menjadi lebih jelas saat kita menerapkan Perintah spesifik untuk game ini.


Langkah 3: Buat Manajer Perintah

Untuk menambahkan fitur undo, Anda akan membuat kelas CommandManager. CommandManager bertanggung jawab untuk melacak, mengeksekusi, dan membatalkan implementasi Command.

(Ingat bahwa interface Command menyediakan metode untuk membuat perubahan dari satu keadaan program ke yang lain dan juga membalikkannya.)

1
public class CommandManager {
2
    private Command lastCommand;
3
4
    public CommandManager() {}
5
6
    public void executeCommand(Command c) {
7
        c.execute();
8
        lastCommand = c;
9
    }
10
11
    ...
12
13
}

Untuk mengeksekusi Command, CommandManager dilewatkan contoh Command, dan itu akan mengeksekusi Command dan kemudian menyimpan Command yang paling baru dieksekusi untuk referensi nanti.

Menambahkan fitur undo ke CommandManager hanya perlu memberitahukannya untuk membatalkan Command terbaru yang dieksekusi.

1
public boolean isUndoAvailable() {
2
    return lastCommand != null;
3
}
4
5
public void undo() {
6
    assert(lastCommand != null);
7
    lastCommand.undo();
8
    lastCommand = null;
9
}

Kode ini adalah semua yang diperlukan untuk memiliki CommandManager yang fungsional. Agar berfungsi dengan baik, Anda harus membuat beberapa implementasi interface Command.


Langkah 4: Buat Implementasi dari Command Interface

Tujuan dari pola Command untuk tutorial ini adalah untuk memindahkan kode apa pun yang mengubah status game tic-tac-toe menjadi instance Command. Yaitu, kode dalam metode placeX() dan placeO() adalah apa yang akan Anda ubah.

Di dalam kelas TicTacToeModel, tambahkan masing-masing dua kelas bagian dalam yang disebut PlaceXCommand dan PlaceOCommand, yang masing-masing mengimplementasikan interface Command.

1
public class TicTacToeModel {
2
3
	...
4
5
	private class PlaceXCommand implements Command {
6
7
		public void execute() {
8
			...
9
		}
10
11
		public void undo() {
12
			...
13
		}
14
15
	}
16
17
	private class PlaceOCommand implements Command {
18
19
		public void execute() {
20
			...
21
		}
22
23
		public void undo() {
24
			...
25
		}
26
27
	}
28
29
}

Tugas implementasi Command adalah untuk menyimpan keadaan dan memiliki logika untuk transisi ke keadaan baru yang dihasilkan dari pelaksanaan Command atau untuk transisi kembali ke keadaan awal sebelum Command dieksekusi. Ada dua cara mudah untuk mencapai tugas ini.

  1. Simpan seluruh negara bagian sebelumnya dan negara bagian berikutnya. Setel status saat ini game ke status berikutnya saat execute() dipanggil dan atur status saat ini game ke status sebelumnya yang disimpan saat undo() dipanggil.
  2. Simpan hanya informasi yang berubah antar states. Ubah hanya informasi yang disimpan ini ketika execute() atau undo() dipanggil.
1
//Option 1: Storing the previous and next states

2
private class PlaceXCommand implements Command {
3
    private TicTacToeModel model;
4
    //

5
    private int[][] previousGridState;
6
    private boolean previousTurnState;
7
    private int[][] nextGridState;
8
    private boolean nextTurnState;    
9
    //

10
    private PlaceXCommand (TicTacToeModel model, int row, int col) {
11
        this.model = model;
12
        //

13
        previousTurnState = model.playerXTurn;
14
        //Copy the entire grid for both states

15
        previousGridState = new int[3][3];
16
        nextGridState = new int[3][3];
17
        for(int i = 0; i < 3; i++) {
18
            for(int j = 0; j < 3; j++) {
19
                //This is allowed because this class is an inner

20
                //class. Otherwise, the model would need to 

21
                //provide array access somehow.

22
                previousGridState[i][j] = m.spaces[i][j];
23
                nextGridState[i][j] = m.spaces[i][j];                
24
            }
25
        }
26
        //Figure out the next state by applying the placeX logic

27
        nextGridState[row][col] = 1;
28
        nextTurnState = false;
29
    }
30
    //

31
    public void execute() {
32
        model.spaces = nextGridState;
33
        model.playerXTurn = nextTurnState;
34
    }
35
    //

36
    public void undo() {
37
        model.spaces = previousGridState;
38
        model.playerXTurn = previousTurnState;
39
    }
40
}

Opsi pertama agak boros, tapi bukan berarti desainnya jelek. Kode mudah dan kecuali informasi negara sangat besar jumlah limbah tidak akan menjadi sesuatu yang perlu dikhawatirkan.

Anda akan melihat bahwa, dalam hal tutorial ini, opsi kedua lebih baik, tetapi pendekatan ini tidak selalu menjadi yang terbaik untuk setiap program. Namun, lebih sering daripada tidak, opsi kedua adalah jalan yang harus ditempuh.

1
//Option 2: Storing only the changes between states

2
private class PlaceXCommand implements Command {
3
    private TicTacToeModel model;
4
    private int previousValue;
5
    private boolean previousTurn;
6
    private int row;
7
    private int col;
8
    //

9
    private PlaceXCommand(TicTacToeModel model, int row, int col) {
10
        this.model = model;
11
        this.row = row;
12
        this.col = col;
13
        //Copy the previous value from the grid

14
        this.previousValue = model.spaces[row][col];
15
        this.previousTurn = model.playerXTurn;
16
    }
17
    //

18
    public void execute() {
19
        model.spaces[row][col] = 1;		
20
        model.playerXTurn = false;
21
    }
22
    //	

23
    public void undo() {
24
        model.spaces[row][col] = previousValue;
25
        model.playerXTurn = previousTurn;
26
    }		
27
}

Opsi kedua hanya menyimpan perubahan yang terjadi, bukan seluruh negara. Dalam hal tic-tac-toe, lebih efisien dan tidak terlalu kompleks untuk menggunakan opsi ini.

Kelas dalam PlaceOCommand ditulis dengan cara yang sama - silakan menulis sendiri!


Langkah 5: Satukan Semuanya

Untuk memanfaatkan implementasi Command, PlaceXCommand dan PlaceOCommand, Anda perlu memodifikasi kelas TicTacToeModel. Kelas harus menggunakan CommandManager dan harus menggunakan instance Command alih-alih menerapkan tindakan secara langsung.

1
public class TicTacToeModel {
2
    private CommandManager commandManager;
3
    //

4
    ...
5
    //

6
    public TicTacToeModel() {
7
        ...
8
        //

9
        commandManager = new CommandManager();
10
    }
11
    //

12
    ...
13
    //

14
    public void placeX(int row, int col) {
15
        assert(playerXTurn);
16
        assert(spaces[row][col] == 0);
17
        commandManager.executeCommand(new PlaceXCommand(this, row, col));
18
    }
19
    //

20
    public void placeO(int row, int col) {
21
        assert(!playerXTurn);
22
        assert(spaces[row][col] == 0);
23
        commandManager.executeCommand(new PlaceOCommand(this, row, col));
24
    }
25
    //

26
    ...
27
}

Kelas TicTacToeModel akan berfungsi persis seperti sebelum perubahan Anda sekarang, tetapi Anda juga dapat mengekspos fitur undo. Tambahkan metode undo() ke model dan juga tambahkan metode pemeriksaan canUndo untuk interface pengguna untuk digunakan di beberapa titik.

1
public class TicTacToeModel {
2
    //

3
    ...
4
    //

5
    public boolean canUndo() {
6
        return commandManager.isUndoAvailable();
7
    }
8
    //

9
    public void undo() {
10
        commandManager.undo();
11
    }
12
13
}

Anda sekarang memiliki model permainan tic-tac-toe yang berfungsi penuh yang mendukung undo!


Langkah 6: Ikuti Lebih Lanjut

Dengan beberapa modifikasi kecil pada CommandManager, Anda dapat menambahkan dukungan untuk operasi yang dilakukan ulang serta jumlah undo dan redos yang tidak terbatas.

Konsep di balik fitur redo hampir sama dengan fitur undo. Selain menyimpan Command terakhir yang dijalankan, Anda juga menyimpan Command terakhir yang dibatalkan. Anda menyimpan Command itu saat membatalkan dipanggil dan menghapusnya saat Command dijalankan.

1
public class CommandManager {
2
3
    private Command lastCommandUndone;
4
5
    ...
6
7
    public void executeCommand(Command c) {
8
        c.execute();
9
        lastCommand = c;
10
        lastCommandUndone = null;
11
    }
12
13
    public void undo() {
14
        assert(lastCommand != null);
15
        lastCommand.undo();
16
        lastCommandUndone = lastCommand;
17
        lastCommand = null;
18
    }
19
20
    public boolean isRedoAvailable() {
21
        return lastCommandUndone != null;
22
    }
23
24
    public void redo() {
25
        assert(lastCommandUndone != null);
26
        lastCommandUndone.execute();
27
        lastCommand = lastCommandUndone;
28
        lastCommandUndone = null;
29
    }
30
}

Menambahkan beberapa undo dan redo adalah masalah menyimpan setumpuk tindakan yang tidak dapat dibatalkan dan diperbaiki. Ketika tindakan baru dieksekusi ditambahkan ke undo stack dan redo stack dihapus. Ketika suatu tindakan dibatalkan, ia ditambahkan ke tumpukan redo dan dihapus dari tumpukan undo. Ketika suatu tindakan redone, itu dihapus dari tumpukan ulang dan ditambahkan ke tumpukan dibatalkan.

Undo and redo in game development with the command patternUndo and redo in game development with the command patternUndo and redo in game development with the command pattern

Gambar di atas menunjukkan contoh tumpukan yang sedang beraksi. Redo stack memiliki dua item dari perintah yang telah dibatalkan. Ketika perintah baru, PlaceX(0,0) dan PlaceO(0,1), dieksekusi, redo stack dihapus dan ditambahkan ke undo stack. Ketika PlaceO(0,1) dibatalkan, itu dihapus dari atas tumpukan dibatalkan dan ditempatkan di tumpukan ulang.

Berikut ini tampilannya dalam kode:

1
public class CommandManager {
2
3
    private Stack<Command> undos = new Stack<Command>();
4
    private Stack<Command> redos = new Stack<Command>();
5
6
    public void executeCommand(Command c) {
7
        c.execute();
8
        undos.push(c);
9
        redos.clear();
10
    }
11
12
    public boolean isUndoAvailable() {
13
        return !undos.empty();
14
    }
15
16
    public void undo() {
17
        assert(!undos.empty());
18
        Command command = undos.pop();
19
        command.undo();
20
        redos.push(command);
21
    }
22
23
    public boolean isRedoAvailable() {
24
        return !redos.empty();
25
    }
26
27
    public void redo() {
28
        assert(!redos.empty());
29
        Command command = redos.pop();
30
        command.execute();
31
        undos.push(command);
32
    }
33
}

Sekarang Anda memiliki model permainan tic-tac-toe yang dapat membatalkan tindakan semua jalan kembali ke awal permainan dan mengulangnya lagi.

Jika Anda ingin melihat bagaimana semua ini cocok, ambil unduhan sumber terakhir, yang berisi kode lengkap dari tutorial ini.


Kesimpulan

Anda mungkin telah memperhatikan bahwa CommandManager terakhir yang Anda tulis akan bekerja untuk implementasi Command apa pun. Ini berarti Anda dapat membuat CommandManager dalam bahasa favorit Anda, membuat beberapa instance dari interface Command, dan menyiapkan sistem lengkap untuk undo/redo. Fitur undo bisa menjadi cara yang bagus untuk memungkinkan pengguna menjelajahi game Anda dan membuat kesalahan tanpa merasa berkomitmen pada keputusan yang buruk.

Terima kasih telah tertarik pada tutorial ini!

Sebagai bahan pemikiran lebih lanjut, pertimbangkan hal berikut: pola Command bersama dengan CommandManager memungkinkan Anda untuk melacak setiap perubahan negara selama eksekusi game Anda. Jika Anda menyimpan informasi ini, Anda dapat membuat replay dari pelaksanaan program.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.