一、项目要求:
1)实现贪吃蛇游戏基本功能,屏幕上随机出现一个“食物”,称为豆子。玩家能利用上下左右键控制“蛇”的移动,“蛇”吃到“豆子”后“蛇”身体加长一节,得分增加,“蛇”碰到边界或蛇头与蛇身相撞,“蛇”死亡,游戏结束。
2)进行交互界面的设计,要有开始键、暂停键和停止退出的选项,能够控制游戏进程。对蛇吃到豆子进行分值计算,可以设置游戏速度,游戏音乐等拓展元素。
二、 算法思想:
三、代码实现:
- (游戏主体框架).MainFrame类:
package Proj_GreedySnake;
/*
*主框架
*
* 已知bug:
* 方向按键按的太快,蛇会吃到自己
* */
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;
//继承父类JFrame
public class MainFrame extends JFrame {
private JPanel Gamepanel;
private JLabel scorelab;
private Snake snake;//声明一个蛇的主体
private Timer timer;//声明一个计时器
private SnakeNode Food;//声明食物
private int score = 0;
private int speed;//初始为normal
public MainFrame() throws HeadlessException {
//设置得分版
initSideBar();
//设置窗体
setTitle("贪吃蛇");
setResizable(false);
setSize(615 + 200, 640);
setLayout(null);
setLocationRelativeTo(null);
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setFocusable(true);//设置窗体可获取焦点,若为false,按钮与按键事件会出现冲突
//设置游戏速度
setspeed();
//设置网格,绘制蛇身,食物等操作
initGamePanel();
//初始化设置蛇身
initSnake();
//初始化设置计时器
initTimer();
//设置操纵蛇的方向的键盘事件响应
setKeyboardListenner();
//绘制食物到网格上
initFood();
}
private void setspeed() {
JOptionPane.showMessageDialog(null, "Set your speed",
"GreedySnake", JOptionPane.WARNING_MESSAGE);
String[] options = {"easy", "normal", "hard"};
String temp = (String) JOptionPane.showInputDialog(null, "Choose your rank (don't press cancel butoon)",
"Set speed", JOptionPane.INFORMATION_MESSAGE, null, options, options[1]);
switch (temp) {
case "easy" -> speed = 300;
case "normal" -> speed = 100;
case "hard" -> speed = 50;
}
}
private void initFood() {
Food = new SnakeNode();
Food.randomLocation();
}
private void setKeyboardListenner() {
addKeyListener(new KeyAdapter() {
@Override
//设置每个按钮对应的响应事件(设置方向按钮),同时设置两个反方向的按钮不能一起响应
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
if (snake.getDirection() != Direction.DOWN)
snake.setDirection(Direction.UP);
break;
case KeyEvent.VK_DOWN:
if (snake.getDirection() != Direction.UP)
snake.setDirection(Direction.DOWN);
break;
case KeyEvent.VK_LEFT:
if (snake.getDirection() != Direction.RIGHT)
snake.setDirection(Direction.LEFT);
break;
case KeyEvent.VK_RIGHT:
if (snake.getDirection() != Direction.LEFT)
snake.setDirection(Direction.RIGHT);
break;
}
}
});
}
private void initTimer() {
timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
//蛇移动
snake.move();
//蛇吃食物
SnakeNode head = snake.getBody().getFirst();
if (head.getX() == Food.getX() && head.getY() == Food.getY()) {
snake.eatFood();
Food.randomLocation();
score += 10;
scorelab.setText("得分: " + score);
}
Gamepanel.repaint();//刷新网格
//若蛇死了,游戏结束
if (!snake.isLiving()) {
JOptionPane.showMessageDialog(null, "Game Over"+'\n'+"Your score: "+score, "GreedySnake", JOptionPane.WARNING_MESSAGE);
timer.cancel();//清除本次定时器任务
}
}
};
//设置定时任务的执行周期(设置蛇跑的速度)
timer.scheduleAtFixedRate(timerTask, 0, speed);
}
private void initSnake() {
snake = new Snake();
}
private void initGamePanel() {
Image backImg = ImageUtil.getImage("Proj_GreedySnake/img/background.jpg");
Image food = ImageUtil.getImage("Proj_GreedySnake/img/food.png");
Image head = ImageUtil.getImage("Proj_GreedySnake/img/doge.png");
Gamepanel = new JPanel() {
@Override
//绘制网格与蛇身
public void paint(Graphics g) {
//刷新网格前,要清除上一次绘制的网格
g.clearRect(0, 0, 615, 640);
//绘制背景图
g.drawImage(backImg, 0, 0, null);
//绘制网格,如果只想保留背景图可把这段代码删掉
for (int i = 0; i <= 40; i++) {
g.drawLine(0, i * 15, 600, i * 15);
}
for (int i = 0; i <= 40; i++) {
g.drawLine(i * 15, 0, i * 15, 600);
}
//把蛇身绘制上去
LinkedList<SnakeNode> body = snake.getBody();
SnakeNode Head = snake.getBody().getFirst();
g.drawImage(head, Head.getX() * 15, Head.getY() * 15, 15, 15, this);
for (int i = 1; i < body.size(); i++) {
g.setColor(Color.ORANGE);
g.fillRoundRect(body.get(i).getX() * 15, body.get(i).getY() * 15, 15, 15, 10, 10);//获取每个蛇身节点的位置,并绘制
}
//在绘制食物前先检查蛇身和食物是否重叠
for (SnakeNode i : snake.getBody()) {
if (Food.getX() == i.getX() && Food.getY() == i.getY())
Food.randomLocation();
}
//绘制食物
g.drawImage(food, Food.getX() * 15, Food.getY() * 15, 20, 20, null);
}
};
Gamepanel.setSize(615, 640);
add(Gamepanel);
}
private void initSideBar() {
JButton restart;
JButton pause;
JButton keepGo;
//设置暂停按钮
pause = new JButton("暂停");
pause.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
timer.cancel();
requestFocus();
}
});
pause.setBounds(675, 350, 100, 30);
add(pause);
//设置继续按钮
keepGo = new JButton("继续");
keepGo.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
timer.cancel();
initTimer();
requestFocus();
}
});
keepGo.setBounds(675, 400, 100, 30);
add(keepGo);
//设置重开按钮
restart = new JButton("重新开始");
restart.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
timer.cancel();
Food.randomLocation();
JOptionPane.showMessageDialog(null, "Restart", "GreedySnake", JOptionPane.WARNING_MESSAGE);
//选择难度
setspeed();
//重新初始化蛇
initSnake();
//初始化计时器任务
initTimer();
//设置点击按钮后,窗口重新获得焦点,否则会与键盘事件冲突
requestFocus();
//重新初始化游戏得分
score = 0;
scorelab.setText("得分:0");
}
});
restart.setBounds(675, 300, 100, 30);
add(restart);
//设置得分框
scorelab = new JLabel("得分:0");
scorelab.setFont(new Font("wordtype", Font.BOLD, 15));
scorelab.setBounds(675, 250, 100, 50);
add(scorelab);
}
public static void main(String[] args) {
new MainFrame();
}
}
- 创建一个Snake类,构造蛇身以及定义蛇的行为和状态:
package Proj_GreedySnake;
import java.util.LinkedList;
/*
* 使用LinkList构造蛇身
* 蛇的移动,蛇吃食物和状态检测
* */
public class Snake {
private LinkedList<SnakeNode> body;//设的身体
private Direction direction = Direction.UP;//蛇运动的方向,默认向上
public LinkedList<SnakeNode> getBody() {
return body;
}
public void setBody(LinkedList<SnakeNode> body) {
this.body = body;
}
public Snake() {
initSnake();
}
public Direction getDirection() {
return direction;
}
public void setDirection(Direction direction) {
this.direction = direction;
}
//初始化蛇身,
private void initSnake() {
body = new LinkedList<>();
//为蛇添加三个SnakeNode,即蛇的身长为3
body.add(new SnakeNode(20, 15));
body.add(new SnakeNode(20, 16));
body.add(new SnakeNode(20, 17));
}
//声明一个蛇移动的方法,思路是获取蛇的头部坐标,当进行移动时蛇头坐标发生变化,向蛇头增加一个新的SnakeNode,蛇尾删除节点
public void move() {
//如果蛇还活着可以移动
if (isLiving()) {
SnakeNode head = body.getFirst();
switch (direction) {
case UP -> body.addFirst(new SnakeNode(head.getX(), head.getY() - 1));
case DOWN -> body.addFirst(new SnakeNode(head.getX(), head.getY() + 1));
case LEFT -> body.addFirst(new SnakeNode(head.getX() - 1, head.getY()));
case RIGHT -> body.addFirst(new SnakeNode(head.getX() + 1, head.getY()));
}
body.removeLast();
}
}
public void eatFood() {
SnakeNode head = body.getFirst();
switch (direction) {
case UP -> body.addFirst(new SnakeNode(head.getX(), head.getY() - 1));
case DOWN -> body.addFirst(new SnakeNode(head.getX(), head.getY() + 1));
case LEFT -> body.addFirst(new SnakeNode(head.getX() - 1, head.getY()));
case RIGHT -> body.addFirst(new SnakeNode(head.getX() + 1, head.getY()));
}
}
//判断蛇的死亡状态
public boolean isLiving() {
boolean isliving = true;
//如果蛇碰到边界,则isLiving置false
SnakeNode head = body.getFirst();
if (head.getX() < 0 || head.getY() < 0 || head.getX() >= 40 || head.getY() >= 40) {
isliving = false;
}
//如果蛇碰到自己身体,也会死亡
for (int i = 1; i < body.size(); i++) {
SnakeNode node = body.get(i);
if (head.getX() == node.getX() && head.getY() == node.getY()) {
isliving = false;
break;
}
}
return isliving;
}
}
- 创建一个SnakeNode类,获取蛇的坐标,以及为食物生成随机坐标:
package Proj_GreedySnake;
/*
* 创建坐标节点记录蛇的身体的坐标
* */
import java.util.Random;
public class SnakeNode {
private int x;
private int 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;
}
public SnakeNode(int x, int y) {
this.x = x;
this.y = y;
}
public SnakeNode() {
}
//在格子内生成随机坐标
public void randomLocation(){
Random random = new Random();
this.x= random.nextInt(40);
this.y= random.nextInt(40);
}
}
- 创建一个枚举类,设定方向参数
package Proj_GreedySnake;
/*
* 创建枚举类型,为蛇的移动方向设置参量
* */
public enum Direction {
//方向枚举
UP,DOWN,RIGHT,LEFT
}
- 创建一个图片获取类,以获取图片素材
package Proj_GreedySnake;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
/**
* 根据图片的相对路径获取图片
*/
public class ImageUtil {
// @param imagePath, @return 图片
public static Image getImage(String imagePath) {
URL url = ImageUtil.class.getClassLoader().getResource(imagePath);
BufferedImage img = null;
try {
img = ImageIO.read(url);
} catch (IOException e) {
e.printStackTrace();
}
return img;
}
}
四、成品展示: