Làm Game Siêu Xịn Bằng Java Phần 2

Làm Game Siêu Xịn Bằng Java Phần 2

Chào mừng bạn quay trở lại với series Lập trình game bằng Java. Ở phần trước, chúng ta đã hoàn thành việc hiển thị các Icon lên, đảm bảo việc chúng ngẫu nhiên, đầy đủ, và các các Icon giống nhau sẽ luôn xuất hiện theo cặp. Đặc biệt là các bạn có thể tùy ý thêm vào các Icon mà mình yêu thích, nên các bạn có thể tạo ra một game Pokemon của riêng mình.

Ở phần 2 này, mình sẽ cùng các bạn tiếp tục việc hoàn thiện giao diện, thêm vào thanh thời gian, điểm, và xử lý sự kiện 2 Pokemon giống nhau được chọn sẽ biến mất. Chúng ta cùng bắt đầu thôi!

1. Hoàn thiện giao diện game

Giao diện game Pokemon hoàn chỉnh của mình sẽ bao gồm: Màn hình game, thanh thời gian, ô tính điểm và nút New game. Ở phần 1, chúng ta đã hoàn thành việc hiển thị màn hình game. Bây giờ chúng ta sẽ hoàn thiện nốt các phần còn lại. 

1.1. Hoàn thành giao diện

Để hoàn thiện giao diện game, trong lớp MainFrame(), mình tạo thêm một hàm createControlPanel() để khởi tạo các phần còn thiếu gồm: Thanh thời gian, ô tính điểm, và button New game; đồng thời, mình cũng tạo ra 1 biến MAX_TIME=300 ,1 biến time= maxTime và các Component mình dùng để khởi tạo.

private int MAX_TIME = 300;
public int time = MAX_TIME;
public JLabel lbScore;
private JProgressBar progressTime;
private JButton btnNewGame;

Mình để 2 biến time và lbScore là public vì đây sẽ là 2 biến mình sẽ dùng để xử lý và thay đổi giá trị tại các lớp khác.

Tiếp theo, mình viết hàm createButton() dùng để khởi tạo 1 button mới, mục đích của hàm này là để khởi tạo nút New game, bạn cũng có thể tùy ý sáng tạo thêm các nút mới, nhưng nhớ là thêm các action cho các nút mới nhé.

private JButton createButton(String buttonName) {
	JButton btn = new JButton(buttonName);
	btn.addActionListener(this);
	return btn;
}

Và để hoàn thiện phần giao diện này, mình hoàn thiện hàm createControlPanel() để khởi tạo các phần còn thiếu của giao diện.

private JPanel createControlPanel() {
    //tạo JLabel lblScore với giá trị ban đầu là 0
	lbScore = new JLabel("0");
	progressTime = new JProgressBar(0, 100);
	progressTime.setValue(100);

	//tạo Panel chứa Score và Time

	JPanel panelLeft = new JPanel(new GridLayout(2, 1, 5, 5));
	panelLeft.add(new JLabel("Score:"));
	panelLeft.add(new JLabel("Time:"));

	JPanel panelCenter = new JPanel(new GridLayout(2, 1, 5, 5));
	panelCenter.add(lbScore);
	panelCenter.add(progressTime);

	JPanel panelScoreAndTime = new JPanel(new BorderLayout(5, 0));
	panelScoreAndTime.add(panelLeft, BorderLayout.WEST);
	panelScoreAndTime.add(panelCenter, BorderLayout.CENTER);

	// tạo Panel mới chứa panelScoreAndTime và nút New Game
	JPanel panelControl = new JPanel(new BorderLayout(10, 10));
	panelControl.setBorder(new EmptyBorder(10, 3, 5, 3));
	panelControl.add(panelScoreAndTime, BorderLayout.CENTER);
	panelControl.add(btnNewGame = createButton("New Game"),
			BorderLayout.PAGE_END);


	// Set BorderLayout để panelControl xuất hiện ở đầu trang
	JPanel panel = new JPanel(new BorderLayout());
	panel.setBorder(new TitledBorder("Good luck"));
	panel.add(panelControl, BorderLayout.PAGE_START);
	
	return panel;
}

