学习用Java写简单贪吃蛇第二天
- 准备开始
- 贪吃蛇结点的构建
- 页面的显示搭建
- 大部分功能的实现
- 贪吃蛇的开始初始化
- 放置普通食物
- 判断蛇是否咬到了自己
- 设置吃到食物后的发生
- 设置方向键的限定
- 时间时钟到了执行的方法
- 绘图
- 完善
- 吃完特殊食物会怎么样
- 当蛇的长度到达一定长度会怎么样
- 游戏音乐的开启
- 游戏上方按钮的绘制
- 按钮的创建
- 按钮的初始化
- 根据难度以及模式判断
- 具体展示
- 总结
准备开始
贪吃蛇结点的构建
结点是使用双向链表来搭建的,里面定义了x和y分别表示结点所处的位置,用next代表下一个结点,用prev代表前一个结点,而之前的方向,定义在主方法中。
package exp2;
public class SnakeNode {
private int x;
private int y;
protected SnakeNode next;
protected SnakeNode prev;
public SnakeNode(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
页面的显示搭建
这里写的是关于设置页面标题,设置页面图标,设置页面大小,以及上一篇文章中说的将贪吃蛇画的JPanel进入加载。
package exp2;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
public class Snake {
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setBounds(450,160,900,758);
frame.setResizable(false);
//点击关闭能真正关闭
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//添加画布
frame.add(new exp2.MyPanel());
//设置标题和icon
frame.setTitle("贪吃蛇小游戏");
URL url = frame.getClass().getResource(".\\..\\..\\images\\snakeIcon.png");
//System.out.println(url);
Image img = Toolkit.getDefaultToolkit().getImage(url);
frame.setIconImage(img);
frame.setVisible(true);
}
}
大部分功能的实现
贪吃蛇的开始初始化
对于如何在程序运行时,我将贪吃蛇的初始化封装成了一个方法,只要在JPanel刚创建调用构造方法时进行调用。
private void initSnake() {
SnakeNode snake1 = new SnakeNode(125,135);
initNode(snake1);
SnakeNode snake2 = new SnakeNode(100,135);
initNode(snake2);
SnakeNode snake3 = new SnakeNode(75,135);
initNode(snake3);
setMinFood();
bigFoodNum = 1;
direction = "R";
score = 0;
setBigFoodXY();
hasBigFood = false;
time.start();
}
private void initNode(SnakeNode node) {
if (first == null) {
p = node;
first = node;
end = node;
p.next = null;
p.prev = null;
} else {
p.next = node;
node.prev = p;
p = p.next;
end = p;
}
len ++;
}
里面设置了很多参数,比如初始方向为向右,设置初始食物位置,是否需要设置特殊食物,设置开始分数等等。(具体代码在文末)
放置普通食物
放置食物需要考虑到,食物的位置会不会超出frame框的位置大小,放置的位置会不会和蛇或者特殊食物重叠。
private void setMinFood() {
Boolean setFood;
do {
setFood = true;
foodx = 25 + 25 * rand.nextInt(34);
foody = 110 + 25 * rand.nextInt(24);
p = first;
// 防止生成的食物在蛇身上
while (p != null) {
if (p.getX() == foodx && p.getY() == foody) {
setFood = false;
break;
}
p = p.next;
}
if ((foodx == bigFoodx && foody == bigFoody)
|| (foodx == bigFoodx -25 && foody == bigFoody)
|| (foodx == bigFoodx && foody == bigFoody - 25)
|| (foodx == bigFoodx -25 && foody == bigFoody - 25)) {
setFood = false;
}
} while (!setFood);
}
当然,放置特殊食物也是这样。
判断蛇是否咬到了自己
从头结点以下开始遍历,如果遍历到了身子结点(暂且这么讲),处于的位置相同,说明咬到了自己,返回true代表游戏失败了。在其他方法调用此方法时,就可以判断游戏需要终止了。
private boolean failedCheckOut() {
p = first.next;
while (p != null) {
if (p.getX() == first.getX() && p.getY() == first.getY()) {
return true;
}
p = p.next;
}
return false;
}
设置吃到食物后的发生
这里就不详细解释了,具体解释请看第一天内容,谢谢大家!
private void getMinFood() {
toEnd.next = null;
toEnd.prev = end;
end.next = toEnd;
end = toEnd;
score += 10 * difficulty;
bigFoodNum++;
len ++;
}
设置分数的增加与难度成正比,这里的bigFoodNum是用来判断吃到多少普通食物就会生成一个特殊食物,以及蛇的长度增加。
设置方向键的限定
为什么要限定方向键?您想,如果蛇是往右走的,如果您的方向键按了一下左,下一秒可能直接就咬到自己了,像我这样手比较滑的人,轻轻按一下整个游戏就失败了,所以我设置了方向键的限定。当然,下面的内容也包含了如何设置空格键为游戏的开始与暂停,以及重新开始。
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();//获取键盘背后对应的数字
if (keyCode == KeyEvent.VK_SPACE) {
if (isFailed) {
isFailed = false;
toRestart();
} else {
isStarted = !isStarted;
}
if (isStarted) {
playBGM();
} else {
stopBGM();
}
repaint();
} else if (keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_D) {
tempDir = "L";
direction = tempDir.equals(direction ) ? "L" : "R";
} else if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_A) {
tempDir = "R";
direction = tempDir.equals(direction) ? "R" : "L";
} else if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_W) {
tempDir = "D";
direction = tempDir.equals(direction) ? "D" : "U";
} else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_S) {
tempDir = "U";
direction = tempDir.equals(direction) ? "U" : "D";
}
}
时间时钟到了执行的方法
Java中有一个Timer类,创建的对象可以每隔一段时间调用下面的方法。下面这个方法可以改变蛇的位置,判断是否吃到了食物。里面也有许多下面一部分要讲的完善内容,具体请看下面一部分。
//时钟时间到了就会调用这个移动方法
@Override
public void actionPerformed(ActionEvent e) {
if (isStarted && !isFailed) {
//蛇进行移动
//防止end结点改变,新建时比较简单
toEnd = new SnakeNode(end.getX(),end.getY());
p = end.prev;
if (direction == "R") {
end.setX(first.getX() + speed);
end.setY(first.getY());
if (end.getX() >= 875 && model == 1) {
end.setX(25);
} else if (end.getX() >= 875 && model == 2) {
isFailed = true;
}
} else if (direction == "L") {
end.setX(first.getX() - speed);
end.setY(first.getY());
if (end.getX() <= 0 && model == 1) {
end.setX(850);
} else if (end.getX() <= 0 && model == 2) {
isFailed = true;
}
} else if (direction == "U") {
end.setX(first.getX());
end.setY(first.getY() - speed);
if (end.getY() <= 85 && model == 1) {
end.setY(685);
} else if (end.getY() <= 85 && model == 2) {
isFailed = true;
}
} else if (direction == "D") {
end.setX(first.getX());
end.setY(first.getY() + speed);
if (end.getY() >= 710 && model == 1) {
end.setY(110);
} else if (end.getY() >= 710 && model == 2) {
isFailed = true;
}
}
p.next = null;
end.next = first;
first.prev = end;
first = end;
end = p;
//蛇头与食物是否重合
if (first.getX() == foodx && first.getY() == foody) {
getMinFood();
setMinFood();
}
if ((first.getX() >= bigFoodx)
&& (first.getX() <= bigFoodx + 25)
&& (first.getY() >= bigFoody)
&& (first.getY() <= bigFoody + 25)) {
getBigFood();
slowTimes += 20;
}
if (failedCheckOut()) {
isFailed = true;
stopBGM();
}
repaint();
}
//time.setDelay(timeDely);
if (slowTimes != 0) {
time.setDelay(200);
setBackground(Color.BLUE);
slowTimes --;
} else if (difficulty == easy) {
time.setDelay(150);
} else if (difficulty == medium) {
time.setDelay(100);
} else if (difficulty == hard) {
time.setDelay(60);
}
time.start();
}
绘图
将上面讲的总结起来,在绘图功能中实现,每隔很短的时间执行绘图操作,对于我们就感觉在动一样啦。
public void paintComponent(Graphics g) {
super.paintComponent(g);
//添加标题
title.paintIcon(this,g,25,11);
//绘制游戏区
g.fillRect(25,110,850,600);
//打印分数和长度
g.setColor(Color.WHITE);
g.setFont(new Font("arial", Font.PLAIN, 30));
g.drawString("Length:" + len,730,50);
g.drawString("Score:" + score,730,80);
//打印蛇
if (direction.equals("R")) {
right.paintIcon(this, g, first.getX(), first.getY());
} else if (direction.equals("L")) {
left.paintIcon(this, g, first.getX(), first.getY());
} else if (direction.equals("U")) {
up.paintIcon(this, g, first.getX(), first.getY());
} else if (direction.equals("D")) {
down.paintIcon(this, g, first.getX(), first.getY());
}
p = first.next;
while (p != null) {
body.paintIcon(this, g, p.getX(), p.getY());
p = p.next;
}
//打印食物
food.paintIcon(this,g,foodx,foody);
if (len < 150) {
if (bigFoodNum % 6 == 0) {
hasBigFood = true;
}
if (hasBigFood) {
bigFood.paintIcon(this,g,bigFoodx,bigFoody);
}
}
if (len == 10) {
g.setColor(Color.YELLOW);
g.setFont(new Font("arial", Font.BOLD, 40));
g.drawString("So nice!!!",100,200);
}
//显示背景
if (slowTimes != 0) {
setBackground(Color.GREEN);
g.setColor(Color.WHITE);
g.setFont(new Font("arial", Font.BOLD, 40));
g.drawString("Slow down!",600,600);
} else {
setBackground(Color.WHITE);
}
//打印开始
if (!isStarted && !isFailed) {
g.setColor(Color.YELLOW);
g.setFont(new Font("arial", Font.BOLD, 40));
g.drawString("Press Space to Start",300,300);
}
//重新开始
if (isFailed) {
g.setColor(Color.RED);
g.setFont(new Font("arial", Font.BOLD, 40));
g.drawString("Failed! Press Space to Restart",200,300);
}
}
完善
非常感谢您能读到这里,谢谢您!相信如果您仔细看的话,上面的内容里有许多我已经完善好的东西。就比如说:
吃完特殊食物会怎么样
(很抱歉不能再复制一次上面的代码,影响您的阅读,因为会比较长,您可以直接在文末下载代码来阅读,谢谢)在paintComponent方法中,如果吃下特殊食物,slowTimes会增加一定的数,当它不为0时,我设置了背景颜色变为绿色,以及显示Slow down和速度变为200的时间内刷新,为0背景是白色的。
此处在paintComponent方法中
//显示背景
if (slowTimes != 0) {
setBackground(Color.GREEN);
g.setColor(Color.WHITE);
g.setFont(new Font("arial", Font.BOLD, 40));
g.drawString("Slow down!",600,600);
} else {
setBackground(Color.WHITE);
}
private void getBigFood() {
toEnd.next = null;
toEnd.prev = end;
end.next = toEnd;
end = toEnd;
score += 20 * difficulty;
len ++;
bigFoodNum ++;
hasBigFood = false;
slowTimes += 20;
setBigFoodXY();
}
此处在actionPerformed方法中
//time.setDelay(timeDely);
if (slowTimes != 0 && isStarted) {
time.setDelay(200);
setBackground(Color.BLUE);
slowTimes --;
} else if (difficulty == easy) {
time.setDelay(150);
} else if (difficulty == medium) {
time.setDelay(100);
} else if (difficulty == hard) {
time.setDelay(60);
}
time.start();
当蛇的长度到达一定长度会怎么样
我这里只写了会在界面打印“So nice”,比较菜,见谅。(比如可以速度加快什么的)
if (len == 10) {
g.setColor(Color.YELLOW);
g.setFont(new Font("arial", Font.BOLD, 40));
g.drawString("So nice!!!",100,200);
}
游戏音乐的开启
private void LoadBGM() {
try {
//如果bgm文件比较大,使用SourseDataLine
bgm = AudioSystem.getClip();
InputStream is = this.getClass().getResourceAsStream("..\\music\\bgm.wav");
AudioInputStream ais = AudioSystem.getAudioInputStream(new BufferedInputStream(is));
bgm.open(ais);
} catch (LineUnavailableException e) {
e.printStackTrace();
} catch (UnsupportedAudioFileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
音乐的循环播放以及按下space键的停止播放
private void playBGM() {
bgm.loop(Clip.LOOP_CONTINUOUSLY);
}
private void stopBGM() {
bgm.stop();
}
游戏上方按钮的绘制
按钮的创建
private final JMenuBar jMenuBar = new JMenuBar();
private final JMenu choose = new JMenu("选项");
private final JMenuItem stop = new JMenuItem("暂停");
private final JMenuItem start = new JMenuItem("开始");
private final JMenu difChoose = new JMenu("难度选择");
private final JMenuItem dif1 = new JMenuItem("难度1");
private final JMenuItem dif2 = new JMenuItem("难度2");
private final JMenuItem dif3 = new JMenuItem("难度3");
private final JMenu modelChoose = new JMenu("模式选择");
private final JMenuItem model1 = new JMenuItem("不碰壁模式");
private final JMenuItem model2 = new JMenuItem("碰壁模式");
private final JMenuItem exit = new JMenuItem("退出");
按钮的初始化
private void initItem() {
difChoose.add(dif1);
difChoose.add(dif2);
difChoose.add(dif3);
modelChoose.add(model1);
modelChoose.add(model2);
jMenuBar.add(start);
jMenuBar.add(stop);
jMenuBar.add(difChoose);
jMenuBar.add(modelChoose);
jMenuBar.add(exit);
//panel.add(popMenu);
//设置字体
choose.setFont(new Font("宋体",Font.BOLD,20));
difChoose.setFont(new Font("宋体",Font.BOLD,20));
modelChoose.setFont(new Font("宋体",Font.BOLD,20));
setJMenuItemFont(dif1);
setJMenuItemFont(dif2);
setJMenuItemFont(dif3);
setJMenuItemFont(model1);
setJMenuItemFont(model2);
setJMenuItemFont(start);
setJMenuItemFont(stop);
setJMenuItemFont(exit);
this.add(jMenuBar);
start.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (isFailed) {
isFailed = false;
toRestart();
} else {
isStarted = true;
}
playBGM();
repaint();
}
});
stop.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
isStarted = false;
stopBGM();
repaint();
}
});
dif1.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
difficulty = easy;
}
});
dif2.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
difficulty = medium;
}
});
dif3.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
difficulty = hard;
}
});
model1.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
model = 1;
}
});
model2.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
model = 2;
}
});
exit.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
System.exit(0);
}
});
}
根据难度以及模式判断
模式为1表明为穿墙模式,模式为2表明为不可穿墙模式
此处的1和2没有见名知意,不好意思。
if (direction == "R") {
end.setX(first.getX() + speed);
end.setY(first.getY());
if (end.getX() >= 875 && model == 1) {
end.setX(25);
} else if (end.getX() >= 875 && model == 2) {
isFailed = true;
}
难度的设定:简单时间延迟设定为150,中等难度设定为100,困难为60(如果迟到特殊食物设定为200)
//time.setDelay(timeDely);
if (slowTimes != 0 && isStarted) {
time.setDelay(200);
setBackground(Color.BLUE);
slowTimes --;
} else if (difficulty == easy) {
time.setDelay(150);
} else if (difficulty == medium) {
time.setDelay(100);
} else if (difficulty == hard) {
time.setDelay(60);
}
time.start();
具体展示
总结