Biarkan Pemain Anda Membatalkan Kesalahan Dalam Game Mereka Dengan Pola Perintah
() 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.
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.
- 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 saatundo()
dipanggil. - Simpan hanya informasi yang berubah antar states. Ubah hanya informasi yang disimpan ini ketika
execute()
atauundo()
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.



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.