一、整體項目結構
使用Maven來管理項目結構
二、基本功能實現
(一)創建游戲窗口(靜態)
package com.baidu.czy; import java.applet.Applet; import java.applet.AudioClip; import java.awt.*; import java.awt.event.ActionEvent; import java.io.File; import java.net.URI; import java.net.URL; import javax.swing.*; //本類繼承自JFrame,創建游戲窗口,只需要new本類對象 public class GameStart extends JFrame { File f; URI uri; URL url; private ActionEvent e; // 添加背景音樂 public GameStart() { try { f = new File("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\bg1.wav"); uri = f.toURI(); url = uri.toURL(); //解析地址 AudioClip aau; aau = Applet.newAudioClip(url); aau.loop(); //循環播放 } catch (Exception e) { e.printStackTrace(); } } private static final long serialVersionUID = 1L; //用於存放數據的二維數組,構成4*4網格的游戲界面數值,數組中的值就是其對應位置方格的值,0代表無值 private int Numbers[][] = new int[4][4]; public void init() { this.setTitle("2048游戲"); this.setLocation(450, 100); this.setSize(800, 600); //自定義,不使用面板布局格式 this.setLayout(null); //添加標簽 JLabel jLabel = new JLabel("歡迎來到2048游戲!"); jLabel.setFont(new Font("華文行楷", Font.CENTER_BASELINE, 40)); jLabel.setForeground(new Color(0X0000FF)); jLabel.setBounds(20, 500, 400, 50); this.add(jLabel); //添加圖片1( Public ImageIcon(String filename)//參數可以是絕對路徑也可以是相對路徑 ) JLabel jLabel1 = new JLabel(new ImageIcon("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\c.png")); jLabel1.setBounds(400, 5, 400, 600); this.add(jLabel1); //添加圖片2 JLabel jLabel13 = new JLabel(new ImageIcon("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\d.png")); jLabel13.setBounds(1, 80, 400, 600); this.add(jLabel13); // 開始游戲按鈕 ImageIcon imgicon = new ImageIcon("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\start.png"); JButton start = new JButton(imgicon); start.setFocusable(false);//設置此按鈕不可獲取焦點 start.setBorderPainted(false);//設置此按鈕沒有邊框 start.setFocusPainted(false);//設置不繪制邊框,設置 paintFocus屬性,對於要繪制的焦點狀態,該屬性必須為 true。paintFocus 屬性的默認值為 true。一些外觀沒有繪制焦點狀態;它們將忽略此屬性 start.setContentAreaFilled(false);//設置不繪制邊框,設置 contentAreaFilled 屬性。如果該屬性為 true,則按鈕將繪制內容區域。如果希望有一個透明的按鈕,比如只是一個圖標的按鈕,那么應該將此屬性設置為 false。不要調用 setOpaque(false)。contentAreaFilled 屬性的默認值為 true。 start.setBounds(5, 10, 120, 30);// 設置按鈕的x,y坐標位置和寬度與高度 this.add(start); //后退一步按鈕 ImageIcon backicon = new ImageIcon("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\backicon.png"); JButton back = new JButton(backicon); back.setFocusable(false); back.setBorderPainted(false); back.setFocusPainted(false); back.setContentAreaFilled(false); back.setBounds(270, 10, 120, 30);// 設置按鈕的x,y坐標位置和寬度與高度 this.add(back); // 關於按鈕 ImageIcon imgicon2 = new ImageIcon("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\about.png"); JButton about = new JButton(imgicon2); about.setFocusable(false); about.setBorderPainted(false); about.setFocusPainted(false); about.setContentAreaFilled(false); about.setBounds(160, 10, 70, 30); this.add(about); // 分數顯示 JLabel scoreLabel = new JLabel("分數:0"); scoreLabel.setBounds(5, 45, 120, 30); scoreLabel.setFont(new Font("幼圓", Font.CENTER_BASELINE, 18)); scoreLabel.setForeground(new Color(0x000000)); this.add(scoreLabel); //靜音按鈕 JCheckBox isSoundBox = new JCheckBox("靜音"); isSoundBox.setBounds(320, 45, 120, 30); isSoundBox.setFont(new Font("黑體", Font.CENTER_BASELINE, 18)); isSoundBox.setFocusable(false); isSoundBox.setBorderPainted(false); isSoundBox.setFocusPainted(false); isSoundBox.setContentAreaFilled(false); this.add(isSoundBox); this.setDefaultCloseOperation(EXIT_ON_CLOSE);//使用 System exit 方法退出應用程序。僅在應用程序中使用。 this.setResizable(false); //設置此窗體是否可由用戶調整大小。 this.setVisible(true);// 顯示界面 // 創建事件處理類,將事件源綁定監聽器 ComponentListener cl = new ComponentListener(this, Numbers, scoreLabel, start, about, back, isSoundBox); start.addActionListener(cl); about.addActionListener(cl); back.addActionListener(cl); isSoundBox.addActionListener(cl); this.addKeyListener(cl); } // 重寫窗體 @Override //paint是Java中AWT畫圖方法 public void paint(Graphics g) { //調用父類的構造方法 super.paint(g); //設置畫筆顏色 g.setColor(new Color(0x66FF66)); //填充整個4*4圓角矩形區域,使用當前顏色填充指定的圓角矩形 g.fillRoundRect(15, 110, 370, 370, 15, 15);// 大矩形框 g.setColor(new Color(0xFFFAFA)); for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { //填充每一個4*4小方格區域 g.fillRoundRect(25 + i * 90, 120 + j * 90, 80, 80, 15, 15);// 小矩形框 } } // 調整數字的位置並上色 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { //如果小方格上數字不為0,則說明有值,進行繪制背景色與數字 if (Numbers[j][i] != 0) { int FontSize = 30; int MoveX = 0, MoveY = 0; switch (Numbers[j][i]) { case 2: g.setColor(new Color(0xFF0000)); FontSize = 30; MoveX = 0; MoveY = 0; break; case 4: g.setColor(new Color(0xede0c8)); FontSize = 30; MoveX = 0; MoveY = 0; break; case 8: g.setColor(new Color(0xf2b179)); FontSize = 30; MoveX = 0; MoveY = 0; break; case 16: g.setColor(new Color(0xf59563)); FontSize = 29; MoveX = -5; MoveY = 0; break; case 32: g.setColor(new Color(0xf67c5f)); FontSize = 29; MoveX = -5; MoveY = 0; break; case 64: g.setColor(new Color(0xf65e3b)); FontSize = 29; MoveX = -5; MoveY = 0; break; case 128: g.setColor(new Color(0xedcf72)); FontSize = 28; MoveX = -10; MoveY = 0; break; case 256: g.setColor(new Color(0xedcc61)); FontSize = 28; MoveX = -10; MoveY = 0; break; case 512: g.setColor(new Color(0xedc850)); FontSize = 28; MoveX = -10; MoveY = 0; break; case 1024: g.setColor(new Color(0xedc53f)); FontSize = 27; MoveX = -15; MoveY = 0; break; case 2048: g.setColor(new Color(0xedc22e)); FontSize = 27; MoveX = -15; MoveY = 0; break; default: g.setColor(new Color(0x000000)); break; } //數字不為0的小方格覆蓋原色,根據不同的值上不同的色 g.fillRoundRect(25 + i * 90, 120 + j * 90, 80, 80, 15, 15);// 小矩形框覆蓋上色 g.setColor(new Color(0x000000)); g.setFont(new Font("Kristen ITC", Font.PLAIN, FontSize)); //繪制字符串,參數分別為:要繪制的字符串,字符串繪制的x坐標,y坐標 g.drawString(Numbers[j][i] + "", 25 + i * 90 + 30 + MoveX, 120 + j * 90 + 50 + MoveY); } } } } }
(二)實現監聽(具體功能的實現)
package com.baidu.czy; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.Random; import javax.swing.*; public class ComponentListener extends KeyAdapter implements ActionListener { private GameStart UI;// 界面對象 private int Numbers[][];// 存放數據的數組 private Random rand = new Random(); private int BackUp[][] = new int[4][4];//用於備份數組,供回退時使用 private int BackUp2[][] = new int[4][4];//用於備份數組,供起死回生時使用 public JLabel lb; //分數標簽 int score = 0; int tempscore, tempscore2;//記錄回退的分數值 public JButton bt, about, back; public JCheckBox isSoundBox; //是否勝利,true:勝利,false:失敗 private boolean isWin = false; //是否復活,true:使用復活,false:不使用復活 private boolean relive = false; //是否可以回退,true:不可回退,false:可以回退 (是否已經進行過一次回退了) private boolean hasBack = false; //是否播放音樂,true:播放音效,false:不播放音效 private boolean isSound = true; //事件 private ActionEvent e; public ComponentListener(GameStart UI, int[][] Numbers, JLabel lb, JButton bt, JButton about, JButton back, JCheckBox isSoundBox) { this.UI = UI; this.Numbers = Numbers; this.lb = lb; this.bt = bt; this.about = about; this.back = back; this.isSoundBox = isSoundBox; } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == bt ) { //游戲開始 isWin = false; //各個小格賦初值0 for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) Numbers[i][j] = 0; //游戲開始,分數為0 score = 0; lb.setText("分數:" + score); //生成4個0-3之間的隨機數 int r1 = rand.nextInt(4); int r2 = rand.nextInt(4); int c1 = rand.nextInt(4); int c2 = rand.nextInt(4); //由r1,c1;r2,c2組成兩個初始值,所以初始值的坐標不能重復 while (r1 == r2 && c1 == c2) { r2 = rand.nextInt(4); c2 = rand.nextInt(4); } // 生成初始數字(2或者4) int value1 = rand.nextInt(2) * 2 + 2; int value2 = rand.nextInt(2) * 2 + 2; // 把數字存進對應的位置 Numbers[r1][c1] = value1; Numbers[r2][c2] = value2; //數字更改,重新繪制圖形,為此組件創建圖形上下文 UI.paint(UI.getGraphics()); } else if (e.getSource() == about) { //點擊了關於標簽 JOptionPane.showMessageDialog(UI, "游戲規則:\n" + "1、開始時棋盤內隨機出現兩個數字,出現的數字僅可能為2或4\n" + "2、玩家可以選擇上下左右四個方向,若棋盤內的數字出現位移或合並,視為有效移動\n" + "3、玩家選擇的方向上若有相同的數字則合並,每次有效移動可以同時合並,但不可以連續合並\n" + "4、合並所得的所有新生成數字相加即為該步的有效得分\n" + "5、玩家選擇的方向行或列前方有空格則出現位移\n" + "6、每有效移動一步,棋盤的空位(無數字處)隨機出現一個數字(依然可能為2或4)\n" + "7、棋盤被數字填滿,無法進行有效移動,判負,游戲結束\n" + "8、棋盤上出現2048,判勝,游戲結束。\n" ); } else if (e.getSource() == back && hasBack == false) { System.out.println("回退"); //點擊了回退一步標簽,而且只能回退一次,只有再執行一次上下左右操作才可以再次回退 hasBack = true; //判斷本次回退是回退上一步,還是復活,回退上上步 if (relive == false) { //替換上一步的分數 score = tempscore; lb.setText("分數:" + score); for (int i = 0; i < BackUp.length; i++) { Numbers[i] = Arrays.copyOf(BackUp[i], BackUp[i].length); } } else { //選擇了起死回生 score = tempscore2; lb.setText("分數:" + score); for (int i = 0; i < BackUp2.length; i++) { Numbers[i] = Arrays.copyOf(BackUp2[i], BackUp2[i].length); } //再給一次復活的機會 relive = false; } //重新繪制 UI.paint(UI.getGraphics()); } else if (e.getSource().equals(isSoundBox)) { //是否選中靜音復選框 if (isSoundBox.isSelected()) { isSound = false; } else { isSound = true; } } } // 鍵盤監聽,監聽游戲焦點的←,↑,→,↓;方向鍵鍵值:左:37上:38右:39下:40 public void keyPressed(KeyEvent event) { int Counter = 0;// 記錄數字有效移動位數,判斷是否移動了 int NumCounter = 0;// 記錄當前有數字的小方格數量,判斷是否已滿 int NumNearCounter = 0;// 記錄相鄰格子數字相同的對數 hasBack = false; //每次進行真正的移位合並操作之前,記錄前一步 //記錄上上步 if (BackUp != null || BackUp.length != 0) { tempscore2 = tempscore;// 先把分數備份好 // 下面的for循環調用java.util.Arrays.copyOf()方法復制數組,實現備份 for (int i = 0; i < BackUp.length; i++) { BackUp2[i] = Arrays.copyOf(BackUp[i], BackUp[i].length); } } //記錄上步 tempscore = score;// 先把分數備份好 // 下面的for循環調用java.util.Arrays.copyOf()方法復制數組,實現備份 for (int i = 0; i < Numbers.length; i++) { BackUp[i] = Arrays.copyOf(Numbers[i], Numbers[i].length); } if (isWin == false) { switch (event.getKeyCode()) { case 37: // 向左移動 /* (1)在移動的過程中,判斷與其相鄰的格子,如果相鄰的格子為空,則移動,並將當前的格子清0 (2)移動后,若格子相鄰並且數值相等,則求和並且清0 */ if (isSound == true) { new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\move.wav").start();// 播放移位音樂 } //經過這個循環,把每行有值的格子,都被搬到最左邊了,同一行右側有值的格子,覆蓋左側值為0的格子 for (int h = 0; h < 4; h++) for (int l = 0; l < 4; l++) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; //per相當於是相鄰位置 int pre = l - 1; while (pre >= 0 && Numbers[h][pre] == 0) { Numbers[h][pre] = temp; //移動后清0 Numbers[h][pre + 1] = 0; pre--; Counter++; } } //表盤當前左側相鄰相等的值會相加,造成左邊值為【和】,相鄰右邊值為【0】 for (int h = 0; h < 4; h++) for (int l = 0; l < 4; l++) if (l + 1 < 4 && (Numbers[h][l] == Numbers[h][l + 1]) //相鄰兩列數值相加,並要求兩列不同時為0 && (Numbers[h][l] != 0 || Numbers[h][l + 1] != 0)) { if (isSound == true) new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\merge.wav").start(); //數值合並 Numbers[h][l] = Numbers[h][l] + Numbers[h][l + 1]; Numbers[h][l + 1] = 0; Counter++; score += Numbers[h][l]; if (Numbers[h][l] == 2048) { isWin = true; } } //經過這個循環,把每行有值的格子,都被搬到最左邊了,同一行右側有值的格子,覆蓋左側值為0的格子 for (int h = 0; h < 4; h++) for (int l = 0; l < 4; l++) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; int pre = l - 1; while (pre >= 0 && Numbers[h][pre] == 0) { Numbers[h][pre] = temp; Numbers[h][pre + 1] = 0; pre--; Counter++; } } break; case 39:// 向右移動 if (isSound == true) new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\move.wav").start(); for (int h = 3; h >= 0; h--) for (int l = 3; l >= 0; l--) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; int pre = l + 1; while (pre <= 3 && Numbers[h][pre] == 0) { Numbers[h][pre] = temp; Numbers[h][pre - 1] = 0; pre++; Counter++; } } for (int h = 3; h >= 0; h--) for (int l = 3; l >= 0; l--) if (l + 1 < 4 && (Numbers[h][l] == Numbers[h][l + 1]) && (Numbers[h][l] != 0 || Numbers[h][l + 1] != 0)) { if (isSound == true) new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\merge.wav").start(); Numbers[h][l + 1] = Numbers[h][l] + Numbers[h][l + 1]; Numbers[h][l] = 0; Counter++; score += Numbers[h][l + 1]; if (Numbers[h][l + 1] == 2048) { isWin = true; } } for (int h = 3; h >= 0; h--) for (int l = 3; l >= 0; l--) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; int pre = l + 1; while (pre <= 3 && Numbers[h][pre] == 0) { Numbers[h][pre] = temp; Numbers[h][pre - 1] = 0; pre++; Counter++; } } break; case 38: // 向上移動 if (isSound == true) new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\move.wav").start(); for (int l = 0; l < 4; l++) for (int h = 0; h < 4; h++) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; int pre = h - 1; while (pre >= 0 && Numbers[pre][l] == 0) { Numbers[pre][l] = temp; Numbers[pre + 1][l] = 0; pre--; Counter++; } } for (int l = 0; l < 4; l++) for (int h = 0; h < 4; h++) if (h + 1 < 4 && (Numbers[h][l] == Numbers[h + 1][l]) && (Numbers[h][l] != 0 || Numbers[h + 1][l] != 0)) { if (isSound == true) new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\merge.wav").start(); Numbers[h][l] = Numbers[h][l] + Numbers[h + 1][l]; Numbers[h + 1][l] = 0; Counter++; score += Numbers[h][l]; if (Numbers[h][l] == 2048) { isWin = true; } } for (int l = 0; l < 4; l++) for (int h = 0; h < 4; h++) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; int pre = h - 1; while (pre >= 0 && Numbers[pre][l] == 0) { Numbers[pre][l] = temp; Numbers[pre + 1][l] = 0; pre--; Counter++; } } break; case 40: // 向下移動 if (isSound == true) new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\move.wav").start(); for (int l = 3; l >= 0; l--) for (int h = 3; h >= 0; h--) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; int pre = h + 1; while (pre <= 3 && Numbers[pre][l] == 0) { Numbers[pre][l] = temp; Numbers[pre - 1][l] = 0; pre++; Counter++; } } for (int l = 3; l >= 0; l--) for (int h = 3; h >= 0; h--) if (h + 1 < 4 && (Numbers[h][l] == Numbers[h + 1][l]) && (Numbers[h][l] != 0 || Numbers[h + 1][l] != 0)) { if (isSound == true) new PlaySound("E:\\JavaProject\\HandleGame2048(2)\\src\\main\\resources\\res\\merge.wav").start(); Numbers[h + 1][l] = Numbers[h][l] + Numbers[h + 1][l]; Numbers[h][l] = 0; Counter++; score += Numbers[h + 1][l]; if (Numbers[h + 1][l] == 2048) { isWin = true; } } for (int l = 3; l >= 0; l--) for (int h = 3; h >= 0; h--) if (Numbers[h][l] != 0) { int temp = Numbers[h][l]; int pre = h + 1; while (pre <= 3 && Numbers[pre][l] == 0) { Numbers[pre][l] = temp; Numbers[pre - 1][l] = 0; pre++; Counter++; } } break; } //移位,合並,移位完成后,判斷是否有可重復值 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { //判斷相鄰左右位置有沒有重復值 if (Numbers[i][j] == Numbers[i][j + 1] && Numbers[i][j] != 0) { NumNearCounter++; } //判斷相鄰上下位置有沒有重復值 if (Numbers[i][j] == Numbers[i + 1][j] && Numbers[i][j] != 0) { NumNearCounter++; } if (Numbers[3][j] == Numbers[3][j + 1]//第四行只需要判斷是否與右邊有重復 && Numbers[3][j] != 0) { NumNearCounter++; } if (Numbers[i][3] == Numbers[i + 1][3]//第四列只需要判斷與下邊是否有重復 && Numbers[i][3] != 0) { NumNearCounter++; } } } //判斷不為0的空余格子數 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (Numbers[i][j] != 0) { NumCounter++; } } } System.out.println(Counter); //有效移位數>0,則補充一個新的2或者4 if (Counter > 0) { lb.setText("分數:" + score); //隨機產生0~3的數字,選中位置 int r1 = rand.nextInt(4); int c1 = rand.nextInt(4); while (Numbers[r1][c1] != 0) { r1 = rand.nextInt(4); c1 = rand.nextInt(4); } //產生2或4 int value1 = rand.nextInt(2) * 2 + 2; Numbers[r1][c1] = value1; } if (isWin == true) { UI.paint(UI.getGraphics()); JOptionPane.showMessageDialog(UI, "恭喜你贏了!\n您的最終得分為:" + score); } if (NumCounter == 16 && NumNearCounter == 0) { //移動后滿格並且沒有可合並的小格子,游戲結束relive:復活一次 relive = true; JOptionPane.showMessageDialog(UI, "沒地方可以合並咯!!" + "\n很遺憾,您輸了~>_<~" + "\n悄悄告訴你,游戲有起死回生功能哦,不信你“退一步”試試?" + "\n說不定能扭轉乾坤捏 (^_~)"); } UI.paint(UI.getGraphics()); } } }
(三)實現線程播放音樂
package com.baidu.czy; import java.io.File; import java.io.IOException; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.SourceDataLine; //播放聲音的線程 public class PlaySound extends Thread { private String filename; public PlaySound(String wavfile) { filename = "" + wavfile; } public void run() { File soundFile = new File(filename); AudioInputStream audioInputStream = null; try { //獲得音頻輸入流 audioInputStream = AudioSystem.getAudioInputStream(soundFile); } catch (Exception e1) { e1.printStackTrace(); return; } //指定聲音流中特定數據安排 AudioFormat format = audioInputStream.getFormat(); SourceDataLine auline = null; DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); try { //從混頻器獲得源數據行 auline = (SourceDataLine) AudioSystem.getLine(info); //打開具有指定格式的行,這樣可使行獲得所有所需的系統資源並變得可操作。 auline.open(format); } catch (Exception e) { e.printStackTrace(); return; } //允許數據行執行數據 I/O auline.start(); int nBytesRead = 0; // 這是緩沖 byte[] abData = new byte[512]; try { while (nBytesRead != -1) { //從音頻流讀取指定的最大數量的數據字節,並將其放入給定的字節數組中 nBytesRead = audioInputStream.read(abData, 0, abData.length); if (nBytesRead >= 0) //通過此源數據行將音頻數據寫入混頻器 auline.write(abData, 0, nBytesRead); } } catch (IOException e) { e.printStackTrace(); return; } finally { auline.drain(); auline.close(); } } }
(四)測試類
package com.baidu.czy.test; import com.baidu.czy.GameStart; public class StartGameTest { public static void main(String[] args) { GameStart view = new GameStart(); view.init(); new GameStart(); } }
三、運行結果