GridLayout(int rows, int cols, int hgap, int vgap)Tạo GridLayout với số hàng, số cột, khoảng cách giữa các hàng và các cột. Với Code của mình là GridLayout(2, 1, 5, 5) , mình đã tạo ra 1 GridLayout với 2 hàng, 1 cột, khoảng cách giữa các hàng và cột là 5px. 

Mình sử dụng GridLayout  vì mình muốn thêm Score (thanh điểm) và Time (Thanh thời gian) vào dùng 1 khối. Với việc sử dụng GridLayout ,mình sẽ dễ dàng thêm vào panelLeft và panelCenter các Label của thanh điểm và thanh thời gian, và để chúng xuất hiện theo 1 form đã định sẵn.

BorderLayout(int hgap, int vgap): Xây dựng một Border Layout với các khoảng cách gap theo chiều dọc và ngang đã xác định giữa các thành phần.

BorderLayout.WEST: Ràng buộc hiển thị ở hướng tây (nằm ở bên trái của container)

BorderLayout.CENTER: Ràng buộc hiển thị ở trung tâm (ở giữa container).

BorderLayout.PAGE_END: Hiển thị ở sau dòng cuối cùng (last line) của nội dung layout.

BorderLayout.PAGE_START: Hiển thị ở trước dòng đầu tiên (first line) của nội dung layout.

EmptyBorder(10, 3, 5, 3)EmptyBorder() là border trống, nó thường được dùng để tạo khoảng trống, công dụng như sử dụng padding để đặt khoảng cách giữa nội dung và bên ngoài.

Sau khi hoàn thành hàm createControlPanel() , chúng ta sẽ khởi tạo nó tại hàm createMainPanel(), và để nó xuất hiện ở bên phải.

private JPanel createMainPanel() {
	JPanel panel = new JPanel(new BorderLayout());
	...
    panel.add(createControlPanel(), BorderLayout.EAST);
	return panel;
}

Khi đó, màn hình game sẽ thay đổi thành như hình ảnh sau.

Nhìn đã khá ổn rồi đúng không? Tuy nhiên đó mới chỉ là giao diện thôi, và vẫn chưa hề có chức năng nào cả. Tiếp theo, chúng ta sẽ thêm chức năng cho nút New game và chạy thời gian cho game. 

1.2. Hoàn thiện chức năng giao diện

Đầu tiên, chúng ta hãy cùng đến với nút New game.  Để thêm chức năng cho nút New game, mình viết hàm newGame(). Mỗi khi gọi đến hàm này, tất cả các dữ liệu về time, score, hay các Icon đều sẽ được cài lại mặc định ban đầu.

public void newGame() {
	time = MAX_TIME;
	graphicsPanel.removeAll();
	mainPanel.add(createGraphicsPanel(), BorderLayout.CENTER);
	mainPanel.validate();
	mainPanel.setVisible(true);
	lbScore.setText("0");
}

Như các bạn đã thấy ở những dòng code trên, hàm newGame() của mình trả lại time bằng giá trị MAX_TIME (Thời gian ban đầu), score trả về giá trị 0, và graphicsPanel chính là ô chứa các Icon sẽ removeAll() (gỡ hết các Icon đi) và thêm lại 1 bảng các Icon mới. validate() dùng để set thuộc tính hợp lệ cho Component được tạo ra.

Sau đó, mình tạo ra hàm showDialogNewGame(). Đây là một hàm quan trọng, vì nó sẽ được sử dụng khá nhiều, khi bạn thắng, khi bạn thua, hoặc khi bạn ấn vào nút New Game

public void showDialogNewGame(String message, String title, int t) {
	int select = JOptionPane.showOptionDialog(null, message, title,
	JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
				null, null);
	if (select == 0) {
		newGame();
	} else {
		if(t==1){
        System.exit(0);
        }
	}
}

Hàm showDialogNewGame() này sẽ hiển thị ra 1 message bằng JOptionPane.YES_NO_OPTION với 3 giá trị truyền vào lần lượt là: message - thông báo hiển thị ra của hộp thoại JOptionPanetittle là tiêu đề của hộp thoại, cùng biến t để kiểm tra nguồn xuất hiện của hộp thoại. Nguồn xuất hiện ở đây mình phân biệt từ 2 nguồn, hoặc là sau khi thắng game hoặc thua game, khi đó giá trị truyền vào của sẽ là 1; hoặc là sau khi ấn vào nút New game khi trò chơi vẫn đang chạy, khi đó giá trị truyền vào của t sẽ là 0. Với t=1, nghĩa là sau khi bạn thắng game hoặc thua game, nếu bạn không muốn tiếp tục trò chơi, chọn No thì game sẽ thoát bằng hàm System.exit(0);. Còn nếu t=0, là khi game vẫn đang diễn ra, nếu bạn chọn No thì sẽ không xảy ra gì cả, hộp thoại biến mất và game sẽ được tiếp tục với trò chơi hiện tại của bạn. Trong cả 2 trường hợp, nếu chọn Yes , hàm newGame() sẽ được chạy, và chúng ta sẽ có 1 trò chơi mới.

Cuối cùng, việc còn lại là thêm action cho nút New Game của chúng ta. Chú ý rằng lớp MainFrame của chúng ta được kế thừa từ 2 Interface là ActionListener và Runnable, vậy nên ngay sau khi khởi tạo, chúng ta có 2 phương thức override là actionPerformed(ActionEvent e) và run()Việc thêm action cho nút New Game mình sẽ thực hiện trực tiếp trong phần override  của phương thức actionPerformed(ActionEvent e).

@Override
public void actionPerformed(ActionEvent e) {
       if (e.getSource() == btnNewGame) {
	   showDialogNewGame("Your game hasn't done. Do you want to create a new game?", "Warning",0);
	}
}

Phần này rất đơn giản đúng không? Mình dùng e.getSource() để kiểm tra nếu nút New Game  (có tên là btnNewGame) của mình được click vào, khi đó mình sẽ gọi đến hàm showDialogNewGame, hiển thị ra 1 hộp thoại nhắc nhở với các tham số truyền vào như trên. Với tham số t=0, nếu chọn No, game của mình sẽ không được tạo mới, và vẫn tiếp tục bình thường.

Sau khi xử lý action thành công cho nút New Game, chúng ta sẽ tiếp tục xử lý về thời gian của game.  Như trong giao diện game của chúng ta, time được hiển thị dưới dạng một thanh JProgressBar. Để thay đổi thời gian, chúng ta cần liên tục thay đổi giá trị của thanh JProgressBar này. Ở trên, chúng ta đã can thiệp vào nút New Game bằng phần override của phương thức actionPerformed, đối với phần xử lý time này, chúng ta sẽ can thiệp vào phần override còn lại, đó là phương thức run().

Ý tưởng của mình rất đơn giản, đó là liên tục cập nhập giá trị của biến progressTime được khởi tạo từ thanh JProgressBar ở trên bằng tỉ lệ của sự thay đổi của biến time với thời gian tối đa của game (maxTime) và nó sẽ được cập nhập mỗi giây (tương ứng với 1000 ms). Các bạn hãy cùng theo dõi đoạn code của mình để rõ hơn.

@Override
public void run() {
        while (true) {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		progressTime.setValue((int) ((double) time / MAX_TIME * 100));
	}
}

Chúng ta biết bằng giá trị tối đa của progressTime là 100 (Tương ứng với 100%). Mình muốn game của mình có thời gian tối đa là 5 phút, tương ứng với 300s, đó là giá trị của MAX_TIME, đồng thời cũng là giá trị khởi tạo của time. Việc thay đổi giá trị của progressTime dựa vào tỉ lệ của 2 biến time và MAX_TIME cần đặt ép kiểu (double) ở trước vì time và MAX_TIME đều là kiểu int, nên nếu không ép kiểu, việc thời gian (time) bị giảm đi, giá trị của time/MAX_TIME sẽ trở thành 0, khi đó việc nhân với 100 ở sau sẽ trở nên vô nghĩa. Và dĩ nhiên, sau khi ép kiểu, để có thể setValue() cho progressTime, chúng ta cần ép kiểu 1 lần nữa giá trị của (double) time/MAX_TIME*100 về kiểu  int

