Java 实现2048游戏之详细教程
一、整体项目结构
使用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(); } }
三、运行结果