Trong phần thứ 3 cũng là phần cuối của Series này, mình sẽ cùng các bạn tìm hiểu về phần quan trọng nhất cũng là khó nhất của game, đó là thuật toán tìm kiếm giữa 2 Icon giống nhau ; đồng thời, mình sẽ cùng các bạn hoàn thiện game với các hiệu ứng âm thanh nhé.
Ở phần trước, chúng ta đã cùng nhau tìm hiểu và hoàn thiện các chức năng cơ bản của game là tính điểm, chạy thời gian game, thông báo về thắng, thua, và cuối cùng là xử lý việc biến mất của 2 Icon giống nhau.
Sau phần 3 này, chúng ta sẽ có được 1 game Pikachu hoàn chỉnh để bạn có thể tự chơi, vừa chơi vừa nghe nhạc, tận hưởng game do chính mình làm ra. Nghe hấp dẫn chưa nào? Bắt đầu thôi!
1. Thuật toán tìm kiếm giữa 2 Icon giống nhau
1.1. Phân tích
Nếu đã từng chơi game, chắc hẳn chúng ta đã biết, để hai Pikachu có thể “ăn” nhau và biến mất trên bảng, chúng cần đảm bảo 2 điều kiện:
- Là 2 icon giống nhau.
- Đường đi của 2 icon ấy không bị chặn.
Vậy khi nào là chúng bị chặn? Mình định nghĩa việc 2 icon bị chặn là 2 icon mà để có thể vẽ đường đi ngắn nhất giữa chúng theo các khoảng trống, cần nhiều hơn 3 đường. Hãy cùng xem qua 2 hình ảnh dưới đây:
Ở bên trái là 1 trường hợp thỏa mãn khi 2 Icon đường nối với nhau bằng 3 đoạn thẳng, còn ở bên phải, với việc cần ít nhất 4 đoạn thẳng để nối giữa 2 icon này, nên dù có giống nhau chúng vẫn không thỏa mãn điều kiện về đường đi. Nói cách khác, đường đi thỏa mãn 2 icon ở ảnh bên phải đã bị CHẶN.
Vậy các đường đi ấy được vẽ như thế nào?
1.2. Ý tưởng
Hãy cùng để ý 2 bức ảnh ở trên. Trong 2 bức ảnh ấy, mình đã vẽ đường đi cụ thể của 2 icon giống nhau, và nó sẽ men theo vị trí của các ô trống để tìm đường đi bằng các đoạn thẳng vuông góc với nhau. Để làm được điều ấy, mình cần xác định được vị trí của các ô trống. Như trong những phần trước, chúng ta đã dùng một ma trận matrix để thể hiện cho bảng của các icon trong game. Tại ma trận, mỗi icon sẽ có 1 số tương ứng với giá trị của icon đó; và mỗi khi 2 icon bị biến mất, giá trị của cả 2 icon đều được trả về 0. Nói cách khác, số 0 chính là giá trị của các ô trống không có icon.
Tiếp tục quan sát 2 bức ảnh trên, các đoạn thẳng nối 2 icon còn vượt ra ngoài cả bảng, vậy nghĩa là vượt ra cả ma trận, điều này liệu có đúng không? Nếu ai đã chơi Pokemon rồi, thì chắc chắn biết đoạn thẳng nối ấy hoàn toàn đúng. Hãy quan sát 2 icon sau:
Vốn dĩ 2 icon chúng ta được chọn đã nằm ở ngoài cùng của bảng, và không hề có ô trống nào cả, nhưng theo quy tắc, 2 icon này hoàn toàn thỏa mãn với 3 đoạn nối như sau:
Để tạo ra các đoạn nối thỏa mãn điều đó, ý tưởng của mình là bọc ma trận chính của mình trong 1 vòng gồm các số 0, mình gọi đó là các lề. Việc tạo ra 4 lề tương ứng với tất cả các ô trống với giá trị 0 sẽ bao quanh bảng của mình bằng các vị trí trống, từ đó chúng ta có thể tìm được các đường đi thỏa mãn. Hãy cùng xem ma trận theo ý tưởng của mình khi đó, sẽ là như dưới đây:
Sau đó, mình gắn 2 icon giống nhau bất kỳ vào trong 1 hình chữ nhật được tạo ra từ tọa độ của 2 icon ấy, và nếu 2 icon đó ở vị trí thỏa mãn sẽ chỉ có 3 trường hợp:
Nằm trên cùng 1 cạnh (1 hàng hoặc 1 cột)
Được nối bằng tối đa 3 đoạn thẳng trong phạm vi hình chữ nhật hình thành từ tọa độ của 2 icon
Được nối bằng tối đa 3 đoạn thẳng vượt ra ngoài phạm vi hình chữ nhật hình thành từ tọa độ của 2 icon
Ứng với mỗi trường hợp, chúng ta sẽ xây dựng thuật toán để tìm kiếm đường đi giữa 2 icon đang được xét. Phần thuật toán này, mình bắt đầu từ 1 điểm với tọa độ của icon thứ nhất và xét theo vị trí trống khi tịnh tiến tọa độ của điểm đó theo các hướng khác nhau, nếu đến cuối cùng có thể tìm thấy icon thứ 2 với tối đa 3 đoạn nối, chúng ta sẽ trả về true; ngược lại nếu phải tìm quá 3 đoạn nối, sẽ trả về false. Đó là toàn bộ ý tưởng của mình cho phần xử lý này. Để cùng hiện thức ý tưởng ấy, chúng ta bắt đầu vào phần code nào!
1.3. Xây dựng lại ma trận
Như đã nói ở trên, cũng như từ bài viết đầu tiên ở phần 1 – phần khởi tạo ma trận của các icon mình đã từng đề cập, mình sẽ tạo ra 1 ma trận được bao quanh bởi các số 0, được gọi là Vị trí trống. Để làm được điều đó, trong phần khởi tạo của class ButtonEvent, thay vì khởi tạo row và col được nhập vào, row và col của mình sẽ được tăng lên 2 đơn vị, để tạo ra các đường viền cho ma trận của mình.
public ButtonEvent(MainFrame frame, int row, int col) { … this.row = row + 2; this.col = col + 2; … }
Sự thay đổi ấy dẫn đến sự thay đổi tiếp theo trong phần gán giá trị cho các icon cũng như các phần tử trong ma trận, thay vì bắt đầu từ 0 và kết thúc là row – 1 và col – 1, mình chỉ xét các ô trong vị trí từ 1 đến row – 2 và từ 1 đến col – 2, đồng thời gán giá trị cho các phần tử tại đường viền của matrix với giá trị là 0. Khi đó, ma trận cũ của mình sẽ tự động được bọc bằng 1 đường viền các số 0. Các hàm thay đổi sẽ là hàm createMatrix() trong class Controller và hàm addArrayButton() trong class ButtonEvent.
private void createMatrix() { matrix = new int[row][col]; for (int i = 0; i < col; i++) { matrix[0][i] = matrix[row – 1][i] = 0; } for (int i = 0; i < row; i++) { matrix[i][0] = matrix[i][col – 1] = 0; } … for (int i = 1; i < row – 1; i++) { for (int j = 1; j < col – 1; j++) { … } } … } private void addArrayButton() { … for (int i = 1; i < row – 1; i++) { for (int j = 1; j < col – 1; j++) { … } } }
Cuối cùng đối với hàm showMatrix(), các bạn có thể để như cũ để thấy sự thay đổi của ma trận cũ, hoặc để như mình, là bỏ qua đường viền và chỉ in ra ma trận thật của chúng ta. Việc ý sẽ giúp mình dễ nhìn hơn vì nó ứng với vị trí thật của các icon hiển thị trong game của chúng ta.
public void showMatrix() { for (int i = 1; i < row – 1; i++) { for (int j = 1; j < col – 1; j++) { System.out.printf(“%3d”, matrix[i][j]); } System.out.println(); } }
Vậy là chúng ta đã xử lý xong phần ma trận rồi, tiếp tục đến với phần xây dựng thuật toán tìm kiếm đường đi giữa 2 icon giống nhau nào. Tất cả phần này sẽ được xử lý trong class Controller của chúng ta.
1.4. Thuật toán tìm kiếm đường đi giữa 2 icon giống nhau
Để dễ dàng, mình sẽ xét theo từng trường hợp của 2 icon.
1.4.1. Nằm trên cùng một cạnh
Đây là trường hợp đơn giản và dễ xét nhất. Mình chia làm 2 trường hợp: Nằm trên cùng 1 hàng ngang (x1 = x2) hoặc nằm trên cùng 1 hàng dọc (y1 = y2). Xét trường hợp 2 icon nằm trên cùng 1 hàng ngang (x1 = x2), mình xét 2 tọa độ còn lại là y1 và y2 để tìm ra điểm có hoành độ nhỏ hơn (giả sử trả về y1 < y2), rồi xét liên tục các ô theo hàng ngang x1 từ vị trí y1 đến y2, nếu các ô được xét đều là ô trống (có giá trị là 0), thì đây là 2 icon nằm ở vị trí thỏa mãn. Đường nối tạo ra sẽ có dạng như sau.
Tương tự với trường hợp 2 icon nằm trên cùng 1 hàng dọc. Và để thực hiện việc này, mình viết 2 hàm mới là checkLineX() và checkLineY() trong class Controller, tương ứng với 2 trường hợp 2 icon nằm trên cùng 1 hàng ngang, và nằm trên cùng 1 hàng dọc.
private boolean checkLineX(int y1, int y2, int x) { System.out.println(“check line x”); // find point have column max and min int min = Math.min(y1, y2); int max = Math.max(y1, y2); // run column for (int y = min + 1; y < max; y++) { if (matrix[x][y] != 0) { // if see barrier then die System.out.println(“die: ” + x + “” + y); return false; } System.out.println(“ok: ” + x + “” + y); } // not die -> success return true; } private boolean checkLineY(int x1, int x2, int y) { System.out.println(“check line y”); int min = Math.min(x1, x2); int max = Math.max(x1, x2); for (int x = min + 1; x < max; x++) { if (matrix[x][y] != 0) { System.out.println(“die: ” + x + “” + y); return false; } System.out.println(“ok: ” + x + “” + y); } return true; }
Với cách làm này, có thể các bạn sẽ thắc mắc nếu mình click vào 1 ô 2 lần, 2 hàm này sẽ vẫn trả về true và ô được chọn vẫn sẽ thỏa mãn và biến mất? Đúng là với việc kiểm tra này, trường hợp ấy sẽ vẫn trả về true, nhưng mình sẽ tách việc kiểm tra 2 ô có trùng nhau không ở phần riêng. Tất cả đều có lý do cả đó, hãy đọc tiếp để tìm hiểu nhé!
1.4.2. Được nối bằng tối đa 3 đoạn thẳng trong phạm vi hình chữ nhật hình thành từ tọa độ của 2 icon.
Xét 2 điểm chỉ trong phạm vi được bọc bởi giới hạn từ 2 tọa độ của 2 icon ứng với 2 đầu của hình chữ nhật. Với trường hợp này, mình cũng chia làm 2 trường hợp nhỏ:
Đó là xét theo chiều ngang và chiều dọc.
2 trường hợp này hoàn toàn tương tự nhau về cách xét, chỉ là khác về hướng đi. Mục đích của việc chia làm 2 trường hợp là để mình có thể bao quát toàn bộ các con đường với các hướng đi khác nhau. Giống như ở 1 ngã tư, chúng ta có 4 hướng đi, và muốn biết hướng đi nào dẫn đến đích, chúng ta cần bao quát được con đường từ cả 4 hướng.
Cùng nhau xét 2 điểm p1, p2 với trường hợp xét theo chiều ngang trước nhé. Với việc bắt đầu tìm hướng đi theo chiều ngang, mình lấy ra 2 hoành độ của 2 điểm là y1 và y2. Sau đó, mình so sánh 2 hoành độ này, tìm ra hoành độ nhỏ hơn, và bắt đầu tịnh tiến từ ô có hoành độ nhỏ hơn sang phía ô có hoành độ lớn hơn với vòng for và giá trị tăng là 1. Với mỗi lần tịnh tiến y thêm 1 đơn vị ấy, mình liên tục kiểm tra xem ô nằm tại vị trí kiểm tra có phải là ô trống không. Nếu đó là ô trống, mình sẽ kiểm tra thêm 2 đoạn thẳng còn lại để nối 2 điểm ấy có thỏa mãn không bằng 2 hàm checkLineX() và checkLineY() với các tham số truyền vào tương ứng là điểm cuối cùng trong mỗi đoạn thẳng được vẽ. Hãy quan sát hình ảnh dưới đây để hiểu rõ hơn:
Ở trên mình đã bôi đỏ các điểm sẽ được xét. Đó đều tương ứng là những điểm nối trong những đoạn thẳng. Nếu kiểm tra được 1 đường đi thỏa mãn, hàm kiểm tra sẽ trả về true. Ngược lại nếu gặp 1 icon khác trước khi tìm được đường đi thỏa mãn, hàm sẽ trả về false. Chú ý rằng, mình kiểm tra liên tục bằng vòng for, nên nếu gặp 1 icon khác khi chưa tìm được đường đi thỏa mãn, điều đó nghĩa là tất cả các trường hợp trước đều không có đường đi đúng, đó là cơ sở bảo đảm cho việc trả về false của mình. Và dĩ nhiên, khi kiểm tra hết đường đi đến giới hạn của hình chữ nhật, nếu không có trường hợp nào đúng sẽ mặc định trả về false. Tương tự với phần xét 2 icon theo chiều dọc.
Sẽ có trường hợp 2 icon chỉ được nối với nhau bằng 2 đoạn, nghĩa là chỉ cần xét 2 đoạn là đã tìm được icon còn lại rồi. Với trường hợp ấy, khi chạy hàm checkLineX() và checkLineY() sẽ có 1 trong 2 hàm xét 2 điểm trùng nhau. Tuy nhiên như mình nói ở trên, khi 2 điểm trùng nhau, hàm này vẫn sẽ trả về true, nhờ vậy, việc kiểm tra sẽ không bị ảnh hưởng. Cùng xem 1 ví dụ 2 icon được nối bằng 2 đoạn nhé:
Trong hình trên là trường hợp mình xét 2 icon theo chiều dọc. Mình đã đánh dấu những đoạn nối và hàm checkLineY() sẽ là hàm với vị trí của 2 icon được xét trùng nhau. Và với những suy nghĩ ở trên, mình đã viết thêm 2 hàm mới checkRectX() và checkRectY() trong class Controller.
private boolean checkRectX(Point p1, Point p2) { System.out.println(“check rect x”); // find point have y min and max Point pMinY = p1, pMaxY = p2; if (p1.y > p2.y) { pMinY = p2; pMaxY = p1; } for (int y = pMinY.y; y <= pMaxY.y; y++) { if (y > pMinY.y && matrix[pMinY.x][y] != 0) { return false; } // check two line if ((matrix[pMaxY.x][y] == 0) && checkLineY(pMinY.x, pMaxY.x, y) && checkLineX(y, pMaxY.y, pMaxY.x)) { System.out.println(“Rect x”); System.out.println(“(” + pMinY.x + “,” + pMinY.y + “) -> (” + pMinY.x + “,” + y + “) -> (” + pMaxY.x + “,” + y + “) -> (” + pMaxY.x + “,” + pMaxY.y + “)”); // if three line is true return column y return true; } } // have a line in three line not true then return -1 return false; } private boolean checkRectY(Point p1, Point p2) { System.out.println(“check rect y”); // find point have y min Point pMinX = p1, pMaxX = p2; if (p1.x > p2.x) { pMinX = p2; pMaxX = p1; } // find line and y begin for (int x = pMinX.x; x <= pMaxX.x; x++) { if (x > pMinX.x && matrix[x][pMinX.y] != 0) { return false; } if ((matrix[x][pMaxX.y] == 0) && checkLineX(pMinX.y, pMaxX.y, x) && checkLineY(x, pMaxX.x, pMaxX.y)) { System.out.println(“Rect y”); System.out.println(“(” + pMinX.x + “,” + pMinX.y + “) -> (” + x + “,” + pMinX.y + “) -> (” + x + “,” + pMaxX.y + “) -> (” + pMaxX.x + “,” + pMaxX.y + “)”); return true; } } return false; }
1.4.3. Được nối bằng tối đa 3 đoạn thẳng vượt ra ngoài phạm vi hình chữ nhật hình thành từ tọa độ của 2 icon.
Về cơ bản, phần này khá giống với phần trên, chỉ là giới hạn của việc tìm kiếm không phải chỉ ở giữa tung độ và hoành độ của 2 icon, mà vượt ra ngoài khoảng ấy. Mình chia việc kiểm tra thành 2 trường hợp: Theo chiều dọc và theo chiều ngang. Vì 2 trường hợp này cũng đều tương tự nhau, chỉ khác nhau về hướng tìm kiếm, nên mình sẽ xét trường hợp tìm kiếm đường đi theo chiều ngang.
Đối với trường hợp này, mình tạo ra hàm checkMoreLineX() trong class Controller. Để giải quyết vấn đề đoạn thẳng vượt ra ngoài hình chữ nhật hình thành từ đọa độ của 2 icon, tạo ra biến type trong tham số truyền vào của hàm checkMoreLineX(). Biến type chỉ nhận 2 giá trị: 1 và -1, tương ứng là 2 hướng đi về phía trước, hoặc ngược lại. So sánh 2 icon rồi tìm ra icon có hoành độ nhỏ hơn tương ứng là pMinY và pMaxY. Tương ứng với giá trị của type, nếu type = 1, tạo ra 1 biến y nhận giá trị bằng pMaxY.y + type , ngược lại, nếu type = -1, biến y của mình sẽ nhận giá trị bằng pMinY.y + type. Đối với việc xét theo chiều ngang này, với 2 biến pMaxY.y và pMinY.y chính là giới hạn hoành độ của hình chữ nhật tạo thành bởi 2 icon đang xét. Việc cộng thêm type vào 2 biến chính là việc mở rộng đường tìm kiếm ra ngoài phạm vi của hình chữ nhật đó.
private boolean checkMoreLineX(Point p1, Point p2, int type) { System.out.println(“check chec more x”); // find point have y min Point pMinY = p1, pMaxY = p2; if (p1.y > p2.y) { pMinY = p2; pMaxY = p1; } // find line and y begin int y = pMaxY.y + type; int row = pMinY.x; int colFinish = pMaxY.y; if (type == -1) { colFinish = pMinY.y; y = pMinY.y + type; row = pMaxY.x; System.out.println(“colFinish = ” + colFinish); } … }
Trong phần code trên,khởi tạo thêm 2 biến đó là row và colFinish. Quá trình tìm kiếm luôn có 2 điểm: Điểm bắt đầu tìm kiếm và điểm kết thúc tìm kiếm. Ví dụ như ở những phần trên, luôn là chạy từ hoành độ nhỏ hơn (y1) đến hoành độ lớn hơn (y2). Ở đây, row và colFinish cũng đóng vai trò tương đương như vậy. row chính là tung độ (x) của điểm bắt đầu tìm kiếm, và colFinish là hoành độ (y) của điểm kết thúc tìm kiếm. Và ứng với 2 trường hợp của biến type, row và colFinish cũng sẽ nhận những giá trị khác nhau.
Tiếp theo, chúng ta hãy xem qua các điểm nối của trường hợp này nhé:
Tại đây mình có 3 điểm nối bởi vì trường hợp đó là xét 2 icon và các đoạn nối sẽ vượt ra ngoài phạm vi của hình chữ nhật tạo từ 2 icon ấy. Cần chú ý đến điểm giới hạn, cũng là điểm nối thứ nhất, đó là 1 góc hình chữ nhật mà chúng ta đang xét. Mình kiểm tra giá trị của ô tại điểm nối này, chính là ô matrix[row][colFinish], ô này bắt buộc phải có giá trị bằng 0 hoặc ô này chính là 1 trong 2 icon mà chúng ta đang xét, điều đó cũng có nghĩa là 2 icon nằm ở vị trí cùng 1 cột. Khi đó chúng ta mới tiếp tục xét đến các ô nằm ở vị trí trong khoảng từ ô bắt đầu kiểm tra đến ô tại điểm nối thứ nhất. Đường đi này không được bị chặn bởi 1 icon nào, nghĩa là tất cả các giá trị của các ô trong khoảng đó đều sẽ bằng 0, kiểm tra nó bằng hàm checkLineX(pMinY.y, pMaxY.y, row).
if ((matrix[row][colFinish] == 0 || pMinY.y == pMaxY.y) && checkLineX(pMinY.y, pMaxY.y, row)) { … }
Sau khi đã kiểm tra và vẽ được đoạn nối đến điểm nối thứ nhất thỏa mãn, chúng ta sẽ tiếp tục với điểm nối thứ 2 và thứ 3. Về phần này, vì không còn giới hạn phạm vi, nên không thể dùng vòng for được. Thay vào đó, với biến y đã được khởi tạo ở trên, mình dùng vòng while để kiểm tra xem chừng nào 2 ô matrix[pMinY.x][y] và matrix[pMaxY.x][y] sẽ còn cùng có giá trị bằng 0. Trong vòng while, mình liên tục kiểm tra đoạn nối giữa 2 điểm ứng với 2 ô ma trận đang xét bằng hàm checkLineY(pMinY.x, pMaxY.x, y), nếu đoạn nối ấy thỏa mãn, nghĩa là đã tìm được đường đi thỏa mãn. Khi này, hàm checkMoreLineX() sẽ trả về true, nếu không, giá trị của y sẽ tiếp tục được cộng vào 1 giá trị bằng type (Tiếp tục tăng lên hoặc tiếp tục giảm đi). Nếu vòng While kết thúc mà vẫn chưa tìm được đường đi thỏa mãn, hàm sẽ trả về false.
Trường hợp tìm kiếm theo chiều dọc cũng tương tự như vậy. Dưới đây là code full phần này của mình:
private boolean checkMoreLineX(Point p1, Point p2, int type) { System.out.println(“check chec more x”); // find point have y min Point pMinY = p1, pMaxY = p2; if (p1.y > p2.y) { pMinY = p2; pMaxY = p1; } // find line and y begin int y = pMaxY.y + type; int row = pMinY.x; int colFinish = pMaxY.y; if (type == -1) { colFinish = pMinY.y; y = pMinY.y + type; row = pMaxY.x; System.out.println(“colFinish = ” + colFinish); } // find column finish of line // check more if ((matrix[row][colFinish] == 0 || pMinY.y == pMaxY.y) && checkLineX(pMinY.y, pMaxY.y, row)) { while (matrix[pMinY.x][y] == 0 && matrix[pMaxY.x][y] == 0) { if (checkLineY(pMinY.x, pMaxY.x, y)) { System.out.println(“TH X ” + type); System.out.println(“(” + pMinY.x + “,” + pMinY.y + “) -> (” + pMinY.x + “,” + y + “) -> (” + pMaxY.x + “,” + y + “) -> (” + pMaxY.x + “,” + pMaxY.y + “)”); return true; } y += type; } } return false; } private boolean checkMoreLineY(Point p1, Point p2, int type) { System.out.println(“check more y”); Point pMinX = p1, pMaxX = p2; if (p1.x > p2.x) { pMinX = p2; pMaxX = p1; } int x = pMaxX.x + type; int col = pMinX.y; int rowFinish = pMaxX.x; if (type == -1) { rowFinish = pMinX.x; x = pMinX.x + type; col = pMaxX.y; } if ((matrix[rowFinish][col] == 0|| pMinX.x == pMaxX.x) && checkLineY(pMinX.x, pMaxX.x, col)) { while (matrix[x][pMinX.y] == 0 && matrix[x][pMaxX.y] == 0) { if (checkLineX(pMinX.y, pMaxX.y, x)) { System.out.println(“TH Y ” + type); System.out.println(“(” + pMinX.x + “,” + pMinX.y + “) -> (” + x + “,” + pMinX.y + “) -> (” + x + “,” + pMaxX.y + “) -> (” + pMaxX.x + “,” + pMaxX.y + “)”); return true; } x += type; } } return false; }
1.4.4. Hoàn thiện hàm checkTwoPoint(Point p1, Point p2).
Ở phần trước, chúng ta đã viết hàm checkTwoPoint(Point p1, Point p2) với chức năng chỉ để kiểm tra 2 icon giống nhau thôi. Với rất nhiều hàm kiểm tra ở lần này, bổ sung thêm vào yếu tố đường đi theo thứ tự lần lượt là các trường hợp đã chia ra. Thứ tự kiểm tra sẽ là: Kiểm tra 2 icon xem chúng có nằm trên cùng 1 hàng hoặc 1 cột không, rồi đến kiểm tra xem chúng có được nối với nhau bằng đường zigzag nằm trong phạm vi hình chữ nhật tạo bởi 2 icon đó không, và cuối cùng là kiểm tra 2 icon có được nối bằng đường zigzag nằm ngoài phạm vi hình chữ nhật ấy không. Việc kiểm tra theo thứ tự này sẽ giúp chúng ta kiểm tra tuần tự, tiết kiệm thời gian và không bị bỏ sót trường hợp nào cả. Và dưới đây là hàm checkTwoPoint(Point p1, Point p2) hoàn chỉnh:
public PointLine checkTwoPoint(Point p1, Point p2) { if (!p1.equals(p2) && matrix[p1.x][p1.y] == matrix[p2.x][p2.y]) { // check line with x if (p1.x == p2.x) { System.out.println(“line x”); if (checkLineX(p1.y, p2.y, p1.x)) { return new PointLine(p1, p2); } } // check line with y if (p1.y == p2.y) { System.out.println(“line y”); if (checkLineY(p1.x, p2.x, p1.y)) { System.out.println(“ok line y”); return new PointLine(p1, p2); } } // check in rectangle with x if ( checkRectX(p1, p2)) { System.out.println(“rect x”); return new PointLine(p1, p2); } // check in rectangle with y if (checkRectY(p1, p2)) { System.out.println(“rect y”); return new PointLine(p1, p2); } // check more right if (checkMoreLineX(p1, p2, 1)) { System.out.println(“more right”); return new PointLine(p1, p2); } // check more left if (checkMoreLineX(p1, p2, -1)) { System.out.println(“more left”); return new PointLine(p1, p2); } // check more down if (checkMoreLineY(p1, p2, 1)) { System.out.println(“more down”); return new PointLine(p1, p2); } // check more up if (checkMoreLineY(p1, p2, -1)) { System.out.println(“more up”); return new PointLine(p1, p2); } } return null; }
Đến đây, game đã chính thức được hoàn thiện rồi. Chúng ta có thể dừng lại ở đây, nhưng nếu có thể phát triển game hơn nữa, thì tại sao lại không làm, đúng không? Nào, tiếp tục nhé! ~~
2. Hoàn thiện game với các hiệu ứng âm thanh
Chúng ta có thể làm thêm phần level up, phân chia thành các cấp độ với việc mỗi khi lên cấp, game sẽ mở rộng giao diện với nhiều ô hơn, ít thời gian hơn; hoặc là chúng ta có thể hoàn thành những chức năng cơ bản như thêm nút pause, hoặc thêm Menu Game, thêm chức năng đảo lại ma trận nếu không còn nước đi,… Có rất nhiều thứ có thể làm thêm theo sở thích của mỗi người. Đối với mình thì mình muốn đây chỉ là 1 game nhẹ nhàng với yếu tố thư giãn, nên mình thêm vào 1 số đoạn nhạc để mình có thể vừa chơi game vừa chill, thoải mái. Nếu các bạn muốn mình viết thêm bài hướng dẫn về các chức năng của game, hãy comment ở dưới, mình sẽ đọc và sẽ tiếp tục làm, còn ở bài viết này mình sẽ hoàn thành game của mình với phần hiệu ứng âm thanh. Vậy nên, hãy nhớ tương tác nhé
Tạo thêm 1 class Music ở trong packpage Controller. Sử dụng InputStream để lấy file từ máy, và AudioStream để đọc file nhạc ấy. Chú ý rằng file nhạc nên là file .wav nhé vì đó là đuôi NetBean hỗ trợ nhất và dễ sử dụng nhất. Dưới đây là code class Music của mình:
package controller; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import sun.audio.AudioStream; public class Music { public AudioStream startMusic() { AudioStream BGM = null; try { InputStream test = new FileInputStream(“D:musicpokemon.wav”); BGM = new AudioStream(test); //MD = BGM.getData(); //loop = new ContinuousAudioDataStream(MD); } catch (FileNotFoundException e) { System.out.print(e.toString()); } catch (IOException error) { System.out.print(error.toString()); } return BGM; } public AudioStream winningMusic() { AudioStream BGM = null; try { InputStream test = new FileInputStream(“D:winning.wav”); BGM = new AudioStream(test); //MD = BGM.getData(); //loop = new ContinuousAudioDataStream(MD); } catch (FileNotFoundException e) { System.out.print(e.toString()); } catch (IOException error) { System.out.print(error.toString()); } return BGM; } public AudioStream loseMusic() { AudioStream BGM = null; try { InputStream test = new FileInputStream(“D:lose.wav”); BGM = new AudioStream(test); //MD = BGM.getData(); //loop = new ContinuousAudioDataStream(MD); } catch (FileNotFoundException e) { System.out.print(e.toString()); } catch (IOException error) { System.out.print(error.toString()); } return BGM; } public AudioStream warningMusic() { AudioStream BGM = null; try { InputStream test = new FileInputStream(“D:warning.wav”); BGM = new AudioStream(test); //MD = BGM.getData(); //loop = new ContinuousAudioDataStream(MD); } catch (FileNotFoundException e) { System.out.print(e.toString()); } catch (IOException error) { System.out.print(error.toString()); } return BGM; } }
Với 3 hàm ở trên, một bài bật khi bắt đầu game, 1 bài bật khi thua, 1 bài bật khi thắng, và 1 bài nhạc cảnh báo về thời gian game sắp hết, đơn giản vậy thôi. Cái khó nhất của phần âm nhạc này là, làm sao để các luồng âm nhạc không bị trùng vào nhau. Ví dụ khi thua, hàm loseMusic() sẽ được chạy, lúc đó nhạc nền chạy khi game bắt đầu sẽ cần dừng lại và được thay thế bằng nhạc khi thua game, và nếu chúng ta chọn bắt đầu một game mới, nhạc nền sẽ tiếp tục, đồng thời bản nhạc thua game kia sẽ dừng lại. Nghe có hơi rắc rối không? Tuy nhiên, cách giải quyết vấn đề này rất đơn giản. Mình đã thay đổi hàm showDialogNewGame() trong class MainFrame() một chút nhỏ, chúng ta cùng xem nhé:
import sun.audio.AudioPlayer; import sun.audio.AudioStream; public class MainFrame extends JFrame implements ActionListener, Runnable { … Music m = new Music(); AudioStream as = null; AudioPlayer ap = AudioPlayer.player; public MainFrame() { … as = m.startMusic(); ap.start(as); } … public boolean showDialogNewGame(String message, String title, int t) { pause = true; resume = false; if(t!=0){ ap.stop(as); } int select = JOptionPane.showOptionDialog(null, message, title, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); if (select == 0) { pause = false; ap.start(as); newGame(); return true; } else { if (t == 1) { System.exit(0); return false; } else { ap.start(as); resume = true; return true; } } } }
Trong phần code này, mình đã AudioPlayer là công cụ để phát nhạc. Hàm MainFrame() chính là hàm khởi tạo của game, vậy nên ngay khi game được khởi tạo nhạc sẽ được chạy với bài hát lấy từ hàm startMusic(). Ôn lại những phần trước 1 chút, hàm showDialogNewGame() sẽ được gọi đến khi chúng ta ấn vào nút New Game(), khi chúng ta đã thắng game, hoặc khi hết thời gian và chúng ta thua game. Và như mục đích của chúng ta, mỗi lúc ấy sẽ tắt nhạc nền và sẽ có một đoạn nhạc mới. Vậy nên mình đã thay đổi từ public void showDialogNewGame() thành public boolean showDialogNewGame() để lấy cả lựa chọn của người dùng. Đồng thời nếu hàm được gọi từ nguồn không phải là từ hàm New Game(), nhạc của chúng ta sẽ dừng lại bằng hàm ap.stop(as);. Nếu bắt đầu một game mới, hoặc ấn trở về sau khi ấn nút New game, nhạc sẽ được tiếp tục trở lại và hàm trả về true. Trường hợp còn lại khi chúng ta thoát game, hàm của chúng ta sẽ trả về false.
Tiếp theo, để khởi chạy nhạc khi thắng cuộc, can thiệp vào phần actionPerformed() của class ButtonEvent.
import sun.audio.AudioPlayer; import sun.audio.AudioStream; public class ButtonEvent extends JPanel implements ActionListener { Music m = new Music(); AudioStream as = null; AudioPlayer ap = AudioPlayer.player; … @Override public void actionPerformed(ActionEvent e) { … if (item == 0) { as = m.winningMusic(); ap.start(as); if (frame.showDialogNewGame( “You are winer!nDo you want play again?”, “Win”, 1)) { ap.stop(as); }; } } }
Trong phần này, khi số item còn lại 0, nghĩa là khi thắng cuộc, bật nhạc thắng cuộc bằng AudioPlayer với đoạn nhạc lấy từ hàm winningMusic(), khi đó hàm showDialogNewGame() đồng thời sẽ được gọi, nhạc nền sẽ tắt đi. Đây là lúc chúng ta sử dụng việc thay đổi hàm showDialogNewGame() để nhận giá trị trả về. Nếu hàm showDialogNewGame() trả về true, lựa chọn khi này là bắt đầu game mới, đoạn nhạc chiến thắng này sẽ dừng lại.
Cuối cùng, để hoàn thiện phần hiệu ứng âm thanh, mình can thiệp vào class MyTimeCount() trong phần Main để xử lý 2 phần nhạc còn lại: Phần nhạc cảnh báo, và nhạc khi thua cuộc.
import sun.audio.AudioPlayer; import sun.audio.AudioStream; public class Main { Music m = new Music(); AudioStream as = null; AudioPlayer ap = AudioPlayer.player; … class MyTimeCount extends Thread { public void run() { while (true) { … if (frame.time == 5) { as = m.warningMusic(); ap.start(as); } if (frame.time == 0) { as = m.loseMusic(); ap.start(as); if (frame.showDialogNewGame( “Full timenDo you want play again?”, “Lose”, 1)) { ap.stop(as); }; } } } } }
Phần này cũng tương tự mục đích như phần trên, chỉ có thêm phần nhạc cảnh báo mính sẽ bật song song cùng nhạc nền luôn khi thời gian còn lại 5 giây, để cảnh báo về thời gian cho người chơi.
Lưu ý rằng tất cả những cài đặt này là tùy ý, các bạn có thể cài đặt theo ý của mình, vậy nên hãy tự do sáng tạo nhé ^^
Lời kết:
Vậy là chúng ta đã hoàn thành Series làm game bằng Java cùng game Pokemon này rồi. Cảm ơn các bạn đã đọc đến đây. Chúc các bạn sẽ hoàn thiện game, thậm chí còn hay hơn mình làm rất nhiều nữa. Bất cứ câu hỏi nào hãy để lại ở phần comment nhé, mình sẽ đọc và rep đầy đủ. Chúng ta sẽ lại gặp nhau ở một bài viết sớm nhất. Xin chào và hẹn gặp lại!
Tham khảo:
– Phần 1: https://codelearn.io/blog/view/lam-game-sieu-xin-bang-java-phan-1
– Phần 2: https://codelearn.io/blog/view/lam-game-sieu-xin-bang-java-phan-2
– Code của mình và hình ảnh mình sử dụng: https://github.com/beloyten/codefull
– Phần thuật toán mình tham khảo tại https://nguyenvanquan7826.wordpress.com/