Để xử lý thời gian của game, mình muốn mỗi khi Popup được gọi ra từ hàm showDialogNewGame() hiển thị, thời gian của game sẽ dừng lại, và khi chúng ta muốn tiếp tục trò chơi của mình, thì thời gian của game mới tiếp tục. Để làm được điều đó, mình khai báo thêm 2 biến pause và resume với giá trị khởi tạo là false và thực hiện việc thay đổi các giá trị này trong hàm showDialogNewGame(). Để có thể sử dụng 2 biến pause và resume tại các lớp khác để xử lý, mình khởi tạo thêm getter và setter cho 2 biến này.

private boolean pause=false;
private boolean resume= false;

public boolean isResume() {
        return resume;
}

public void setResume(boolean resume) {
        this.resume = resume;
}
        
public boolean isPause() {
        return pause;
}

public void setPause(boolean pause) {
        this.pause = pause;
}   

public void showDialogNewGame(String message, String title, int t) {
    pause=true;
    resume=false;
	int select = JOptionPane.showOptionDialog(null, message, title,
	JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
				null, null);
	if (select == 0) {
        pause=false;
		newGame();
	} else {
		   if(t==1){
                System.exit(0);
           }else{
                resume=true;
           }
	}
}

Việc thay đổi trong hàm showDialogNewGame() đơn giản là mỗi khi gọi đến hàm này, để thời gian dừng lại, mình thay đổi biến pause về giá trị true resume về giá trị false. Nếu hàm showDialogNewGame() được gọi từ nút New Game, việc chọn No để quay lại game sẽ tiếp tục thời gian đã bị dừng lại của mình. Để làm được điều đó, mình thay đổi giá trị resume thành true, khi đó, thời gian sẽ tiếp tục; Khi chọn Yes để tạo game mới, biến pause của mình khi này đang có giá trị true ngay khi hộp thoại xuất hiện, vậy nên để sau khi khởi tạo lại, thời gian của game chạy bình thường, mình đặt lại giá trị của biến pause về false.

Tiếp theo, để hoàn thiện việc xử lý thời gian của game, cũng như để thay đổi giá trị của time, tại hàm Main, mình tạo class Time extends từ lớp Thread.

MainFrame frame;
class Time extends Thread {
	public void run() {
		while (true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}	
            if(frame.isPause()){
                    if(frame.isResume()){
                            frame.time--;
                    }
            }else{
                    frame.time--;
            }
			if (frame.time == 0) {
				frame.showDialogNewGame(
					"Full time\nDo you want play again?", "Lose",1);
			}
		}
	}
}

class Time chính là phần xử lý chính của biến time, cũng là phần xử lý chính đối với thời gian của game. Trong class Time này, mình cập nhập liên tục giá trị biến time của lớp MainFrame mỗi giây. Trong khi cập nhập, kiểm tra biến pause của lớp MainFrame bằng hàm isPause() để kiểm tra xem game có đang tạm dừng không. Nếu game tạm dừng, time sẽ không thay đổi cho đến khi mình kiểm tra game đã được tiếp tục bằng hàm isResume(). Tại phần cập nhập này, time sẽ giảm đi 1 mỗi giây. Nếu time=0, mình sẽ gọi đến hàm showDialogNewGame() của lớp MainFrame để thông báo cho người chơi rằng họ đã thua vì thời gian đã hết, và hỏi xem có muốn chơi lại không. Việc xử lý của hàm showDialogNewGame này mình đã nói ở trên. Và cuối cùng, khởi tạo class Time để thay đổi time và Thread của lớp MainFrame để thay đổi giá trị của progressBar trong phần khởi tạo của lớp Main(), sau đó để hai Thread này chạy ngay khi game được bắt đầu.

public Main() {
	frame = new MainFrame();
    Time time = new Time();
	time.start();
	new Thread(frame).start();               
}

Vậy là chúng ta đã hoàn thành giao diện và chức năng tính thời gian và nút New Game của game. Phần hoàn thiện chức năng tính điểm của game, chúng ta sẽ tìm hiểu ở phần ngay sau đây cùng với những xử lý cơ bản nhất đối với các Icon nhé!

2. Thao tác xử lý cơ bản với các Icon

