实验二 贪吃蛇

一、实验要求

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 snakeHead = new ArrayList<>();
 
    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 bean = new ArrayList<>();
    
    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 = new ArrayList<>();
                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 food, List head) {
 
        List list = new ArrayList<>();
        Iterator iterator = snake.get(snake.size() - 1).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 iterator = list.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);
            }
        });
至此贪吃蛇游戏基本实现。
 

你可能感兴趣的:(java,开发语言)