项目5 《贪吃蛇》游戏的设计与实现 5.1游 戏 简 介 《贪吃蛇》游戏是一种简单的、经典的大众游戏,自从推出以来,就因其操作简单、娱乐性强而深受广大计算机玩家的喜爱。该游戏的基本规则是利用方向键来改变蛇的运行方向; 空格键暂停或继续游戏; 在随机的地方产生食物; 蛇吃到食物身体就变长; 蛇碰到四周墙壁或自身则游戏结束。 5.2本项目的实训目的 通过本项目的训练,培养读者学会综合运用Java Swing图形用户界面编程中的相关知识(容器类(JFrame类、JPanel类)、布局管理器类、命令按钮类、文本框类、对话框类、Java事件处理机制和处理过程)解决实际问题的能力,使读者熟悉应用系统的开发过程,培养读者的独立思考能力,提高工程实践能力。 5.3本项目所用到的Java相关知识 本项目所用到的知识有JFrame类的使用、JPanel类的使用、paint()方法和repaint()方法、键盘监听事件的处理过程、javax.swing.Timer类的使用,这些内容在前面的几个项目中都有详细的介绍,这里不再详述。 5.4本项目的功能需求分析 本项目主要是完成《贪吃蛇》游戏的基本操作: 实现贪吃蛇的蛇身移动、随机生成食物、蛇吃到食物身体增长、碰到墙壁或自身结束游戏。所以本项目需要满足以下几点要求。 (1) 利用上、下、左、右方向键来改变蛇的运行方向。 (2) 空格键暂停或继续游戏,并在随机的地方产生食物。 (3) 吃到食物蛇身就增长一格,碰到墙壁或自身则游戏结束,否则正常运行。 《贪吃蛇》游戏的核心算法是如何实现移动和吃掉食物,没有碰到食物时,把当前运动方向上的下个节点作为蛇头节点,并以蛇节点的颜色绘制这个节点,然后把蛇尾节点删除,最后重绘屏幕,这样就可以达到蛇移动的效果。而在吃到食物时,则只需把食物节点作为头节点即可。 通过上述分析,本项目需要设计实现出如图51所示的游戏窗口。 图51《贪吃蛇》游戏窗口 5.5本项目的设计方案 根据上述需求分析可知,需要首先创建一个如图51所示的游戏主窗口,在该窗口中添加一个游戏面板,实现贪吃蛇的蛇身移动、随机生成食物、蛇吃到食物身体增长、碰到墙壁或自身结束游戏的效果。 5.6本项目的实现过程 要想完成本项目,需要分为两步: 首先要利用Java Swing提供的组件创建出如图51所示的静态窗口,然后搭建游戏墙,最后利用计时器和键盘事件实现游戏效果。 1. 搭建含有菜单的游戏主窗口 创建一个窗体类HappySnake(继承JFrame类),作为主程序(包含main()方法)。 代码如下: public class HappySnake extends JFrame { //构造方法:初始化游戏窗口 public HappySnake(){ this.setTitle("快乐的贪吃蛇-1.0版"); //设置窗体的标题 this.setSize(820,580); //设置窗体的大小 this.setLocationRelativeTo(null); //居中 this.setResizable(false); //窗口大小不允许改变 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //关闭窗口时, 立即关闭软件 this.setVisible(true); } public static void main(String[] args) { HappySnake f=new HappySnake(); } } 运行该程序,将会得到如图52所示的静态主窗口。 图52静态主窗口 2. 定义游戏面板类SnakePanel作为蛇的舞台,在该舞台上搭建游戏墙 该类用于实现游戏界面,在其中能及时画出游戏的过程,因此该类应继承JPanel类,并且重写paint()方法。 代码如下: public class SnakePanel extends JPanel { //游戏背景图片 BufferedImage bg; //舞台的列数 public static final int COLS = 40; //舞台的行数 public static final int ROWS = 25; //舞台格子的大小 public static final int CELL_SIZE = 20; //构造方法:窗口初始化 public SnakePanel() { try {//读取背景图片bgSnake.jpg给bg变量 bg = ImageIO.read(new File("bgSnake.jpg")); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //画出游戏墙上的网格线,行在y轴方向上,列在x轴方向上 public void paintWallGrid(Graphics g) { for (int i = 0; i < COLS; i++) { for (int j = 0; j <ROWS ; j++) { //每个小方格的宽和高是20 g.drawRect(i * 20, j * 20, 20, 20); } } } //重写paint()方法 public void paint(Graphics g) { super.paint(g); g.drawImage(bg, 0, 0, null); paintWallGrid(g); //画出游戏墙 } } 3. 向主窗口内添加一个游戏面板对象 (1) 在HappySnake类中定义游戏面板SnakePanel的对象。 SnakePanel spanel; //定义游戏面板对象 (2) 在构造方法中创建SnakePanel对象spanel。 spanel = new SnakePanel (); (3) 在构造方法中将spanel对象添加到主窗口中。 this.add(spanel); 这时运行程序会得到如图53所示的画有网格线的主窗口。 图53画有网格线的主窗口 4. 在游戏墙上画出初始静态的蛇 分析: 游戏墙可以看作一个由20行、10列的网格组成的二维数组,存储已经放下的小方块,行在y轴方向上,列在x轴方向上。其中,0表示空白,1表示有小方块。 (1) 定义Cell类,代表蛇身上的每个节点,该节点需要包含其在舞台上的位置和颜色信息。 public class Cell { private int x; private int y; private Color color; public Cell() { } public Cell(int x, int y) { this.x = x; this.y = y; color=Color.RED; //默认颜色为红色 } public Cell(int x,int y, Color color){ this.x = x; this.y = y; this.color = color; } public Color getColor() { return color; } public int getX() { return x; } public int getY() { return y; } } (2) 在游戏面板SnakePanel类中定义存储蛇身上节点的集合对象snake。 private LinkedList<Cell> snake = new LinkedList<Cell>(); (3) 在游戏面板SnakePanel类中定义init()方法,用来初始化蛇身并保存到snake集合对象中。 /*创建默认的蛇: (31,0)(32,0)(33,0)...(39,0) */ private void init(){ color = Color.RED; for(int x=31, y=0; x<40; x++){ snake.add(new Cell(x,y,color)); } } (4) 定义SnakePanel类的构造方法,在该构造方法中调用init()方法完成蛇的初始化。 public SnakePanel() { init(); } (5) 在SnakePanel类中定义paintSnake()方法,该方法用于画出当前蛇。 public void paintSnake(Graphics g) { //g.setColor(this.color); for (Cell cell : snake) { g.setColor(cell.getColor()); g.fillOval (cell.getX()*CELL_SIZE,cell.getY()*CELL_SIZE, CELL_SIZE-2, CELL_SIZE-2); }} (6) 修改paint()方法,在该方法中调用paintSnake()方法在舞台上画出初始的蛇身。 paintSnake(g); //画出蛇 这时运行程序会得到如图54所示的窗口。 图54画有网格线和初始蛇的主窗口 该窗口中虽然画出了蛇,但是当前蛇只是静止的。那么如何让蛇动起来呢? 5. 当前蛇向左移动 如果想让蛇定时向左移动,首先应该向snake集合的首部添加一个Cell节点,同时删除其尾部一个节点,然后在舞台上重画该蛇。与《俄罗斯方块》游戏一样,这里也利用javax.swing.Timer类实现当前蛇移动的效果。 (1) 在SnakePanel类中定义方法createHead(),用于生成蛇向左移动时的头节点。 private Cell createHead(){ Cell head = snake.getFirst(); int x = head.getX(); int y = head.getY(); x--; // return new Cell(x,y); } (2) 在SnakePanel类中定义方法creep(),用于当前蛇向左方向爬行一步。 public void creep(){ snake.removeLast(); snake.addFirst(createHead()); } (3) 在SnakePanel类中定义定时器变量。 Timer timer; (4) 在SnakePanel类中定义定时器监听类TimerListener(作为SnakePanel类的内部类),该类实现ActionListener接口。 class TimerListener implements ActionListener { public void actionPerformed(ActionEvent e) { creep(); //爬行一格 repaint(); //屏幕重绘 } } (5) 在SnakePanel类的构造方法中创建定时器,每隔0.5s触发一次。 timer = new Timer(500, new TimerListener()); (6) 启动定时器。 timer.start(); 此时运行该程序,发现该程序实现了当前蛇每隔0.5s向左移动一步的效果。但是这种移动只是计时器定时触发的,用户无法干预。 实际的游戏过程应该是用户能通过键盘方向键←、→、↑、↓控制蛇自由地向左、右、上、下移动。下面利用键盘监听接口来解决该问题。 6. 游戏规则的实现 规则1: 利用方向键←、→、↑、↓来改变蛇的运行方向。 分析: 如果要想用键盘方向键←、→、↑、↓来控制蛇的移动,首先游戏面板应实现键盘接口KeyListener,然后重写该接口中的keyPressed()方法,实现键盘的如下操作。 方向键↑: 蛇移动的方向变为向上移。 方向键↓: 蛇移动的方向变为向下移。 方向键←: 蛇移动的方向变为向左移。 方向键→: 蛇移动的方向变为向右移。 空格键: 暂停或继续游戏。 (1) 定义变量currentDirection和常量LEFT、RIGHT、UP、DOWN。 SnakePanel类实现键盘接口KeyListener。 public class SnakePanel extends JPanel implements KeyListener{ //其他代码省略 //重写keyPressed()方法 public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: if(currentDirection!=RIGHT) currentDirection=LEFT; repaint(); //屏幕重绘 break; case KeyEvent.VK_RIGHT: if(currentDirection!=LEFT) currentDirection=RIGHT; repaint(); //屏幕重绘 break; case KeyEvent.VK_UP: if(currentDirection!=DOWN) currentDirection=UP; repaint(); //屏幕重绘 break; case KeyEvent.VK_DOWN: if(currentDirection!=UP) currentDirection=DOWN; repaint(); //屏幕重绘 break; case KeyEvent.VK_SPACE: if(timer.isRunning()) timer.stop(); else timer.start(); default: break; } } public void keyTyped(KeyEvent e) { //TODO Auto-generated method stub } @Override public void keyReleased(KeyEvent e) { //TODO Auto-generated method stub } } (2) 修改createHead()方法,实现根据方向和当前的头节点,创建新的头节点的功能。 private Cell createHead(int direction){ Cell head = snake.getFirst(); int x = head.getX(); int y = head.getY(); switch (direction) { case DOWN: y++; break; case UP: y--; break; case RIGHT: x++; break; case LEFT: x--; break; } return new Cell(x,y); } (3) 修改creep()方法,实现按照蛇当前方向爬行一步的功能。 public void creep(){ snake.removeLast(); snake.addFirst(createHead(currentDirection)); } 这时,SnakePanel既是游戏面板,又是键盘监听类,只需要在主窗口的构造方法中添加键盘监听即可。代码如下: this.addKeyListener(spanel); 此时,运行该游戏程序,键盘的上、下、左、右方向键能操控当前蛇的移动方向了。 规则2: 在随机的地方产生食物。 分析: 利用Random类在舞台内部生成随机数x、y,将x、y构造成一个Cell对象作为食物,然后在舞台上将该食物画出即可。但在生成食物时要避开蛇的身体,即检查蛇身是否包含(x,y),如果蛇身包含(x,y),则重新生成 随机数x、y,直到蛇身不包含(x,y)为止。 (1) 定义contains()方法,实现判断蛇身是否包含(x,y)的功能。 public boolean contains(int x, int y) { return snake.contains(new Cell(x,y)); } (2) 重写Cell类的equals()方法。 因为集合中的contains()方法在判断当前集合是否包含某一元素时,需要调用equals()方法,所以应该重写Cell类的equals()方法。 public boolean equals(Object obj) { if(obj==null){ return false; } if(this==obj){//如果是同一个对象,返回true return true; } if(obj instanceof Cell){ Cell other = (Cell)obj; return this.x == other.x && this.y == other.y; } return false; } (3) 定义createFood()方法,实现随机生成食物的功能。 private Cell createFood(){ Random random = new Random(); int x,y; do{ x = random.nextInt(COLS); y = random.nextInt(ROWS); }while(contains(x,y)); return new Cell(x,y,Color.blue); } (4) 在SnakePanel类中,定义食物变量。 Cell food=null; (5) 定义paintFood()方法,在舞台上画出食物。 public void paintFood(Graphics g) { if(food==null) food=createFood(); g.setColor(food.getColor()); g.fill3DRect(food.getX()*CELL_SIZE,food.getY()*CELL_SIZE, CELL_SIZE, CELL_SIZE, true); } (6) 在paint()方法中调用paintFood(Graphics g)方法,画出当前食物。 此时,运行该游戏程序,在舞台上能随机生成食物了。 规则3: 蛇吃到一个食物身体就增长一格,同时生成新食物。 分析: 蛇在爬行时检查是否能够吃到食物,如吃到食物,蛇的长度会增加。 (1) 在SnakePanel类中定义能否吃到食物的变量。 boolean eat=false; (2) 修改creep()方法,创建头节点,判断头节点是否与食物重合,如果重合,则表明能吃到食物,这时只添加头节点,同时删除食物; 否则就删除尾节点,添加头节点。 public void creep(){ Cell head = createHead(currentDirection); //创建新头节点 eat = head.equals(food); //检查是否吃到食物 if(!eat){ snake.removeLast(); }else{ food=null; } snake.addFirst(head); } 至此,该游戏实现了食物的随机生成以及蛇吃到食物的增长。 规则4: 蛇碰到四周边界或自身则游戏结束。 分析: 游戏结束的条件是检查在新的运行方向上是否能够碰撞到四周边界和自身。其中,如果新头节点出界,则表示碰撞边界; 如果新头节点包含在蛇的前n-1节点范围内,则表示蛇碰到自己。 在SnakePanel类中定义hit()方法,实现游戏是否结束的判断。 public boolean hit(){ Cell head = createHead(currentDirection); if(head.getX()<0 || head.getX()>=COLS || head.getY()<0 || head.getY()>=ROWS){ return true; } return snake.subList(0, snake.size()-1).contains(head); } 然后修改creep()方法,在蛇爬行过程中不断调用hit()方法进行判断,如果hit()方法返回值为true,则游戏结束,弹出如图55所示的消息框。 图55游戏结束消息框 public void creep(){ if(hit()){//如果条件为真则结束游戏,否则游戏继续 timer.stop(); JOptionPane.showMessageDialog(null, "game over!!!"); }else{ Cell head = createHead(currentDirection); //创建新头节点 eat = head.equals(food); //检查是否吃到食物 if(!eat){ snake.removeLast(); }else{ food=null; } snake.addFirst(head); } } 7. 画出游戏的成绩和所用时间 (1) 定义存放成绩的变量。 int score=0; (2) 定义paintScore(Graphics g)方法,画出游戏的成绩。 public void paintScore(Graphics g) { g.setColor(Color.blue); Font f = getFont(); Font font = new Font(f.getName(), Font.BOLD, 28); g.setFont(font); g.drawString(""+score, 580, 530); } (3) 修改creep()方法,当蛇吃到食物时,成绩加1。 public void creep(){ if(hit()){ timer.stop(); JOptionPane.showMessageDialog(null, "game over!!!"); }else{ Cell head = createHead(currentDirection); //创建新头节点 eat = head.equals(food); //检查是否吃到食物 if(!eat){ snake.removeLast(); }else{ score++; food=null; } snake.addFirst(head); } } (4) 在paint()方法中调用paintScore(Graphics g)方法,实现如图56所示的成绩绘制。 图56绘制游戏成绩 至此,简单的《贪吃蛇》游戏就开发完成了。 5.7总结 希望读者通过本项目的开发练习,熟练掌握利用Java Swing图形用户界面编程、键盘事件的处理过程以及多线程编程开发游戏的方法。在学完之后可以仿照《俄罗斯方块》游戏的实现方法添加一些控制游戏的功能,进一步修改、完善本游戏。