Ở phần 1, chúng ta đã biết các Icon được khởi tạo trên game đồng thời với một ma trận matrix với các giá trị của từng ô trong matrix tương ứng là các giá trị của các Icon. Các Icon sẽ được xử lý dựa trên ma trận này, và phần xử lý ấy nói đơn giản sẽ gồm 2 bước: 

  • Bước 1: Tìm 2 Icon giống nhau (Cũng là tìm 2 Icon có giá trị bằng nhau). Chú ý rằng các Icon được lấy ra theo giá trị index, đó là giá trị của ô tại vị trí tương ứng của Icon trong ma trận matrix. Các bạn có thể đọc lại phần 1 để rõ hơn về điều này.
  • Bước 2: Kiểm tra xem 2 Icon có nằm trên vị trí có thể kết nối được với nhau hay không. Các bạn nào chơi game Pokemon rồi sẽ đều biết để 2 Icon có thể kết nối được với nhau, 2 Icon đó phải thỏa mãn về điều kiện của đường đi. Đây là phần khó nhất, đồng thời cũng là thuật toán chính của toàn bộ bài toán, mình sẽ dành riêng 1 phần để nói về phần này ở phần sau nhé.

Ở phần 2 này, mình sẽ cùng các bạn tìm hiểu những xử lý cơ bản nhất đối với các Icon, đó là tìm được 2 Icon giống nhau, và xóa chúng đi không quan tâm đến vị trí của chúng. Đây cũng là những xử lý đầu tiên mà khi mình thực hiện code game cũng đã bắt đầu với những điều cơ bản này.

Để bắt đầu, mình tạo thêm 1 class PointLine trong packpage controller. Như tại phần 1, mỗi Icon mình coi là một Point với 2 tọa độ x,y, class PointLine này được tạo ra để định nghĩa việc kết nối 2 Icon với nhau. Cụ thể, một PointLine sẽ gồm hai Point p1, p2, tương ứng là hai Icon trong game.

package controller;

import java.awt.Point;


public class PointLine {
	public Point p1;
	public Point p2;

	public PointLine(Point p1, Point p2) {
		super();
		this.p1 = p1;
		this.p2 = p2;
	}
}

Tiếp theo, tại class Controller, mình viết thêm 1 hàm checkTwoPoint(Point p1, Point p2) với 2 tham số nhận vào là hai Point p1, p2 để kiểm tra giá trị của 2 Point này. Bởi vì mục đích của mình hiện tại đơn giản chỉ là kiểm tra 2 Icon giống nhau, nên mình chỉ cần kiểm tra 2 điều kiện, đó là: p1 và p2 không được trùng nhau (Nghĩa là không cùng chọn vào 1 Icon), và giá trị của 2 Icon tương ứng tại ma trận là bằng nhau.

public PointLine checkTwoPoint(Point p1, Point p2) {
    if (!p1.equals(p2) && matrix[p1.x][p1.y] == matrix[p2.x][p2.y]) {
        return new PointLine(p1, p2);
    }
    return null;
}

Khi đó, 2 Icon được kiểm tra nếu thỏa mãn điều kiện trên sẽ trả về 1 PointLine mới của 2 Point ứng với 2 Icon đó, còn nếu không thỏa mãn, sẽ trả về null.

Tiếp theo để có thể xóa đi 2 Icon giống nhau, mình tiếp tục viết thêm vào class ButtonEvent của packpage controller. Đầu tiên, mình tạo ra 2 biến Point p1, p2 với giá trị khởi tạo là null, 1 biến PointLine line, 1 biến int score=0 để tính điểm và 1 biến int item với giá trị khởi tạo là row*col/2.  Bởi vì các Icon được khởi tạo theo cặp, và khi biến mất, chúng cũng biến mất theo cặp, nên mình sẽ xử lý các Icon này theo các cặp Icon với tổng số cặp Icon chính là item. Ngắn gọn hơn, item chính là tổng số cặp Icon của chúng ta.

private Point p1 = null;
private Point p2 = null;      
private PointLine line;
private int score = 0;
private int item;
public ButtonEvent(MainFrame frame, int row, int col) {
        ... 
	item = row * col / 2;
	...	
}

