该项目是根据GUI编程学习所写,详细的学习课程可以参考狂神说老师的GUI课程:一小时开发贪吃蛇。
以下仅仅为简要说一下逻辑,源码放在最后。
实现该项目总共用到三个类:Data数据类,用于存放静态数据;Snake实现类,用于运行;GamePanel核心类,用于具体实现内部逻辑。
实现结果如图:
Data类要实现以上的内容,我们需要一些静态图片资源,包括蛇头蛇身,介绍图等,该类就是为了存放这些而设计的,如开头广告栏的存储:
public static URL headURL = Data.class.getResource("/StaticFile/header.png");
public static ImageIcon header = new ImageIcon(headURL);
由于这些均为静态资源,使用static进行存储。
Snake类该类为运行类,直接调用GamePanel核心类进行运行。它继承了JFrame类,即一个最外层的窗口,只是在窗口中使用frame.add(new GamePanel());
添加了面板,而该面板GamePanel即为真正的实现功能的类
该类为真正的实现类。
实现贪吃蛇功能,我们要明确一点:表面上看见贪吃蛇一格一格动,实际上是整个图片在一次一次变化。每一次贪吃蛇的移动,象征的是面板的一次更新。
所以逻辑很清晰了,要实现功能,先实现静态,再实现动态:
先直接在这里写上在该类中我用到的变量,便于后面大家对照分别是什么意思:
//定义蛇的数据结构
int length;//蛇的长度
String direct;//蛇的方向
int[] snakeX = new int[600];//蛇的x坐标
int[] snakeY = new int[500];//蛇的Y坐标
int score;//游戏成绩
boolean isStart;//游戏当前的状态,初始为暂停
boolean isFail;//游戏失败状态
//食物坐标
int foodX;
int foodY;
Random random = new Random();//随机数
Timer timer = new Timer(100,this);//定时器,100ms执行一次
这里对部分参数进行解释:
- 随机数,用作确定食物的位置,食物要随机分布,下面init()方法有提到。
- 定时器,定时刷新界面,前面已经讲到了贪吃蛇的移动象征面板的一次更新,使用定时器就可以定时更新一次面板,也就可以规定贪吃蛇的移动速度了
首先我们需要初始化属性的内容,不建议直接在定义时初始化,我这里采用了设计一个init()方法将这些参数初始化:
public void init(){
length = 3;
snakeX[0] = 100; snakeY[0] = 100;//脑袋坐标
snakeX[1] = 75; snakeY[1] = 100;//第一个身体坐标
snakeX[2] = 50; snakeY[2] = 100;//第二个身体坐标
score = 0;//积分初始为0
direct = "R";//初始方向向右
isStart = false;//初始为暂停
isFail = false;//默认为不失败
//食物随机分布在界面上
foodX = 25 + 25 * random.nextInt(34);
foodY = 75 + 25 * random.nextInt(24);
}
静态图片我们这里继承JPanel类,重写了protected void paintComponent(Graphics g)
方法,该方法是用作绘制一次图片的,只要创建该类对象,即可进行一次绘制,自动调用一次这个方法,初始样式如下:
protected void paintComponent(Graphics g){
super.paintComponent(g);//清屏
}
当我们在运行游戏开始时,即调用一次该方法,使之在游戏未动的时候对面板进行一次静态绘制。所以我们可以把我们想要画的初始界面的样子在这个时候先写好,譬如:
protected void paintComponent(Graphics g){
super.paintComponent(g);//清屏
setBackground(Color.white);
//先绘制静态面板
Data.header.paintIcon(this,g,25,11);//开头广告画上去
g.fillRect(25,75,850,600);//默认游戏界面
//画积分
g.setColor(Color.white);
g.setFont(new Font("微软雅黑",Font.BOLD,18));
g.drawString("长度:"+length,750,35);
g.drawString("分数:"+score,750,55);
g.setColor(Color.white);
//设置字体
g.setFont(new Font("微软雅黑",Font.BOLD,40));
g.drawString("按下空格开始游戏",300,300);
}
这样就可以实现一个没有小蛇没有食物,但是基本框架都写好的页面。小蛇的静态图绘制只需要找好坐标,以上面类似绘制上广告的逻辑写即可:
Data.right.paintIcon(this,g,snakeX[0],snakeY[0]);//蛇头
for (int i = 1; i < length; i++) {
Data.body.paintIcon(this,g,snakeX[i],snakeY[i]);//蛇身
}
这样我们就可以实现类似于开头这个图一样的页面了。
静态图绘制好了,我们需要实现动态,那么逻辑应该是怎样的呢?不妨把自己带入成一个游戏玩家:
我们按下空格可以开始,暂停游戏,按下上下左右可以变换贪吃蛇的运动方向,所以我们很明显,需要键盘监听器。
当贪吃蛇吃了食物,长度会变长;贪吃蛇也会按照它的方向保持移动;贪吃蛇吃到自己的身体,会游戏失败,很明显需要事件监听器。
所以要引入这些概念,我们很明显,调用两个监听器接口即可:
public class GamePanel extends JPanel implements KeyListener, ActionListener{
@Override
public void keyPressed(KeyEvent e){}//实现键盘监听
@Override
public void actionPerformed(ActionEvent e){}//实现事件监听
//这两个不需要实现,只需要写上,不然会报错
@Override
public void keyTyped(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {}
}
键盘监听的逻辑很简单:空格就是开始或者暂停,变换isStart变量;上下左右就是小蛇移动,变换direct变量。
事件监听稍微麻烦一点:
-
如果游戏是开始的情况,小蛇就要移动,要使小蛇移动很简单,就是让小蛇的后一节身体的位置变到前一节身体的位置:
for (int i = length - 1; i > 0; i--) { //后一节身体变成前一节身体的位置 snakeX[i] = snakeX[i-1]; snakeY[i] = snakeY[i-1]; }
-
因为要上下左右动,所以监听方向,当向各种方向时,要变化蛇移动的距离和运动方向。考虑到该游戏的自行设计,设定为超出边界就从另一边出来,所以还要设计监听边界问题:
if (direct.equals("R")){//右移 snakeX[0] = snakeX[0] + 25; //边界判断 if (snakeX[0]>850){snakeX[0] = 25;} }else if (direct.equals("L")){//左移 snakeX[0] = snakeX[0] - 25; //边界判断 if (snakeX[0]<0){snakeX[0] = 850;} }else if (direct.equals("U")){//上移 snakeY[0] = snakeY[0] - 25; //边界判断 if (snakeY[0]<75){snakeY[0] = 650;} }else if (direct.equals("D")){//下移 snakeY[0] = snakeY[0] + 25; //边界判断 if (snakeY[0]>650){snakeY[0] = 75;} }
-
贪吃蛇要吃东西,所以需要一个蛇头碰到食物的情况:
if(snakeX[0] == foodX && snakeY[0] == foodY){ length++;//长度+1 score+=10; //然后再次随机生成食物 foodX = 25 + 25 * random.nextInt(34); foodY = 75 + 25 * random.nextInt(24); }
-
失败事件,撞到自己就失败了:
for (int i = 1 ; i<length;i++){ if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]){ isFail = true; repaint(); } }
监听的话已经实现,但是还是没有阐述如何动态,很简单,在监听器最后调用repaint()
方法即可,这个方法可以重新调用paintComponent
方法,也就是重新进行一次静态绘制,而监听器通过改变了属性量,再进行一次静态绘制时,就已经不是原来的那张图了。而游戏中每时每刻都发生变化,这样我们每次绘制的面板都不一样,自然就能实现动态了。
当然,如果没有加入这些监听器是没用的,这里我们再重写以下无参构造:
public GamePanel(){
init();
//获得焦点和键盘事件
setFocusable(true);
addKeyListener(this);
timer.start();
}
写到这里会发现,那个静态的绘制方法是不是过于简陋了,所以我们需要在静态方法里面加入一系列的判断,因为每一次静态绘制,定义的参数量都可能发生了变化,要想对他们的变化做出反应,就得针对他们不同情况的量做不同的绘制。这里在源码里面都有,不具体说了。
源码地址:
链接:百度网盘链接
提取码:xb58
备注:里面包含了静态资源,在staticFile文件夹内,如果想使用请更改Data数据类里面的路径。且该内容仅为源码,想测试效果请自行搭建项目复制粘贴。