一、实验要求
1)、实现贪吃蛇游戏基本功能,屏幕上随机出现一个“食物”,称为豆子,上下左右控制“蛇”的移动,吃到“豆子”以后“蛇”的身体加长一点。
2)、“蛇”碰到边界或蛇头与蛇身相撞,蛇死亡,游戏结束。
3)、为游戏设计友好的交互界面;例如欢迎界面,游戏界面,游戏结束界面。要有开始键、暂停键和停止退出的选项。
4)、对蛇吃到豆子进行分值计算,可以设置游戏速度,游戏音乐等拓展元素。
二、实验步骤
1、首先是搭建游戏基本界面框架
搭建窗口:创建一个JFrame的继承类Frame,完成窗口的基本设置(窗口尺寸,名称,窗口可视化等等)
this.setTitle("贪吃蛇");
this.setSize(500, 600);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
//窗体居中
this.setLocationRelativeTo(null);
this.setVisible(true);
创建一个JPanel的继承类GamePanel,来装游戏的基本界面
首先考虑游戏过程中需要的按钮
基本的游戏按钮需要设置开始,暂停,退出,在这里为了简洁,我把开始和暂停设置在同一个按钮上,根据不同的情况触发不同的功能除此之外,还需要设置一个JLable用于显示当前分数。并对这些组件进行基本设置,如背景色,字体大小等。
设置好后,全部添加到Frame的NORTH位置。
String str = new String("0");
JLabel score = new JLabel(str);
JPanel panel = new JPanel();
JButton start = new Button("暂停");
JButton esc = new Button("退出");
score.setFont(new Font("", Font.PLAIN, 25));
score.setForeground(new Color(121, 135, 162));
System.out.println(score);
JPanel button = new JPanel();
button.setLayout(new GridLayout(1, 3));
button.add(start);
button.add(esc);
button.add(score);
panel.setBorder(new EmptyBorder(20, 0, 7, 0));
panel.setLayout(new BorderLayout());
panel.setOpaque(false);
panel.add(button);
panel.setVisible(true);
frame.add(panel, BorderLayout.NORTH);
2、贪吃蛇算法实现
1)、首先要实现的是蛇头和食物的随机生成
创建一个关于蛇的类Snake,并在其中设置两个列表,一个snake列表,存放除蛇头以外的所有蛇身的坐标,一个snakeHead列表 ,存放蛇头坐标。并且设置两个全局变量x,y记录蛇头坐标。
并分别给蛇头和蛇身设置图片,以及游戏界面背景图片
//蛇身坐标;
List> snake = new ArrayList<>();
//蛇头坐标;
List
public int x;
public int y;
public ImageIcon head = new ImageIcon("C:\\Users\\miracle\\OneDrive\\图片\\贪吃蛇素材\\3.png");
public ImageIcon head1 = new ImageIcon("C:\\Users\\miracle\\OneDrive\\图片\\贪吃蛇素材\\1.png");
public ImageIcon head2 = new ImageIcon("C:\\Users\\miracle\\OneDrive\\图片\\贪吃蛇素材\\2.png");
public ImageIcon body = new ImageIcon("C:\\Users\\miracle\\OneDrive\\图片\\贪吃蛇素材\\水母2.png");
public ImageIcon backGround = new ImageIcon("C:\\Users\\miracle\\OneDrive\\图片\\贪吃蛇素材\\比奇宝.jpg");
同样的,创建一个豆子的类Food,设置全局变量x,y和列表food来存放豆子坐标。
public class Food {
public int x;
public int y;
List
public ImageIcon food = new ImageIcon("C:\\Users\\miracle\\OneDrive\\图片\\贪吃蛇素材\\水母王.png");
}
注意:在上面两个类中,设置列表来存放坐标是为了之后方便用存放列表的列表snake进行判断是否包含相关列表。而又用x,y来存放坐标是为了方便改动坐标。
在主游戏界面GamePanel中分别创建这两个类的对象,s和f。
Snake s = new Snake();
Food f = new Food();
给s中的蛇头坐标和f中的食物坐标分别附一个随机初始值。
do {
s.x = random.nextInt(0, 460);
} while (s.x % 25 != 0);
do {
s.y = random.nextInt(20, 460);
} while (s.y % 25 != 0);
do {
do {
f.x = random.nextInt(0, 460);
} while (f.x % 25 != 0);
do {
f.y = random.nextInt(20, 460);
} while (f.y % 25 != 0);
} while (s.x == f.x && s.y == f.y);
注意:蛇头和食物坐标不能重合!并且要确保移动后能够重合坐标。
2)、贪吃蛇移动实现
首先给开始和暂停按钮设置键盘监听。在这里,我用键盘按键的code值来进行判断并设置相应动作。
start.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
//int x = 0, y = 0;
switch (e.getKeyCode()) {
case 38:
//str = "up";
y = -25;
x = 0;
img=s.head;
break;
case 40:
//str = "down";
y = 25;
x = 0;
img=s.head;
break;
case 37:
//str = "left";
x = -25;
y = 0;
img=s.head1;
break;
case 39:
//str = "right";
x = 25;
y = 0;
img=s.head2;
break;
default:
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
}
});
当按键为“上”时,则蛇头纵坐标需要移动-25,横坐标不变。其中x,y是全局变量,用于存储每次蛇的坐标的变化情况。
但 仅仅这样还不能让蛇动起来,需要面板不断进行刷新,才能实现蛇的移动。
新建一个Thread的子类ReThread,用于面板的刷新,重绘。
public class Rethread extends Thread {
GamePanel gamePanel;
int speed;
public Rethread(GamePanel gamePanel, int speed) {
this.gamePanel = gamePanel;
this.speed = speed;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(speed);
} catch (Exception e) {
e.printStackTrace();
}
gamePanel.repaint();
}
}
}
并在Game Panel主游戏界面中生成对象调用
Rethread rt = new Rethread(this, speed);
rt.start();
然后重写paintComponent方法,用于重绘面板
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(backGround.getImage(), 0, 0, this.getWidth(), this.getHeight(), this);
将蛇和食物设置的图片画在界面中
ImageIcon bean = f.food;
ImageIcon backGround = s.backGround;
ImageIcon b = s.body;
ImageIcon img = s.head;
//画出蛇和食物;
g.drawImage(img.getImage(), s.x, s.y, 25, 25, this);
g.drawImage(bean.getImage(), f.x, f.y, 25, 25, this);
根据之前通过键盘按键得到的蛇的坐标变化情况来改变蛇头的坐标。
s.x = s.x + x;
s.y = s.y + y;
s.snakeHead.set(0, s.x);
s.snakeHead.set(1, s.y);
蛇头坐标改变后,需要判断是否越界,也就是离开游戏界面,若出界,则蛇身清空,游戏结束,打开新的提示界面
if (s.x > 460 || s.x < 0 || s.y > 460 || s.y < 0) {
if (best < sc) {
best = sc;
}
s.snake.removeAll(s.snake);
stop = 1;
OverFrame of = new OverFrame();
of.init(frame, best, speed);
}
此时需要判断,是否只有蛇头,也就是snake列表是否为空。
当蛇身长度不为空时,判断蛇头是否与蛇身的某一坐标重合,若相撞,则打开新的提示界面,结束游戏
if (s.snake.size() >= 1 && s.snake.contains(s.snakeHead)) {
s.snake.removeAll(s.snake);
if (best < sc) {
best = sc;
}
stop = 1;
OverFrame of = new OverFrame();
of.init(frame, best, speed);
}
当snake列表为空时,再判断蛇头是否与食物的坐标重合,若重合,则蛇身长度加1,并将蛇头的上一坐标作为蛇尾的坐标,食物重新随机获取坐标(仍然不能与蛇身重合,且不能越界)
if (s.x == f.x && s.y == f.y && s.snake.size() == 0) {
List
list.add(s.x - x);
list.add(s.y - y);
//s.snake.set(0, s.snakeHead);
s.snake.add(list);
sc = 10;
do {
do {
f.x = random.nextInt(0, 460);
} while (f.x % 25 != 0);
do {
f.y = random.nextInt(20, 460);
} while (f.y % 25 != 0);
f.bean.set(0, f.x);
f.bean.set(1, f.y);
} while (s.snake.contains(f.bean) || s.snakeHead.equals(f.bean));
}
注意:这里要先判断snake是否不为空,再判断是否为空,否则,当snake为空且蛇头与食物 坐标重合时,snake会从空变成不为空,若再进行不为空的判断就会出错。
如若游戏还没结束,则蛇身坐标接着移动,这里我是新建一个类来专门进行蛇身移动的计算。主要思想是,每个身体段的坐标都变成上一段身体的坐标,第0段的身体坐标则是蛇头上一个坐标。
public class Move {
public List> move(List
> snake, int x, int y, List
List
Iterator
int m = iterator.next();
int n = iterator.next();
//蛇尾坐标;
list.add(m);
list.add(n);
for (int i = snake.size() - 1; i > 0; i--) {
snake.get(i).set(0, snake.get(i - 1).get(0));
snake.get(i).set(1, snake.get(i - 1).get(1));
}
snake.get(0).set(0, x);
snake.get(0).set(1, y);
if (head.equals(food)) {
snake.add(list);
}
return snake;
}
}
注意:由于存储列表的列表的特殊性:会随着内部列表的变化而变化,因此,我使用了两个整型变量来存放原有蛇尾坐标。如果吃到豆子,则蛇尾增加原有蛇尾坐标。
在Game Panel判断snake不为空后,进行调用
if (s.snake.size() >= 1) {
s.snake = m.move(s.snake, s.x - x, s.y - y, f.bean, s.snakeHead);
//吃掉豆子后,豆子重新获取;
if (s.snakeHead.equals(f.bean)) {
sc = sc + 10;
do {
do {
f.x = random.nextInt(0, 460);
} while (f.x % 25 != 0);
do {
f.y = random.nextInt(20, 460);
} while (f.y % 25 != 0);
f.bean.set(0, f.x);
f.bean.set(1, f.y);
} while (s.snake.contains(f.bean) || s.snakeHead.equals(f.bean));
}
}
实现了整条蛇的移动后,还要画出移动后的蛇的图案。在此之前,已经将蛇头和食物画在了面板上,接下来就是画出移动后的蛇身。在这里,我调用循环来依次画出每一段蛇身。
for (int i = 0; i < s.snake.size(); i++) {
List list = new ArrayList();
list.add(s.snake.get(i).get(0));
list.add(s.snake.get(i).get(1));
Iterator
int c = iterator.next();
int d = iterator.next();
g.drawImage(b.getImage(), c, d, 25, 25, this);
s.snake.get(i).set(0, s.snake.get(i).get(0));
s.snake.get(i).set(1, s.snake.get(i).get(1));
}
最后再在label上显示出分数,我这里设置的是吃掉一颗豆子,得10分。
score.setText("分数:" + sc);
至此,贪吃蛇的主要游戏界面和主要算法基本实现
3、开始界面和游戏结束界面的搭建
1)、创建一个JPanel继承类StartPanel用于设置开始界面的组件内容。
在此需要包含的组件有游戏名称(JLabel)、开始按钮、速度按钮、历史最高纪录按钮。设置网格布局模式,并按4排1列的方式排列。并设置好这几个组件的外观。(为了增加趣味性,我把游戏名称设置为“抓水母”。)
给三个按钮设置按键监听。
点击开始按钮时,关闭当前窗口,打开之前设置的主要游戏窗口。
start.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
frame.dispose();
Frame frame = new Frame();
frame.init(best);
frame.game(speed);
}
});
点击速度按钮时,改变速度,也就是面板刷新速度,并用相关字符串给出提示
set.addActionListener(new ActionListener() {
int i = 0;
@Override
public void actionPerformed(ActionEvent e) {
if (i == 3) {
set.setText("ffffast");
i++;
speed = 50;
}
if (i == 2) {
set.setText("fast");
i++;
speed = 150;
}
if (i == 1) {
set.setText("normal");
i++;
speed = 300;
}
if (i == 0) {
set.setText("slow");
i++;
speed = 500;
}
if (i == 4) {
i = 0;
}
}
});
点击历史最高按钮时,显示最高分数
history.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
history.setText(best + "");
}
});
}
其中,为了存储和传递速度和最高分数这两个数字,我在每个相关的类中都设置了speed和score两个全局变量,并且调用这些类的对象时,也要进行参数传递,防止出现不匹配的问题。
2)、创建一个JFrame的继承类OverFrame作为结束提示
其中包含“Game over”的JLabel,“重新开始”按钮,“退出”按钮。
当点击重新开始按钮时,关闭当前前窗口,重新打开游戏主界面
yes.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
frame.dispose();
Frame frame = new Frame();
frame.init(best);
frame.game(speed);
}
});
点击退出按钮后,回到开始界面。
esc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
OverFrame.this.dispose();
frame.dispose();
Frame frame = new Frame();
frame.init(best);
StartPanel startPanel = new StartPanel(best, frame);
frame.add(startPanel);
}
});
至此贪吃蛇游戏基本实现。