贪吃蛇是世界知名的益智类小游戏,选择这个题目有以下几个原因:
第一点是为了尽可能将自己所学的知识加以运用;
第二点是为了尽可能做一些“看起来”复杂的程序;
第三点是为了尽可能运用Swing组件的知识,因为对做游戏方面有一定的兴趣,也很想知道流畅的游戏是怎么做出来的,所以选择对这方面进行尝试;
我希望通过自己的所学知识把它剖析开来,通过自己的动手实践,真正的了解它的本质和精髓。
在程序中仍有许多未完善的功能,在设计过程中也遇到了很多的问题,在之后都会有详细描述。
本程序主要是完成贪吃蛇游戏的基本操作,需要满足以下几点要求:
(1) 利用方向键来改变蛇的运行方向。
(2) 空格键暂停或继续游戏,并在随机的地方产生食物。
(3) 吃到食物就增长蛇的身体长度。
(4) 碰到壁或自身则游戏结束,否则正常运行。
根据整体需求,大致设计思路如下:在游戏开始后,在没有触发死亡条件的时候,按下方向键可以让蛇移动。按键移动很简单,只需要监听键盘触发事件即可。关键是按下按键怎么移动。在我看来,整个程序最关键的就是如何实现移动。即最难的就是计算坐标。
在设计的过程中,我把显示区域想象成一个大表格,里面的格子就是组成蛇的基本单位,一个格子也可以代表一个食物。然后使用坐标来区分这些格子。.
经过实验,根据蛇的图片素材(蛇的图片素材 来自于网络),坐标之间的距离为25,所以界面也必须是25的倍数,食物的生成坐标也必须是25的倍数,否则如果不是25的倍数,蛇可能就吃不到食物,也可能在触碰到边界的时候多出一部分。也就是说坐标应该像如下图这样:
接下来关于如何实现吃掉食物就简单的多了:只要蛇头的坐标与食物的坐标重合,蛇的身体长度加一就可以了,同时生成新的食物。食物的随机出现只需要调用random就可以了,范围根据屏幕(画布)大小决定,如下:
//如果小蛇的头和食物坐标重合
if(snakeX[0]==foodx && snakeY[0]==foody){
//长度+1
length++;
//重新生成食物
foodx = 25 + 25 * random.nextInt(32);
foody = 75 + 75 * random.nextInt(6);
}
之后我建立了两个数组:
int[] snakeX = new int[600]; //蛇的坐标X
int[] snakeY = new int[500]; //蛇的坐标Y
分别来记录蛇横坐标的变化和蛇纵坐标的变化。并在初始化方法里设置蛇的初始长度,蛇的初始头部坐标,第一个身体坐标,第二个身体坐标:
length = 3;
snakeX[0] = 100; snakeY[0] = 100; //头部坐标
snakeX[1] = 75; snakeY[1] = 100; //第一个身体坐标
snakeX[2] = 50; snakeY[2] = 100; //第二个身体坐标
接下来就是如何实现移动的效果。
首先介绍一下游戏帧的概念:
游戏帧就是游戏运行时每秒所运行的帧数(简称FPS,Frames Per Second) 和视频一样,FPS越大,在屏幕上的视频就越来越平滑,直到一个临界点(大约是100FPS),超过这个临界点,再高的FPS都只是一个令人惊奇的数值,400FPS和100FPS在人的视觉中几乎没有差别。
也就是说设定一定数值的帧就可以让一个静态图片连续转动,从而让游戏流畅运行。
所以这里就用到了一个定时器,不断的刷新页面,获得动画的一个效果。
Timer timer = new Timer(110, this);//定时器
所以,蛇的移动其实是一张张静态的图片。这条蛇在画布上移动,说白了就是每个点坐标的变化。即在每次画面刷新之前,我都对蛇的坐标进行相应的变化,即可达到流畅运行的感觉。(比如在未按方向键的情况下,蛇会默认向一个方向一致移动,即坐标一直向x/y轴对应方向加25的移动。在按下某个除移动方向外的方向键,设就会改变移动方向,即按改变的x/y轴对应方向25的移动。而蛇身我用一个循环,不断往复地重复蛇头上一次移动到的坐标,从而实现蛇坐标的改变以及感官上的不断移动)
还有碰到墙壁或自身死亡,能够暂停和开始都是比较容易实现的功能,在代码部分可以详细见到。
以下为实现贪吃蛇游戏各模块的功能,大致流程:
(1) 此程序的入口,定义了JFrame的对象jf,并设置窗口的大小,标题,在屏幕的位置等。
(2) 源代码见StartGame类。
(1) 实现蛇的身体、头部,食物等素材的导入。(所以素材放在image包下)
(2) 源代码见Data类,素材见image包。
(2) 继承自Jpanel并实现KeyListener和ActionListener接口。
(3) 方法:
(4) 源代码见GamePanel类
(1) 实现菜单栏的创建
(2) 源代码见GameJMenuBar类
环境变量:
使用软件:eclipse
Jdk:1.7.0_10
解决办法:(屏幕的宽度和高度—窗口的宽度和高度)/2 即可定位到屏幕的中间位置,而且可以根据不同电脑的分辨率自行定位。
Toolkit kit=Toolkit.getDefaultToolkit();
Dimension screenSize=kit.getScreenSize();
//获取屏幕的宽度和高度
int width=screenSize.width;
int height=screenSize.height;
int x=(width-WIDTH)/2;
int y=(height-HEIGHT)/2;
jf.setLocation(x,y);
解决方法:为菜单项添加事件监听器,然后在actionPerformed方法里,去判别点击的选项,并给出相应的事件即可。(给出了一个在窗口中多行显示的办法)
item1.addActionListener(this);// 为菜单项添加事件监听器
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if(cmd.equals("作者的话")){
JPanel panel = new JPanel(new GridLayout(0, 1, 5, 5));
JLabel Label1 = new JLabel("…" );
JLabel Label2 = new JLabel("…");
JLabel Label3 = new JLabel("…");
panel.add(Label1);
panel.add(Label2);
panel.add(Label3);
JOptionPane.showMessageDialog(this,panel,"作者的话",JOptionPane.PLAIN_MESSAGE);
}
……………//其他内容
}
解决方法:
//头部的图片
public static URL headerURL = Data.class.getResource("/image/header.png");
public static ImageIcon header = new ImageIcon(headerURL);
其中的getResource接受一个字符串参数,如果以”/”开头,就在classpath根目录下找(不会递归查找子目录),如果不以”/”开头,就在调用getResource的字节码对象所在目录下找(同样不会递归查找子目录)。有了ImageIcon才能把图片显示出来。
其他图片的导入同理。
解决方法:
protected void paintComponent(Graphics g){
super.paintComponent(g); //清屏
this.setBackground(Color.white);//设置背景的颜色
//绘制游戏区域
g.fillRect(25, 75, 950, 575);
//画一条静态的蛇
if(fx.equals("R")){
Data.right.paintIcon(this, g, snakeX[0], snakeY[0]);
}else if(fx.equals("L")){
Data.left.paintIcon(this, g, snakeX[0], snakeY[0]);
}else if(fx.equals("U")){
Data.up.paintIcon(this, g, snakeX[0], snakeY[0]);
}else if(fx.equals("D")){
Data.down.paintIcon(this, g, snakeX[0], snakeY[0]);
}
//蛇的身体长度通过length来控制
for(int i=1;i<length;i++){
Data.body.paintIcon(this, g, snakeX[i], snakeY[i]);
}
//画食物
Data.food.paintIcon(this, g, foodx, foody);
………………//其他内容
}
paintComponent()是swing的一个方法,相当于图形版的main(),是会自执行的。
解决方法:比较简单,只要蛇头的坐标大于或者小于游戏区域的横或者纵坐标就会判定出界而死亡,或者只要蛇头的坐标等于蛇身的坐标就会判定碰到自身而死亡。
public void actionPerformed(ActionEvent e) {
//如果游戏处于开始状态,并且游戏没有结束
if(isStart && isDie == false){
for(int i=length-1;i>0;i--){ //身体移动
snakeX[i] = snakeX[i-1];
snakeY[i] = snakeY[i-1];
}
//通过控制方向让头部移动
if(fx.equals("R")){
snakeX[0] = snakeX[0] + 25; //头部移动
//边界判断
if(snakeX[0]>950){isDie=true;}
}else if(fx.equals("L")){
snakeX[0] = snakeX[0] - 25; //头部移动
//边界判断
if(snakeX[0]<25){isDie=true;}
}else if(fx.equals("U")){
snakeY[0] = snakeY[0] - 25; //头部移动
//边界判断
if(snakeY[0]<75){isDie=true;}
}else if(fx.equals("D")){
snakeY[0] = snakeY[0] + 25; //头部移动
//边界判断
if(snakeY[0]>625){isDie=true;}
}
//如果小蛇的头和小蛇的身体坐标重合,失败
for(int i=1;i<length;i++){
if(snakeX[0]==snakeX[i] && snakeY[0]==snakeY[i]){
isDie=true;
}
}
//刷新界面
repaint();
}
timer.start(); //让时间动起来
}
解决方法:需要键盘的监听,只要按下键盘上对应的按键,就会触发相应的事件。
public void keyPressed(KeyEvent e) {
// 键盘按下,未释放
//获取按下的键盘是哪个键
int keyCode = e.getKeyCode();
if(keyCode==KeyEvent.VK_SPACE){ //如果按下的是空格键
if(isDie){ //失败,游戏再来一遍
isDie = false;
init(); //重新初始化游戏
}else{ //暂停游戏
isStart = !isStart;
}
repaint(); //刷新界面
}
……………//其他内容
}
还有一些未完成的功能:调整蛇速度的快慢;蛇回头可以碰到自己的身体;地形更加复杂(或者随机地图);
关于调整蛇速度的快慢:我用了两种方案,一种是添加菜单栏,在菜单栏上调整蛇速度的快慢,另一种是在Jpanel里添加按钮或者单选框。但是第一种情况下,改变Timer速度在编译器中不会报错,但是在点击选项(运行)时会报错,并且没有用,暂时通过百度没发现如何解决。第二种情况下,如果点击按钮或者单选框,蛇的速度确实发生了改变,但是键盘却失灵了。在编译器阶段和运行阶段均没有报错。而且如果点击了其中一个按钮,其他的按钮就不会实现功能。暂时没发现如何解决。
关于蛇回头可以碰到自己的身体:目前还没有想到如何实现,但是想到了可能要怎么做,就是在按下方向键之后,与这个方向相反的按键会失去从键盘上的监听。之后会继续研究。
关于地形更加复杂:如果蛇在移动过程中,不碰触边界,不碰到自身还是相对容易的,有些贪吃蛇是会出现随机地图的(即出现墙壁,碰到墙壁就会死亡)。其中,碰到墙壁就死亡容易实现,关键在于如何构造地图,应该不能选择随机设置墙壁坐标,否则有可能没有通路,也就是只能提前设置好。或者随机设置需要精心计算一个算法(我想应该可以,不过很麻烦)。设置好之后每次死亡换一次地图如何实现也正在考虑。关键在于存储地图所用的数据结构。目前在我来看,我只能像构建食物一样,一个一个的将墙壁在地图上绘制,很麻烦。暂时没想到有更好的实现方式。