Tiếp theo, mình tạo thêm 2 hàm excute()setDisable() để thực hiện việc xóa đi các Icon. Trong đó, tại hàm setDisable(), các Icon tương ứng trên các Button sẽ bị xóa đi, các Button ấy sẽ bị vô hiệu hóa và đồng thời background sẽ trả về background của cả khối Panel được tạo ra. Còn hàm Excute() sẽ nhận 2 tham số truyền vào là hai Point p1, p2, để setDisable() cho các nút tương ứng với 2 Point này.

public void execute(Point p1, Point p2) {
	System.out.println("delete");
	setDisable(btn[p1.x][p1.y]);
	setDisable(btn[p2.x][p2.y]);
}

private void setDisable(JButton btn) {
	btn.setIcon(null);
	btn.setBackground(backGroundColor);
	btn.setEnabled(false);
}

Cuối cùng, mình can thiệp vào phần override của phương thức actionPerformed(ActionEvent e) để xử lý Event của các Button tương ứng với các Icon được tạo ra. Khi chúng ta click vào 1 Icon, hàm e.getActionCommand() sẽ trả về tọa độ của Icon mà chúng ta vừa click vào. Mình lưu tọa độ ấy vào 1 biến String là btnIndex, sau đó mình tách tọa độ ấy ra qua dấu "," rồi lần lượt lưu vào 2 biến và y.

String btnIndex = e.getActionCommand();
int indexDot = btnIndex.lastIndexOf(",");
int x = Integer.parseInt(btnIndex.substring(0, indexDot));
int y = Integer.parseInt(btnIndex.substring(indexDot + 1,
		btnIndex.length()));

Tiếp theo, kiểm tra biến p1. Chú ý rằng chúng ta đang xét các Icon theo cặp, nghĩa là chúng ta sẽ xét lần lượt 2 Icon được chọn. Nếu p1=null, nghĩa là chưa có Icon nào được chọn, Icon mình đang click vào là Icon đầu tiên của cặp Icon mình đang xét. Khi đó, mình gán giá trị cho p1 bằng một Point mới với tọa độ x,y vừa lấy từ Button được chọn và p1 chính là Icon với tọa độ x,y đó. Đồng thời, mình cũng tạo hiệu ứng để thể hiện Icon đang được chọn bằng cách setBorder() cho Button tương ứng của Icon ấy.

if (p1 == null) {
	p1 = new Point(x, y);
	btn[p1.x][p1.y].setBorder(new LineBorder(Color.red));
}

Vậy nếu p1 không bằng null thì sao? Điều đó có nghĩa là đã có 1 Icon được chọn rồi, và Icon ở Button mà mình đang click vào, chính là Icon còn lại trong cặp Icon mà mình đang xét. Khi đó, mình sẽ tạo ra 1 Point mới với tọa độ x,y lấy từ Button được chọn và gán giá trị đó vào p2. Mình gọi hàm checkTwoPoint() từ lớp Controller để kiểm tra 2 điểm p1, p2 và gán giá trị trả về của hàm checkTwoPoint(p1, p2)  vào biến line được khởi tạo ở trên. Nếu line không bằng null, nghĩa là 2 điểm này thỏa mãn điều kiện kiểm tra, nói cách khác, 2 Icon được chọn là 2 Icon giống nhau. Khi đó, mình sẽ gọi hàm excute() với 2 Button tương ứng với 2 điểm p1, p2; đồng thời, mình cũng đặt giá trị của những phần tử trong ma trận tương ứng với 2 điểm p1, p2 này về 0. Trong ma trận của mình, 0 sẽ thể hiện cho các ô trống, đồng thời cũng là các Icon bị mất đi. Đây là 1 phần rất quan trọng để xử lý thuật toán về vị trí của các Icon mà mình sẽ giới thiệu với các bạn ở phần sau. Sau khi xóa thành công 2 Icon được chọn, line sẽ được trả về null, điểm số của chúng ta sẽ được tăng thêm 10 điểm, số cặp Icon là item sẽ trừ đi 1, thời gian time trong lớp MainFrame sẽ được cộng thêm 1 để là phần thưởng cùng với score và mình sẽ hiển thị lại số điểm trên lbScore của lớp MainFrame chính là giá trị của biến score của mình.

