2、贪吃蛇游戏设计
WavesBrightt - 贪吃蛇 - 开发
0、游戏设计核心部分解析(后期补充)
- 当看到这片区域的时候,整个游戏的开发差不多已经结束了,在结束之前,我对当前项目做一个总结
- 我们的游戏效果是通过GUI功能实现的,而将小蛇绘制上去,则是在GUI当中的面板类=>JPanel实现的
- JPanel当中提供了许多的方法供我们在这个面板上面进行图片的绘制
- 图片资源源于网络
- 整个小蛇的身体坐标由二维数组构成,食物也是同理的
- 要达成小蛇的动态移动和通过键盘控制方向,需要借助两个东西
- 一是定时器
- 二是键盘监听
- 下面就是对于整个项目开发过程当中的每个点的总结
1、游戏素材获取
1.1、图片素材获取
1.2、游戏概念
- 如何让游戏动起来?
- 首先我们了解任何一款游戏的时候,游戏是动画,蛇会动,无论是我们玩的游戏还是什么,都有一个概念,叫做帧,游戏是一帧一帧运行的
- 那么我们的 时间片 足够小,例如一秒30帧,一秒60帧,这样的话,他一个静态的图片,他由于连续转动,也可以达到类似动画annotion的效果,能够让咱们的游戏,动起来
- 如何实现的呢,通过定时器实现,这个定时器让页面不断地刷新,从而让游戏动起来,达成动画的效果
- 键盘响应
- 我们需要根据键盘的监听,来对贪吃蛇进行操控
- 如何才做到上述的要求?
1.3、逻辑布局
- 实际上就是一张一张静态的图片通过切换组合形成了我们眼前的这些游戏动画
- 手动通过copy我们可以实现小蛇在这个页面上的滑动
- 那么通过键盘监听,我们可以对小蛇的行动方向进行改变
- 当小蛇吃到所谓的食物的时候,让小蛇的身体变长,这就是我们这个游戏的核心理念
2、绘制静态窗口
2.1、创建demo
这是一个普通的java项目
2.2、项目结构
2.3、GUI编程设计
- 我们整体的页面是使用GUI进行设计的
- GUI也被称作窗体,我们可以在这个窗体当中,通过绘制面板,绘制图形图像,来展示我们的贪吃蛇游戏
- 下面是关于GUI窗体,JFrame的窗口设计
/**
* JFrame
* @return 返回一个GUI窗体对象
*/
public JFrame getJframe(){
// 1、加载静态窗口,绘制静态窗口,实例化JFrame对象
JFrame gameStart = new JFrame();
// 2、设置窗口大小
// x,y,width,height
gameStart.setBounds(520,120,900,720);
// 3、设置窗口标题
gameStart.setTitle("WavesBright-SnakeGame");
// 4、该窗口不可以手动调整大小
gameStart.setResizable(false);
// 5、设置窗口的关闭事件
gameStart.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 6、填充面板对象 => 自定义面板对象
gameStart.add(new JFrameGamePanel());
// 7、设置让窗口进行展示
gameStart.setVisible(true);
return gameStart;
}
3、外部数据类设计
4.1、概述
- 我们是不是在静态资源当中存储了图片,但是这个图片需要在GUI页面当中成功加载出来需要经过如下步骤
- URL:首先我们需要读取到这个图片在项目当中存储的位置,这里使用的是相对路径,且只能使用相对路径
- ImageIcon:该对象就是在GUI当中的图片对象了,该对象的实例化方法需要一个URL参数,这个URL参数就是上述方法的参数
- 这是我项目当中的statics目录
- 为了书写简单一点,我将这个目录放在了与项目类设计的同级文件当中
4.2、一个图片文件转换成ImageIcon的步骤
1、概述
- 图片类 =URL,可以获取到图片的资源,定位图片地址
- ps:通过getResource可以获取到我们当前项目下的图片,需要传递的参数为图片的相对路径
- 转换成了URL对象后我们才能通过这个URL对象转换成ImageIcon对象
- 将我们URL 定位 到的 图片地址 绘制成一张图片,然后在通过面板接口Jpanel当中提供的函数,将图片绘制到面板上
- ImageIcon 图片
- new ImageIcon(headerUrl),实例化图片对象,需要的参数为URL对象解析的图片地址
- 这样我们就可以在面板Panle上绘制我们的图片了
2、代码设计
/**
* 图片类 =URL,可以获取到图片的资源,定位图片地址
* ps:通过getResource可以获取到我们当前项目下的图片,需要传递的参数为图片的相对路径
*/
public static URL headerUrl = SnakeData.class.getResource("/statics/header.png");
/**
* 将我们URL定位到的图片地址绘制成一张图片
* ImageIcon 图片
* new ImageIcon(headerUrl),实例化图片对象,需要的参数为URL对象解析的图片地址
* 这样我们就可以在面板Panle上绘制我们的图片了
*/
public static ImageIcon header = new ImageIcon(headerUrl);
4.3、代码改进
1、概述
- 上述只是简单的设置了一张图片,但是图片的库存当中有很多图片
- 如果按照上述的代码进行图片的转换,那么我要设置多少个变量才行?
- 这里想到了使用Map集合类型,以图片的前缀作为键名,转换URL对象集合和ImageIcon对象集合
- 首先创建上述两个集合我们得手动创建一个数组,这个数组是静态数组,我们会对这个数组进行迭代得到我们组装的URL=>Map集合
- 在通过迭代URL集合从而转换为我们最终需要的ImageIcon集合
2、设计
1、静态数组设计
/**
* 项目结构当中的静态图片数组
*/
private static String[] imagesAddress = {
"body.png",
"down.png",
"food.png",
"header.png",
"left.png",
"right.png",
"up.png",
};
2、URL和ImageIcon的Map集合设计
/**
* @param staticsUrls 储的哈希表结构URL地址对象集合
* @param staticsImages 存储的哈希表结构的imageIcon对象集合
*/
public HashMap<String,URL> staticsUrls;
public HashMap<String,ImageIcon> staticsImages;
3、URL集合的数据填充
- 实例化一个Map集合<String,URL>,获取当前类下的静态资源数组对象
- 对数组进行迭代,对迭代项进行字符串切割=> split,因为是根据 点. 进行切割的,所以要注意转义字符的使用
- 在迭代过程当中实例化URL对象,将我们字符串切割的前缀 + "/statics/"进行拼接得到我们最后的URL实例化对象
- 将前缀设置为Map集合的键名,URL设置为value值,这样我们整个数组的URL都获取到了
- 通过迭代这个URL集合,我们可以得到最终需要转换的ImageIcon对象集合
/**
* 通过迭代我们的静态资源数组,将我们的图片存储到HashMap集合当中
* @return 存储完毕的哈希表集合
*/
public void setStaticsUrls(){
// 采用哈希Map集合存储我们的URL对象数据
HashMap<String,URL> mapUrls = new HashMap<>();
String[] addresses = this.getImagesAddress();
// 迭代我们当前内部对象当中的数组,我们需要进行字符串的拼接
for (String address : addresses) {
// 将我们内部的对象进行切割,以.为间隔,这里需要进行转译
String[] fix = address.split("\\.");
// 获取我们当中的前缀,前缀会用来做我们哈希表的key值(键指)
String prefix = fix[0];
/**
* 图片类 =URL,可以获取到图片的资源,定位图片地址
* ps:通过getResource可以获取到我们当前项目下的图片,需要传递的参数为图片的相对路径
*/
URL url = SnakeData.class.getResource("/statics/"+address);
// 存储到Hash集合当中
mapUrls.put(prefix,url);
}
this.staticsUrls = mapUrls;
}
4、ImageIcon集合数据填充
- 迭代我们当前类的URL集合
- 创建Map集合<String,ImageIcon>
- 获取value值
- 实例化ImageIcon对象,将value值传递进去
- 以URL的key值作为我们ImageIcon集合的键值,将实例化的ImageIcon传递进去
/**
* 迭代静态资源数组,得到我们最终的图片对象
* @param
*/
public void setStaticsImages(){
// 获取到我们组装完毕的URL地址集合
HashMap<String, URL> staticsUrls = this.staticsUrls;
// 实例化我们当前的ImageIcons集合
HashMap<String,ImageIcon> images = new HashMap<>();
// 对其进行迭代
for (String key : staticsUrls.keySet()) {
/**
* 将我们URL定位到的图片地址绘制成一张图片
* ImageIcon 图片
* new ImageIcon(headerUrl),实例化图片对象,需要的参数为URL对象解析的图片地址
* 这样我们就可以在面板Panle上绘制我们的图片了
*/
ImageIcon value = new ImageIcon(staticsUrls.get(key));
// 添加到我们的ImageIcon集合当中
images.put(key,value);
}
// 组装完毕返回即可
this.staticsImages = images;
}
5、设置构造器,此对象被new出来的时候加载我们上述设计的两个方法
/**
* 设置构造方法,实例化我们这个类的时候我们就可以加载到当中的
* 设置的两个集合
*/
public SnakeData(){
this.setStaticsUrls();
this.setStaticsImages();
}
4、绘制小蛇
4.1、绘制游戏面板 => Jpanel
- JPanel是swing包下的面板类容器,我们可以在这个面板容器当中的绘制我们的游戏内容
- 并且最主要的一点就是,JPanel是可以加载在JFrame对象当中的,这也是我们绘制面板对象的原因
- JPanle类设计
- 首先该类是需要继承自swing包下的JPanle的实现类
- 继承了JPanle类后,我们需要重写父类当中的一个方法 => paintComponent
- 该方法有一个参数对象,Graphics g,该对象是我们面板对象中的画笔对象,我们的游戏界面绘制都是通过这只画笔对象来完成的
- 这个方法的最初结构如下
- 通过重构该方法,我们可以使用画笔对象=>Graphics在我们自定义面板类当中绘制我们的小蛇和食物
- 首先该类是需要继承自swing包下的JPanle的实现类
4.2、按键监听
-
与我们键盘输入的Scanner不一样,接下来的这个键盘监听是实时监听的,我们需要这个自定义面板的基础类上,去实现一个接口 => KeyListener
- KeyListener:键盘监听对象,是event包下的接口
- 我们需要对这个接口内部的方法进行实现,从而完成对我们键盘事件的监听
-
-
实现这个接口需要实现该接口当中的三个方法
-
/** * 接受键盘输入的监听 * @param e 键盘输入值 */ @Override public void keyTyped(KeyEvent e) { // 敲击键盘,发生在按键按下后,按键放开前。 } @Override public void keyPressed(KeyEvent e) { // 按下按键时发生。 } @Override public void keyReleased(KeyEvent e) { // 松开按键时发生。 }
-
4.3.继续绘制面板
1、概述
- 首先实例化我们刚刚创建的外部数据类对象SnakeData,我们需要从这个数据类对象当中获取到我们小蛇的头部,身体,还有顶部标题的图片资源,并将其绘制到面板上
- 在面板类当中定义如下变量
- snakeLength:小蛇的长度
- coordinate:二维数组用来描述小蛇在面板上的坐标位置
- direction:小蛇的移动方向
- snakeData:外部数据存储对象,我们可以通过这个实例化该对象,通过对象的的构造方法获取到我们转换完毕的图片集合
2、设置面板类的构造器,初始化小蛇各类参数信息
- 初始化小蛇的身体长度
- 初始化小蛇的头部坐标
- 初始化小蛇的第一节身体长度坐标
- 初始化小蛇第二节身体长度坐标
- 初始化小蛇的头部方向=> 默认为右边 => R
代码设计
/**
* 初始化游戏数据
*/
public void initGameData(){
//蛇的身体长度初始化为3
this.snakeLength = 3;
// 原一维数组设计
/*this.snakeLengthX[0] = 100; this.snakeLengthY[0] = 100;
// 第一节身体的长度
this.snakeLengthX[1] = 75; this.snakeLengthY[1] = 100;
// 第二节身体的长度
this.snakeLengthX[2] = 50; this.snakeLengthY[2] = 100;*/
// 头部的长度
this.coordinate[0][0]= 100;this.coordinate[0][1]= 100;
// 第一节身体的长度
this.coordinate[1][0]= 75;this.coordinate[1][1]= 100;
// 第二节身体的长度
this.coordinate[2][0]= 50;this.coordinate[2][1]= 100;
// 初始化蛇的头部方向
this.direction = "R";
// 游戏是否开始
Boolean gameStart = false;
}
4.4、绘制面板内容
1、概述
- 从之前的讲述当中我们知道,继承自这个面板类JPanle,我们如果要自己绘制面板,我们需要重写这个实现类当中的方法=>paintComponent
- 在这个方法当中,我们可以通过 画笔对象 => Graphics g,来对我们的面板进行绘制
- 那么接下来我们在画板类当中进行了如下操作
- 实例化外部数据类对象,我们需要从该对象中获取到封装完毕的Map集合 => ImageIcon图片类型对象集合
- 绘制我们面板的背景颜色
- 绘制顶部的标题栏 => 通过集合当中指定key值的图片,借助画笔,将他们绘制到面板上
- 绘制当前游戏区域 => g.fillRect(指定xy轴坐标,宽高大小),这里我们设计的是850宽600高,小蛇一节身体的长度是25像素
- 绘制我们的小蛇,和广告栏一样,但是需要对小蛇的头部进行判定,头部的头像有四个图片,根据四个方向来渲染头部的四个方向
- 绘制小蛇的身体,使用迭代进行绘制,为什么要用循环?小蛇是要吃食物的,吃食物身体长度就会增加,如果不用迭代那么身体的长度就固定死了
- 判断当前游戏是否开始,gameStart == false?如果当前游戏没有开始,则在面板上绘制字符串提示内容
2、代码设计
/**
* 重新绘制面板,重写父类当中的方法 paintComponent
* @param g 画笔,画图工具的画笔,我们的游戏界面绘制都是通过这只画笔对象来完成的
*/
@Override
protected void paintComponent(Graphics g) {
/**
* 调用了父类当中的方法,这个方法的目的是清屏
* 我们游戏开始的时候可以通过这个方法来清屏
*/
super.paintComponent(g);
// 实例化我们的外部工具类
snakeData = new SnakeData();
// 设置背景颜色
this.setBackground(Color.BLACK);
/**
* 绘制顶部的标题栏,计分板
* paintIcon()函数下有四个参数
* Component:需要绘制到哪个组件上?那肯定是我们当前的面板上=>this
* Graphics:绘制所需要的画笔对象
* 剩下两个参数为x轴和y轴的距离
* 这样我们的顶部计分栏就设计完毕了
*/
snakeData.staticsImages.get("header").paintIcon(this,g,25,11);
// 设置当前的游戏区域,通过画笔填充
// 横向25,纵向75,这个75是顶部图片加间隔的距离,不是图片本身的高度
g.fillRect(25,75,850,600);
// 判断当前蛇头的方向
switch(direction){
case "U":
// 绘制当前蛇头方向的图片
snakeData.staticsImages.get("up").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
case "D":
snakeData.staticsImages.get("down").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
case "L":
snakeData.staticsImages.get("left").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
case "R":
snakeData.staticsImages.get("right").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
default:
break;
}
for(int i = 1; i< snakeLength; i++){
/**
* 方法改进,我们上述写身体的坐标位置实际上是把身体写死了
* 身体的长度是不断变化的,不断变化那就要涉及到循环的处理
*循环从1开始,那么循环一开始就是我们第一节。。。。第n节的位置
*/
// 蛇的身体长度通过 => snakeLength 来控制
snakeData.staticsImages.get("body").paintIcon(this,g,coordinate[i][0],coordinate[i][1]);
}
// 判断当前游戏是否开始
if(!this.gameStart){
// 绘制画笔颜色
g.setColor(Color.WHITE);
// 设置画笔字体,微软雅黑,粗体,40号大小
g.setFont(new Font("微软雅黑",Font.BOLD,40));
g.drawString("点击空格开始进行贪吃蛇游戏!",200,300);
}
}
5、键盘监听
5.1、设置键盘监听事件
1、概述
- 通过三个键盘事件,我们这里应该选择使用keyPressed事件进行重构=> ps:键盘按下
- 通过事件源对象KeyEvent,获取我们输入的键盘信息
- 对键盘信息的值进行判断,如果当前按下的键,为空格,那么就将我们的gameStart取反即可
2、代码设计
/**
* 按下按键时发生。
* @param e
*/
@Override
public void keyPressed(KeyEvent e) {
// 按住
// 通过事件源对象,获取输入的按键信息
int keyCode = e.getKeyCode();
// 判断当前的keyCode是否是空格键
if(keyCode == KeyEvent.VK_SPACE){
// 将gameStart取反
this.gameStart = !this.gameStart;
// 刷新当前面板 => 重新绘制当前面板
repaint();
}
}
5.2、完善构造器当中的方法
1、概述
- 获取键盘的监听事件,目的是为了让键盘输入焦点能够聚集在游戏上面
- 设置监听的对象是谁,KeyListener这个接口的实现类,也就是我们自定义的面板类对象,所以是this
2、代码修改
/**
* 画板构造器,初始化小蛇数据
*/
public JFrameGamePanel(){
// 初始化小蛇身体数据
initGameData();
// 获取键盘的监听事件,目的是为了让键盘输入焦点能够聚集在游戏上面
this.setFocusable(true);
// 设置监听的对象是谁,KeyListener这个接口的实现类
this.addKeyListener(this);
}
6、定时器Timer
6.1、概述
- swing包下的Timer对象,对其进行实例化
- 实例化需要传递以下几个参数
- 这是这个swing包下Timmer对象的构造方法
- delay: 多久刷新一次,毫秒级别
- listener: 监听的对象是谁,实现了接口ActionListener的类,也就是我们当前的自定义面板类
6.2、参数设计
0.1s刷新一次,监听对象为我们设计的这个面板对象
/**
*定时器
* @param delay 多久刷新一次,毫秒级别
* @param listener 监听的对象是谁?就是监听我们这个面板对象,实现了这个接口的面板对象
*/
Timer timer = new Timer(100,this);
6.3、设置监听内容
1、概述
- 对接口(ActionListener)的actionPerformed方法完成事件的监听,进行重构
- 在这里我们会实现,游戏启动时,小蛇身体的移动
2、小蛇移动图解
3、代码设计
- 我们定义了一个Timer类型的变量,定义这个Timer变量后,我们需要实现awt.event包下的ActionListener接口当中的方法actionPerformed,通过设置该方法,我们在游戏页面开始的时候就可以对面板进行刷新帧率的判断了
/ 定时器,监听事件流动,让小蛇运动起来
@Override
public void actionPerformed(ActionEvent e) {
// 游戏开始了
if(gameStart){
// 将面前的身体坐标赋值给后面的身体
for(int i = snakeLength - 1; i > 0; i--){
// 横坐标赋值
coordinate[i][0] = coordinate[i-1][0];
// 纵坐标赋值
coordinate[i][1] = coordinate[i-1][1];
}
// 小蛇脑壳向前移动一位,25个坐标值
coordinate[0][0] = coordinate[0][0] + 25;
// 边界判断,避免飞出屏幕
if(coordinate[0][0] >= 875){
// 从头开始
coordinate[0][0] = 25;
}
// 上述操作完毕之后,刷新我们的页面,不刷新没办法运行哈
repaint();
}
this.timer.start();
}
6.4、构造器改进,定时器启动=>start()
该方法为定时器的启动方法,不启动的话定时器是无法对面板进行刷新的
构造器改进
/**
* 画板构造器,初始化小蛇数据
*/
public JFrameGamePanel(){
// 初始化小蛇身体数据
initGameData();
// 获取键盘的监听事件,目的是为了让键盘输入焦点能够聚集在游戏上面
this.setFocusable(true);
// 设置监听的对象是谁,KeyListener这个接口的实现类
this.addKeyListener(this);
// 定时器开始
this.timer.start();
}
7、控制小蛇的方向
7.1、概述
- 我们需要继续在键盘监听事件当中完善我们的键盘输入方法
- 我们需要监听的按键有如下几个
- 空格键:管理游戏面板展示和开始
- 上:小蛇往上移动
- 下:小蛇往下以偶定
- 左:小蛇往左移动
- 右:小蛇往右边移动
- 0 : 使用神秘力量
- 上述变量如果进行if-else的话显然太麻烦了,而且需要做很多无谓的判断,所以我们这里采用switch进行判断
- 我们设计的事件有如下几个
- 在对方向进行判断的时候,我们需要增加一个条件
- 当进行方向位移判断的时候,你要移动的方向,不能与上一次移动的方向相反,否则就会出现头撞击身体的情况,这显然是不允许的
- 当然,经过我后续的测试,还是发现了一个bug,暂时没时间解决,下面会单独提出来讲一下
7.2、设计事件
1、空格
2、上
3、下
4、左
5、右
7.3、小蛇身体的移动=>定时器代码修改
1、概述
- 在我们原先定时器当中,我们小蛇默认是一直朝右边移动的,但是我们现在通过键盘输入对小蛇的方向
- 我们自然可以控制小蛇在移动时候的头部方向坐标判断了
- 由于switch本身占据的代码行数比较多,我们将小蛇头部方向的移动进行了一个单独的封装
- 在进行定时器判断的时候,只需要将当前的小蛇头部方向传递给该函数即可
2、代码图解
我们这里依旧采用switch进行判断,到达边界值的时候,进行了一点游戏改化,不把小蛇弄死,从另一边冒出来
2.1、小蛇朝右边移动
2.2、小蛇朝左边移动
2.3、小蛇朝上移动
2.4、小蛇朝下移动
3、代码设置
// 蛇头行动方向判定
public void snakeDirection(String direction){
switch(direction){
case "R":
// 小蛇脑壳向右移动一位,25个坐标值
coordinate[0][0] = coordinate[0][0] + 25;
// 边界判断,避免飞出屏幕
if(coordinate[0][0] >= 875){
// 从头开始
coordinate[0][0] = 25;
}
break;
case "L":
// 小蛇脑壳向左移动一位,25个坐标值
coordinate[0][0] = coordinate[0][0] - 25;
// 边界判断,避免飞出屏幕
if(coordinate[0][0] <= 0){
// 从右侧初始位置开始
coordinate[0][0] = 850;
}
break;
case "U":
// 小蛇脑壳向上移动一位,25个坐标值
coordinate[0][1] = coordinate[0][1] - 25;
// 边界判断,避免飞出屏幕
if(coordinate[0][1] <= 50){
// 从顶部初始位置开始
coordinate[0][1] = 675;
}
break;
case "D":
// 小蛇脑壳向前下移动一位,25个坐标值
coordinate[0][1] = coordinate[0][1] + 25;
// 边界判断,避免飞出屏幕
if(coordinate[0][1] >= 675){
// 从顶部初始位置开始
coordinate[0][1] = 75;
}
break;
default:
break;
}
}
8、吃食物设计
8.1、概述
- 按照之前的开发模式,现在我们想添加一个食物就很简单了
- 设计当前这个食物的坐标,采用随机数种子进行设计 =>Random
- 在构造方法当中初始化我们的食物坐标
- 在面板绘制当中,绘制我们的食物
- 在定时器的方法当中追加对食物的判断
8.2、新增变量,初始化数据方法的修改
1、新增变量
foodCoordinateX和foodCoordinateY
2、init方法的修改
- 为什么前面要加一个25,当出现随机数种子为25 * 0的时候,左边保底都有一个25的格子
- 同理,当纵坐标的随机数种子为25 * 0 的时候,顶部保底都有一个25的格子,也就是 50 + 25 ,广告的高度 + 25(一个格子的距离)
3、代码修改
/**
* 初始化游戏数据
*/
public void initGameData(){
//蛇的身体长度初始化为3
this.snakeLength = 3;
/*this.snakeLengthX[0] = 100; this.snakeLengthY[0] = 100;
// 第一节身体的长度
this.snakeLengthX[1] = 75; this.snakeLengthY[1] = 100;
// 第二节身体的长度
this.snakeLengthX[2] = 50; this.snakeLengthY[2] = 100;*/
// 头部的长度
this.coordinate[0][0]= 100;this.coordinate[0][1]= 100;
// 第一节身体的长度
this.coordinate[1][0]= 75;this.coordinate[1][1]= 100;
// 第二节身体的长度
this.coordinate[2][0]= 50;this.coordinate[2][1]= 100;
// 初始化蛇的头部方向
this.direction = "R";
// 游戏是否开始
this.gameStart = false;
// 食物当前数组坐标
this.foodCoordinateX = 25 + 25 * random.nextInt(34);
// 纵坐标
this.foodCoordinateY = 75 + 25 * random.nextInt(24);
}
8.3、面板绘制新增内容
1、概述
加一行代码就可以了,生成图片对于现在的我们来说太简单了
2、代码修改
/**
* 重新绘制面板,重写父类当中的方法 paintComponent
* @param g 画笔,画图工具的画笔,我们的游戏界面绘制都是通过这只画笔对象来完成的
*/
@Override
protected void paintComponent(Graphics g) {
/**
* 调用了父类当中的方法,这个方法的目的是清屏
* 我们游戏开始的时候可以通过这个方法来清屏
*/
super.paintComponent(g);
// 实例化我们的外部工具类
snakeData = new SnakeData();
// 设置背景颜色
this.setBackground(Color.BLACK);
/**
* 绘制顶部的广告栏,计分板
* paintIcon()函数下有四个参数
* Component:需要绘制到哪个组件上?那肯定是我们当前的面板上=>this
* Graphics:绘制所需要的画笔对象
* 剩下两个参数为x轴和y轴的距离
* 这样我们的顶部计分栏就设计完毕了
*/
snakeData.staticsImages.get("header").paintIcon(this,g,25,11);
//
// 设置当前的游戏区域,通过画笔填充
// 横向25,纵向75,这个75是顶部图片加间隔的距离,不是图片本身的高度
g.fillRect(25,75,850,600);
// 判断当前蛇头的方向
switch(direction){
case "U":
// 蛇的身体长度通过 => snakeLength 来控制
snakeData.staticsImages.get("up").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
case "D":
// 蛇的身体长度通过 => snakeLength 来控制
snakeData.staticsImages.get("down").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
case "L":
// 蛇的身体长度通过 => snakeLength 来控制
snakeData.staticsImages.get("left").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
case "R":
// 蛇的身体长度通过 => snakeLength 来控制
snakeData.staticsImages.get("right").paintIcon(this,g,coordinate[0][0],coordinate[0][1]);
break;
default:
break;
}
// 生成当前蛇的身体部分
for(int i = 1; i< snakeLength; i++){
/**
* 方法改进,我们上述写身体的坐标位置实际上是把身体写死了
* 身体的长度是不断变化的,不断变化那就要涉及到循环的处理
*循环从1开始,那么循环一开始就是我们第一节。。。。第n节的位置
*/
// 蛇的身体长度通过 => snakeLength 来控制
snakeData.staticsImages.get("body").paintIcon(this,g,coordinate[i][0],coordinate[i][1]);
}
// 生成当前食物
snakeData.staticsImages.get("food").paintIcon(this,g,foodCoordinateX,foodCoordinateY);
// 判断当前游戏是否开始
if(!this.gameStart){
// 绘制画笔颜色
g.setColor(Color.WHITE);
// 设置画笔字体,微软雅黑,粗体,40号大小
g.setFont(new Font("微软雅黑",Font.BOLD,40));
g.drawString("点击空格开始进行贪吃蛇游戏!",200,300);
}
// 当前小蛇是否死亡
if(snakeStatus){
// 绘制画笔颜色
g.setColor(Color.RED);
// 设置画笔字体,微软雅黑,粗体,40号大小
g.setFont(new Font("微软雅黑",Font.BOLD,40));
g.drawString("小蛇已经死亡,请重新开始游戏!",200,300);
}
}
8.4、定时器判定
1、概述
- 在定时器当中新增一条判断
- 在游戏开始的时候,如果当前小蛇,蛇头的坐标和食物的坐标重合
- 那么小蛇的身体长度就增加一位
- 然后我们重新生成随机数种子=>随机生成食物坐标
2、代码设计
// 定时器,监听事件流动,让小蛇运动起来
@Override
public void actionPerformed(ActionEvent e) {
// 游戏开始了,并且当前小蛇没有死亡
if(gameStart && !snakeStatus){
// 将面前的身体坐标赋值给后面的身体
for(int i = snakeLength - 1; i > 0; i--){
// 横坐标赋值
coordinate[i][0] = coordinate[i-1][0];
// 纵坐标赋值
coordinate[i][1] = coordinate[i-1][1];
}
// 根据当前操控方向位置,对蛇头位置的移动方向进行修改
this.snakeDirection(this.direction);
// 生成随机食物,如果当前蛇头的坐标和食物坐标重合
if(coordinate[0][0] == foodCoordinateX && coordinate[0][1] == foodCoordinateY){
// 身体长度 + 1
snakeLength++;
// 重新设置食物坐标
foodCoordinateX = 25 + 25 * random.nextInt(34);
foodCoordinateY = 75 + 25 * random.nextInt(24);
}
// 判断当前身体是否和脑壳重叠
for(int i =1 ; i<snakeLength; i++){
if(coordinate[0][0] == coordinate[i][0] && coordinate[0][1] == coordinate[i][1] ){
// 游戏失败
this.snakeStatus = true;
}
}
// 上述操作完毕之后,刷新我们的页面,不刷新没办法运行哈
repaint();
}
timer.start();
}
9、小蛇死亡判定
9.1、概述
- 新增一个判定,小蛇的死亡状态判定
- 当小蛇的脑壳,与任何一节身体的坐标值相等的时候,小蛇直接GG,并出现对应的死亡提示
- 如何实现上述的概念?
- 新增全局变量snakeStatus,默认值为false,代表小蛇当前未死亡
- 在小蛇数据初始化方法当中,初始化小蛇的状态
- 在绘图面板当中,新增小蛇的死亡判定文字描述,当小蛇死亡的时候,游戏停止
- 完善定时器当中的判断
9.2、新增变量,初始化小蛇状态
9.3、画板当中绘制游戏失败的图标
9.4、定时器当中新增死亡判断
- 当小蛇的头部坐标,和小蛇,除了头部以外的身体坐标重复的时候
- 判定当前小蛇死亡,并将当前的游戏状态设置为暂停
- 除了头部以外的身体,使用迭代判断
- 判断条件为,坐标重合
- 对定时器运行条件,新增条件,定时器的运作,除了需要游戏开始,还需要当前小蛇的死亡状态为false
9.5、点击空格重新开始游戏
- 点击空格的时候,新增判断
- 如果当前小蛇死亡状态为true,那么就重新调用小蛇初始化方法,然后再开始游戏
- 如果小蛇当前死亡状态为false,那么就按照之前的判定,对游戏状态进行取反即可
10、得分功能添加
10.1、概述
- 新增得分功能,该功能可以计算当前蛇的身体长度和得分分数,默认吃一个食物得10分
- 新增全局变量score,该变量表示分数变量
- 在数据初始化方法=> initGameData当中新增初始化分数
- 在画板当中绘制我们的得分项和长度判定
- 在定时器函数当中追加得分分数加成
10.2、新增变量和初始化数据函数追加
10.3、面板绘制当中绘制当前得分项和身体长度
10.4、定时器当中追加对分数的判定
1、概述
分数如何判定?肯定是吃到食物以后对我们的分数进行累加即可
2、代码追加
11、bug产生
11.1、概述
- 我之前编写关于头部移动的代码的时候,增加过一条这样的判断
- 即,在头部进行转向的时候,你按键输入的方向,不能为与你当前移动方向相反的键位
- 例如现在小蛇正在往下走,你输入了一个向上走的指令,蛇头直接180度偏转,那不是直接死掉
- 代码写好了以后,确实不会按照相反方向进行位移了,但是测试的时候还是出现了上面这种情况
- 为什么呢?这个小蛇很明显就是相反位移导致的死亡
- 出现这个情况的原因是,按键输入的速度太快了
- 例如现在我输入了一个向下的指令,我在这个蛇头,还没有进行位移的时候,再输入一个向右的指令
- 根据我代码的设计判断,蛇头当前方向是向下,自然是可以向右移动的
- 出现这个情况的媒介就是,你的手速超过了当前屏幕的刷新速度,确实可以,因为本身设计的帧率就不高
- 我已经死在这种情况下很多次了
11.2、如何解决呢,有空解决
12、神秘力量
12.1、概述
- 小蛇要吃到这个食物,需要通过输入键盘的事件来进行判定,但是输入键盘的目的是为了什么?修改当前蛇头的方向,他只是修改这个蛇头方向的字符串而已。
- 吃到食物以后,才会生成新的食物坐标
- 那么以上图为例,我如果要吃到这个食物,有很多种方式,我这里就选择,控制一个方向来吃掉这个食物
- 怎么自动输入?
- 先不考虑运行的算量关系,我相信我添加这么一个小功能,应该还是能做到的
- 我现在需要考虑的是,这个脚本,应该在什么时候运行?脚本运行的时机,修改方向的时机是什么时候?
- 食物是一开始初始化变量的时候,生成的坐标,往后,吃到食物,生成坐标是在定时器的方法当中生成的
- 可以通过单方向修改吃到食物,但是会出现蛇的身体过长导致脑壳撞到身体
12.2、上手
This is 神秘力量 の 雏形
看不懂是不是,没关系,一步一步来
- 我认为运行脚本这个方法应该加在定时器的函数当中
- 在生成随机食物的判定当中,是这么写的
- 脑壳的坐标值和食物的坐标值重合,那么就会执行下方判定
- 我这里就可以设计,没有生成新的随机食物的时候,运行一个脚本函数,这个脚本函数需要有一个开关,即,决定你这个玩家,是否要开启当前脚本
12.3、脚本设计 => plugIn
1、概述
- 在运行这个脚本的时候,我们脚本会做一次运算,这个运算是为了计算,小蛇要吃到这个食物,需要输入的方向键
- 我们只需要输入一次方向键即可吃到食物,但具体是上左下右呢,就要看我们具体的运算了
- 这里我想到的是,根据当前蛇头的方向来判断小蛇吃到这个食物需要输入的方向
2、图解
2.1、蛇脑壳方向是上或者下情况的时候
- 当前蛇头是上或者下的,那么我只需要通过输入左、右方向键即可吃到这个食物
- 那具体怎么吃呢?计算当前蛇头的横坐标与食物横坐标的差值,脑壳方向是上、下的时候才计算横坐标的差值哈
- 如果当前横坐标差值为负值,那么就说明食物在脑壳的左边,否则就在右边
2.2、蛇脑壳方向是左、右的时候
- 同理,当前蛇脑壳的方向是左或者右,那么我要吃到这个食物只需要输入上、下方向键即可吃到
- 怎么吃?蛇头上下计算横坐标的差值,那么蛇头方向左右的时候就计算纵坐标差值即可
3、根据图解设计代码
- 我们上述的计算方式,只需要运行一次即可,为什么只需要运行一次?我一次就能得到结果我为啥还要每次调用的时候都计算?没必要吧
- 所以计算下一次运行方向这一块的算法,运行一次即可,设计一个开关控制他,在哪里设计这个开关?
- 在没吃到食物,并且,脚本开关是开启的时候,在第一次调用该方法之后,取消位置计算
脚本运行方向算法设计
/**
* this is 外挂
*/
public void plugIn(){
// 让定时器来调用这个脚本
// 我们肯定要计算食物坐标和小蛇脑壳坐标的差值,但是没有必要每次都计算
// 定时器调用这个脚本的时候,计算方法运行一次,运行一次以后计算方法关闭
// 当吃到食物的时候再让计算方法的变量重置即可
// 当前第一次运算
if(!plugin){
// 获取当前蛇头的方向
String nowDirection = this.direction;
// 当前蛇头方向为上下
if("U".equals(nowDirection)||"D".equals(nowDirection)){
// 判断横坐标差值是否为正值or负值
// 为正值,说明食物在右边
if(foodCoordinateX - coordinate[0][0]>0){
// 下一次移动方向为右边
this.nextDirection ="R";
}else{
// 下一次移动方向为左边
this.nextDirection ="L";
}
}else{
if(foodCoordinateY - coordinate[0][1]>0){
// 下一次移动方向为下
this.nextDirection ="D";
}else{
// 下一次移动方向为上
this.nextDirection ="U";
}
}
// System.out.println("下一次移动方向为:" + this.nextDirection);
}
}
4、下一次的方向计算出来了,那么什么时候改变方向?
- 定时器是每次都会调用我们这个方法的,我们这个方法除了顶部需要判断下一次的运行方向之外
- 还需要不断地判断当前坐标值(X,Y轴的值是否和食物重复),单项判断,看图解就明白了
4.1、根据下一次运行方向判断
蛇头方向为上下,计算出下一次运动方向为左右
当蛇头的纵坐标值和食物的纵坐标值相等的时候,改变我们蛇头的方向
将 下一次运动方向的值 赋值给蛇头方向
蛇头方向为左右的时候,那么下一次运行方向为上或者下
当蛇头的横坐标和食物重合的时候,就可以修改我们当前蛇头的方向了
5、代码设计,蛇头什么时候修改方向
- 该方法不需要其他条件的约束,因为是在定时器当中运行的
- 所以会一直判断,直到条件满足为止
- 但是我这里的判断就做的比较粗略了,没有考虑最短路径等等,时间复杂度空间复杂度
- 而且有bug,蛇的身体过长就会把自己创死
// 判断下一次要移动的方向
if("U".equals(nextDirection) || "D".equals(nextDirection)){
// 纵坐标相等时,改变方向
if(coordinate[0][0] == foodCoordinateX){
this.direction = nextDirection;
}
}else{
// 横坐标相等时,改变方向
if(coordinate[0][1] == foodCoordinateY){
this.direction = nextDirection;
}
}
6、脚本代码完整
/**
* this is 外挂
*/
public void plugIn(){
// 让定时器来调用这个脚本
// 我们肯定要计算食物坐标和小蛇脑壳坐标的差值,但是没有必要每次都计算
// 定时器调用这个脚本的时候,计算方法运行一次,运行一次以后计算方法关闭
// 当吃到食物的时候再让计算方法的变量重置即可
// 当前第一次运算
if(!plugin){
// 获取当前蛇头的方向
String nowDirection = this.direction;
// 当前蛇头方向为上下
if("U".equals(nowDirection)||"D".equals(nowDirection)){
// 判断横坐标差值是否为正值or负值
// 为正值,说明食物在右边
if(foodCoordinateX - coordinate[0][0]>0){
// 下一次移动方向为右边
this.nextDirection ="R";
}else{
// 下一次移动方向为左边
this.nextDirection ="L";
}
}else{
if(foodCoordinateY - coordinate[0][1]>0){
// 下一次移动方向为下
this.nextDirection ="D";
}else{
// 下一次移动方向为上
this.nextDirection ="U";
}
}
// System.out.println("下一次移动方向为:" + this.nextDirection);
}
// 判断下一次要移动的方向
if("U".equals(nextDirection) || "D".equals(nextDirection)){
// 纵坐标相等时,改变方向
if(coordinate[0][0] == foodCoordinateX){
this.direction = nextDirection;
}
}else{
// 横坐标相等时,改变方向
if(coordinate[0][1] == foodCoordinateY){
this.direction = nextDirection;
}
}
}