line = controller.checkTwoPoint(p1, p2);
if (line != null) {
	System.out.println("line != null");
	controller.getMatrix()[p1.x][p1.y] = 0;
	controller.getMatrix()[p2.x][p2.y] = 0;
	controller.showMatrix();
	execute(p1, p2);
	line = null;
	score += 10;
	item--;
	frame.time++;
	frame.lbScore.setText(score + "");
}

Sau khi kiểm tra 2 Button ứng với 2 điểm p1, p2, mình sẽ đặt lại Border của Button ứng với điểm p1 mà mình đã thêm Border ở trên về null. Nếu trường hợp 2 Icon được chọn không giống nhau, hoặc chúng ta chọn cùng 1 Icon hai lần, thì chúng ta cần có 1 tín hiệu rằng mình đã chọn sai, việc kiểm tra 2 Icon được chọn đã kết thúc và tiếp theo sẽ là một cặp 2 Icon mới, đúng không? Đây chính là tín hiệu đó. Đồng thời, việc này sẽ giúp người chơi luôn biết mình đang chọn Icon nào, để tìm Icon tương tự. Sau đó, các điểm p1, p2 sẽ được trả về null để tiếp tục với các cặp Icon mới. Cuối cùng, mình kiểm tra số lượng cặp Icon còn lại bằng biến item. Nếu item=0, nghĩa là không còn Icon nào cả, mình sẽ gọi hàm showDialogNewGame() của lớp MainFrame để thông báo chiến thắng đến người chơi. Dưới đây là toàn bộ code phần override hàm actionPerformed() của mình, các bạn có thể theo dõi để hiểu hơn:

@Override
public void actionPerformed(ActionEvent e) {
    String btnIndex = e.getActionCommand();
	int indexDot = btnIndex.lastIndexOf(",");
	int x = Integer.parseInt(btnIndex.substring(0, indexDot));
	int y = Integer.parseInt(btnIndex.substring(indexDot + 1,
			btnIndex.length()));
	if (p1 == null) {
		p1 = new Point(x, y);
		btn[p1.x][p1.y].setBorder(new LineBorder(Color.red));
	} else {
		p2 = new Point(x, y);
		System.out.println("(" + p1.x + "," + p1.y + ")" + " --> " + "("
				+ p2.x + "," + p2.y + ")");
		line = controller.checkTwoPoint(p1, p2);
		if (line != null) {
			System.out.println("line != null");
			controller.getMatrix()[p1.x][p1.y] = 0;
			controller.getMatrix()[p2.x][p2.y] = 0;
			controller.showMatrix();
			execute(p1, p2);
			line = null;
			score += 10;
			item--;
			frame.time++;
			frame.lbScore.setText(score + "");
		}
		btn[p1.x][p1.y].setBorder(null);
		p1 = null;
		p2 = null;
		System.out.println("done");
		if (item == 0) {
		    frame.showDialogNewGame(
				"You are winer!\nDo you want play again?", "Win",1);
		}
	}
}

Sau khi hoàn thành tất cả, chúng ta hoàn toàn có thể có 1 game giải trí nhẹ nhàng với việc nhanh mắt nhanh tay như sau. Các bạn hãy cứ tưởng tượng như đang chơi 1 game Pikachu phiên bản hack ấy :D Click không cần nghĩ luôn.

Tạm kết

Như vậy, ở phần 2 này, chúng ta đã hoàn thành giao diện, các chức năng của game và xử lý được việc xóa đi các Icon giống nhau chưa xét đến vị trí. Ở phần sau, mình sẽ cùng các bạn hoàn thiện thuật toán chính của game, cũng là việc xét đến vị trí của các Icon, tìm đường đi của chúng để kiểm tra xem 2 Icon có thỏa mãn điều kiện về đường đi hay không.

Cảm ơn các bạn đã đọc bài viết của mình. Mọi ý kiến, thắc mắc các bạn hãy để lại ở phần Comment nhé. Mình sẽ đọc và rep đầy đủ :)

Tham khảo:

- Phần 1: https://codelearn.io/blog/view/lam-game-sieu-xin-bang-java-phan-1

- Code của mình và hình ảnh mình sử dụng: https://github.com/beloyten/codephan2