Java实现贪吃蛇(代码逐行注释)

写在最前

作为本人学习Java的第二篇博客,也是自己练手的第一个小项目。耗费了大量的时间和精力,遇到的问题分享给大家,希望大家可以有所收获。

视频展示

Java贪吃蛇

整体思路

因为所有的游戏都要有一个帧的概念,也就是看似动态的画面实则是由多个静态的、变化的画面组成的,当这样的画面足够多的时候,就有了一个动态的观感。这就是我理解的帧的概念。

有了这个思路,我们只要设计好贪吃蛇每一个静态画面,把它的变化连续起来就可以。

那么他们的变化有什么规律呢?

答案就是,我们把蛇在画面中的运动看做是坐标的不断变化,蛇的每一节与前一节的值都是相等的。

比如说,最开始蛇头的坐标是(1,1),蛇身的坐标是(0,1)当蛇运动的时候,蛇头的坐标变成了(2,1),那么蛇身变成(1,1),蛇的每一节一直等于他的前一节,我们利用这样的规律来制作一个贪吃蛇的小游戏。

贪吃蛇主界面(Startgame类)

首先我们要启动一个窗口,在这个窗口里面不断的进行操作,我用的是Java自带的swing写的。

import javax.swing.*;

public class Startgame {
    public static void main(String[] args) {
        /*
        绘制一个静态窗口
         */
        JFrame frame = new JFrame("贪吃蛇by宋阳");//创建一个窗口,参数是窗口标题显示的文字
        frame.setBounds(430, 180, 900, 720);//设置一个窗口的大小
        frame.setResizable(false);//窗口大小不可以改变
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置窗口可以关闭,此处填写参数3可以
   
        frame.add(new GamePanel());//添加组件
        frame.setVisible(true);//打开窗口
    }
}

对于 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)这行代码可以进行扩展一下; **

setDefaultCloseOperation(int operation)是设置用户在此窗体上发起 “close” 时默认执行的操作。方法中的参数解释如下:


为“0”或DO_NOTHING_ON_CLOSE:
不执行任何操作;要求程序在已注册的WindowListener 对象的 windowClosing 方法中处理该操作。

为“1”或HIDE_ON_CLOSE
调用任意已注册的 WindowListener 对象后自动隐藏该窗体。此时没有关闭程序,只是将程序界面隐藏了.


为“2”或DISPOSE_ON_CLOSE
调用任意已注册 WindowListener 的对象后自动隐藏并释放该窗体。但继续运行应用程序,释放了窗体中占用的资源。


为“3”EXIT_ON_CLOSE(在 JFrame 中定义):使用 System exit 方法退出应用程序。仅在应用程序中使用。结束了应用程序。

默认情况下,该值被设置为 HIDE_ON_CLOSE。

至于JFrame类,就把他理解为Java自带的创建窗口的类,只要通过JFrame下的方法就可以创建一个窗口。

项目相关素材的储存(data类)

我们这个小项目,会相应的使用一些素材,比如身体的小绿方块,四个方向的蛇头。这些素材我单独的放在一个类进行储存,方便后面的使用。

import javax.swing.*;

public class date {
    /*
    此类用于存储图片信息
    image和src是同一个级别的文件夹,可以用image/header.png
     */ 
    public static ImageIcon right = new ImageIcon("image/right.png");
    public static ImageIcon body = new ImageIcon("image/body.png");
    public static ImageIcon down = new ImageIcon("image/down.png");
    public static ImageIcon food = new ImageIcon("image/food.png");
    public static ImageIcon up = new ImageIcon("image/up.png");
    public static ImageIcon left = new ImageIcon("image/left.png");


}

我的相应图片放在了项目文件夹的image文件夹,使用ImageIcon​这个类与图片建立联系。

贪吃蛇实现类(GamePanel类)

前面说过,我们想要让蛇运动起来,需要作出一个静态的、变化的画面,并且把他们连续起来。所以在这门我们采用了继承JPanel类,JPanel 是 Java图形用户界面(GUI)工具包swing中的面板容器类,包含在javax.swing 包中,是一种轻量级容器,可以加入到JFrame窗体中。之后在这个画板里面对蛇的被一个变化不断的绘制。

继承JPanel之后,需要对paintComponent方法进行重写,paintComponent()是swing的一个方法,相当于图形版的main(),是会自执行的。如果一个class中有构造函数,则执行顺序是先执行构造函数,再执行这个。我们想要唉主窗体显示的所有画面都要在这个函数里面完成。

此外,我们也要对小蛇默认的一些参数进行设置(例如:长度,开始的坐标等)

相关代码及代码注释如下:

 public GamePanel() {
        init();//初始化参数     
    }

   
    int lenth;
    int foodx = 0;//设置食物的横坐标
    int foody = 0;//设置食物的纵坐标
    boolean isfood = false;//设置当前是否有食物变量,默认为假
    boolean start = false;//设置当前游戏是否开始变量,默认为假
    boolean iskilled=false;
    String fx = "R";//R 向右 L向左 U向上 D向下
    int[] snakex = new int[999];
    int[] snakey = new int[999];
    Random Ran = new Random();//创建随机数对象,给食物的坐标生成随机数并且赋值

    public void init() {//初始化方法,当本类被构造的时候被执行一次
        fx = "R";
        lenth = 9;//设置蛇的输出长度
        snakex[0] = 200;
        snakey[0] = 100;//头部坐标
        snakex[1] = 200;
        snakey[1] = 100;
        snakex[2] = 179;
        snakey[2] = 100;//第一节坐标
        for (int i = 2; i < lenth; i++) {
            /*
            其他节坐标以此赋值,因为第一节身体坐标与头部一样,
            所以从第二节开始输出
             */
            snakex[i + 1] = snakex[i] - 19;
            snakey[i + 1] = snakey[i];
        }
    }
    }

这样我们就绘制了一个静态的小蛇,那么怎么让他动起来呢?

如果我们不断的改变蛇的坐标,并且不断地执行这个代码,小蛇不就动起来了?

前面说过,小蛇改变坐标的方法就是蛇的后一节一直等于前一节,我们改变蛇的方向和行走的距离,只需要改变蛇头方向就可以完成了。

所以,改变蛇坐标的代码如下:

snakex[0] = snakex[0] + 20;//snakex[0]是蛇头的横坐标。
                for (int i = lenth - 1; i > 0; i--) {//计算蛇的身子的坐标
                    snakex[i] = snakex[i - 1];
                    snakey[i] = snakey[i - 1];
                }

因为我小蛇的坐标是不断的变化,我们需要让小蛇坐标变化的代码不断的运行就利用到了定时器timer的概念。

什么是定时器timer?

在java.swing包中有一个timer类,可以使用它在到达给定的时间间隔时发出通告或者说警告。在构造定时器时,需要设置一个时间间隔,并告知当达到时间间隔时需要做些什么。

如何告知呢? java的类库采用的是面向对象方法,可以将某个类的对象传递给定时器,然后定时器调用这个对象的方法。由于对象可以携带一些附加信息,所以传递一个对象比传递一个函数要灵活的多。

不过定时器需要知道调用哪个方法,那么我们可以使用接口给它提供一个规范。timer类会固定调用ActionListener接口的actionPerformed()方法。 那么我们确定功能类的时候,可以让它实现ActionListener接口。

需要注意的是actionPerformed()方法的ActionEvent参数,这个参数提供了事件的相关信息。

画面刷新

先上代码,看不懂不要紧

@Override
    protected void paintComponent(Graphics g) {//重写这个方法
        System.out.println("22222");
        super.paintComponent(g);//清屏的作用,Graphics g 画笔
        this.setBackground(Color.lightGray);//设置画板背景颜色
        g.fillRect(0, 0, 884, 681);//添加组件,绘制游戏区域
        if (fx.equals("R"))//判断方向是哪个,输出对应的方向图标
            date.right.paintIcon(this, g, snakex[0], snakey[0]);
        else if (fx.equals("L"))
            date.left.paintIcon(this, g, snakex[0], snakey[0]);
        else if (fx.equals("U"))
            date.up.paintIcon(this, g, snakex[0], snakey[0]);
        else if (fx.equals("D"))
            date.down.paintIcon(this, g, snakex[0], snakey[0]);
        for (int i = 2; i < lenth; i++) {//循环输出多少个身体
            date.body.paintIcon(this, g, snakex[i], snakey[i]);
        }
        date.food.paintIcon(this, g, foodx, foody);//输出食物
        g.setColor(Color.WHITE);//设置画笔的颜色
        g.setFont(new Font("宋体", Font.BOLD, 20));//设置画笔的大小,字体,
        g.drawString("当前得分:" + (lenth - 9), 750, 50);//画笔输出对应的分数

怎么实现画面的不断刷新呢?

这个问题困扰了我很久,这个方法是重写paintComponent方法,这个方法是给画面输出新的布局。但是我从来没有调用过他,但是他却可以在窗口里面不断地刷新画面。

首先paintComponent方法,并不仅是JPanel的方法,而是继承自JComponent的方法。

这个方法是被swing调用来画组件的,应用不应该直接调用paint,而应该调用repaint。paint这个方法实际上代表了三个protected的方法。 *paintComponent,paintBorder,paintChildren. *里面一次调用者三种方法来确保…(略)

重要的是子类通常要重写这个方法,来定制special特殊的图形组件

其次,那么为什么不应该直接调用paintComponent而应该调用repaint呢?查了很多资料,有一个说的很简单,直接。

repaint()是重要概念,它是在图形线程后追加一段重绘操作,是安全的!是系统真正调用的重绘!所以如果你需要某个部件刷新一下界面,记得调用repaint(),千万不要直接调用paint()!

Graphics是一个抽象类,其实现大都是平台相关的,所以不容易自己创建一个graphics实例。一般graphics的实例会由依照你所在的桌面环境给出。

简答的说, repaint方法先清空了画面的内容,然后间接调用了paint方法,paint方法调用了paintComponent方法,完成了画面的刷新。

所以只要在定时器里面调用了这个方法,就会实时输出当前的画面。

repaint();
        /*
        那么为什么不应该直接调用paint而应该调用repaint呢?查了很多资料,有一个说的很简单,直接。
repaint()是重要概念,它是在图形线程后追加一段重绘操作,是安全的!是系统真正调用的重绘!所以如果你需要某个部件刷新一下界面,记得调用repaint(),
千万不要直接调用paint()!
Graphics是一个抽象类,其实现大都是平台相关的,所以不容易自己创建一个graphics实例。一般graphics的实例会由依照你所在的桌面环境给出。
最后, repaint里面间接调用了paint方法,然后paint方法再调用paintComponent方法,就实现了画面的不断刷新
         */

此外,我们需要对小蛇的位置进行控制,这个比较好解决,设置对键盘的监听,如果键盘的输入时“↑”,与之作出蛇头向上的操作就可以了。

对键盘监听要实现KeyListener接口,重写三个方法,分别是:

   @Override
    public void keyPressed(KeyEvent e) {//键盘按下,未释放操作}
    }
   @Override
    public void keyReleased(KeyEvent e) {//释放某个键操作
    }
    @Override
    public void keyTyped(KeyEvent e) {//键盘按下弹起操作
    }

在相应的方法内写自己要执行的操作就可以了。

本项目用的是keyPressed设置的是按空格键和方向键。

public void keyPressed(KeyEvent e) {//键盘按下,未释放操作
        int keycode = e.getKeyCode();//定义一个变量用来接收键盘的数据
        if (keycode == 32)  //如果按下空格键(数字32),用来启动或者暂停
      
            iskilled=false;
            start = !start;
            repaint();//刷新界面
        }
        /*
        为了防止向左运动的时候,方向键给予一个向右的命令造成死亡,所以设置了在向一个方向运动的时候,
        相反方向的命令不能够被运行。
        方法是当键盘接收到一个方向指令以后,判断是不是当前方向与获取的指令是否相反,相反则不允许更改
        当前方向
         */
        else if (keycode == 38 && fx != "D") {
            fx = "U";
        } else if (keycode == 40 && fx != "U") {
            fx = "D";
        } else if (keycode == 37 && fx != "R") {
            fx = "L";
        } else if (keycode == 39 && fx != "L") {
            fx = "R";
        }
    }

小蛇吃食物设置与吃食物的判断怎么处理?

首先食物的这是,可以利用随机数创建类,给食物的x,y坐标赋予随机数。

那么吃到食物怎么判断呢?

上面的代码写到小蛇是20个坐标进行行走,那么我们认为小蛇的x坐标和y的坐标分别都要食物的坐标相差19个绝对值,我们就可以认为他是最接近小蛇的位置,我们认定他被吃掉。

我也给出了其他的解法,请仔细阅读最后完整代码注释内容。

至于游戏的结束,小蛇到达边界值的判定,分数显示的设置等小功能的编写,这些内容比较简单,如果前面代码有所理解,这些都可以很好的解决。所以这里不单独给出实现代码,只说思路。

游戏的结束:当蛇头的坐标,与其中的一个蛇身的x,y坐标完全相等,我们认为他撞到了自己,游戏结束。

小蛇到达边界值:我们只需要判断蛇头的坐标到达了我们规定的坐标以外,我们给蛇头赋值窗口最开始的坐标。

分数的设置:画笔设置好参数,选择好位置,直接写就行。

避免蛇身向相反运动造成的死亡:小蛇正在向右运动,如果给予一个向左的指令,那么蛇头与蛇身坐标必然相等,则判定游戏结束。所以避免这样的操作,我们在给方向赋值的时候先判断他是不是我们不想要的操作,如果是这次给予指令不成立,等待下一次给予指令。

写在最后

这个程序还有一个bug没有解决,就是每次成功吃掉一个食物,左上角都会显示一个身体的小方块,并且会很快消失,当它消失以后,新的食物才会出现。所以向各位大佬请教!

GamePanel完整代码)

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;

public class GamePanel extends JPanel implements KeyListener, ActionListener {//集成画板类,实现键盘监听方法

    public GamePanel() {
        init();
        this.setFocusable(true);//让这个组件获取到焦点
        this.addKeyListener(this);
        /*
       用于接收键盘事件(击键)的侦听器接口。
                使用组件的 addKeyListener 方法将从该类所创建的侦听器对象向该组件注册。按下、释放或键入键时生成键盘事件。
            然后调用侦听器对象中的相关方法并将该 KeyEvent 传递给它。
         */
        timer.start();//计时器打开
    }

    Timer timer = new Timer(160, this);//每160毫秒刷新一次,保证动画动起来
    int lenth;
    int foodx = 0;//设置食物的横坐标
    int foody = 0;//设置食物的纵坐标
    boolean isfood = false;//设置当前是否有食物变量,默认为假
    boolean start = false;//设置当前游戏是否开始变量,默认为假
    boolean iskilled=false;
    String fx = "R";//R 向右 L向左 U向上 D向下
    int[] snakex = new int[999];
    int[] snakey = new int[999];
    Random Ran = new Random();//创建随机数对象,给食物的坐标生成随机数并且赋值

    public void init() {//初始化方法,当本类被构造的时候被执行一次
        fx = "R";
        lenth = 9;//设置蛇的输出长度
        snakex[0] = 200;
        snakey[0] = 100;//头部坐标
        snakex[1] = 200;
        snakey[1] = 100;
        snakex[2] = 179;
        snakey[2] = 100;//第一节坐标
        for (int i = 2; i < lenth; i++) {
            /*
            其他节坐标以此赋值,因为第一节身体坐标与头部一样,
            所以从第二节开始输出
             */
            snakex[i + 1] = snakex[i] - 19;
            snakey[i + 1] = snakey[i];
        }
    }

    @Override
    protected void paintComponent(Graphics g) {//重写这个方法
        super.paintComponent(g);//清屏的作用,Graphics g 画笔
        this.setBackground(Color.lightGray);//设置画板背景颜色
        g.fillRect(0, 0, 884, 681);//添加组件,绘制游戏区域
        if (fx.equals("R"))//判断方向是哪个,输出对应的方向图标
            date.right.paintIcon(this, g, snakex[0], snakey[0]);
        else if (fx.equals("L"))
            date.left.paintIcon(this, g, snakex[0], snakey[0]);
        else if (fx.equals("U"))
            date.up.paintIcon(this, g, snakex[0], snakey[0]);
        else if (fx.equals("D"))
            date.down.paintIcon(this, g, snakex[0], snakey[0]);
        for (int i = 2; i < lenth; i++) {//循环输出多少个身体
            date.body.paintIcon(this, g, snakex[i], snakey[i]);
        }
        date.food.paintIcon(this, g, foodx, foody);//输出食物
        g.setColor(Color.WHITE);//设置画笔的颜色
        g.setFont(new Font("宋体", Font.BOLD, 20));//设置画笔的大小,字体,
        g.drawString("当前得分:" + (lenth - 9), 750, 50);//画笔输出对应的分数
        if (start == false) {//游戏是否开始,如果没有开始利用画笔在画板内写一句话
            g.setFont(new Font("宋体", Font.BOLD, 40));
            g.drawString("按下空格键进行游戏!", 250, 300);
        }
        /*
        用循环不断检查蛇头坐标与身体坐标是否相等,如果相等,定时器终止,则提示游戏结束
         */
        for (int i = 3; i < lenth; i++) {
            if (snakey[0] == snakey[i] && snakex[0] == snakex[i]) {
                System.out.println("游戏结束!");
                timer.stop();//游戏画面暂停
                iskilled=true;//死亡 状态变为真
                g.setFont(new Font("宋体", Font.BOLD, 40));
                g.drawString("游戏结束!按空格键重新开始游戏", 120, 300);
            }
        }

    }

    @Override
    public void keyPressed(KeyEvent e) {//键盘按下,未释放操作
        int keycode = e.getKeyCode();//定义一个变量用来接收键盘的数据
        if (keycode == 32)  //如果按下空格键(数字32),用来启动或者暂停
        {
          if (iskilled==true){//如果死亡状态为真,初始化全部参数,进行下一次游戏
                init();
            timer.start();
         }
            iskilled=false;
            start = !start;
            repaint();//刷新界面
        }
        /*
        为了防止向左运动的时候,方向键给予一个向右的命令造成死亡,所以设置了在向一个方向运动的时候,
        相反方向的命令不能够被运行。
        方法是当键盘接收到一个方向指令以后,判断是不是当前方向与获取的指令是否相反,相反则不允许更改
        当前方向
         */
        else if (keycode == 38 && fx != "D") {
            fx = "U";
        } else if (keycode == 40 && fx != "U") {
            fx = "D";
        } else if (keycode == 37 && fx != "R") {
            fx = "L";
        } else if (keycode == 39 && fx != "L") {
            fx = "R";
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {//执行定时操作
        if (start == true) {//游戏开始以后,让小蛇动起来
            if (fx.equals("R")) {
                if (snakex[0] > 884) {//超出边界就把坐标变成初始值
                    snakex[0] = 19;
                }
                snakex[0] = snakex[0] + 20;
                for (int i = lenth - 1; i > 0; i--) {//输出右移时候的身子
                    snakex[i] = snakex[i - 1];
                    snakey[i] = snakey[i - 1];
                }
            } else if (fx.equals("L")) {
                if (snakex[0] < 0) {
                    snakex[0] = 885;
                }
                snakex[0] = snakex[0] - 20;

                for (int i = lenth - 1; i > 0; i--) {//输出左移时候的身子
                    snakex[i] = snakex[i - 1];
                    snakey[i] = snakey[i - 1];
                }
            } else if (fx.equals("U")) {
                snakey[0] = snakey[0] - 20;

                if (snakey[0] <10) {
                    snakey[0] = 680;
                }
                for (int i = lenth - 1; i > 0; i--) {//输出上移时候的身子
                    snakex[i] = snakex[i - 1];
                    snakey[i] = snakey[i - 1];
                }
            } else if (fx.equals("D")) {
                snakey[0] = snakey[0] + 20;//每次行走的单位
                if (snakey[0] > 685) {//判断边界值
                    snakey[0] = 1;
                }
                for (int i = lenth - 1; i > 0; i--) {//输出下移时候的身子
                    snakex[i] = snakex[i - 1];
                    snakey[i] = snakey[i - 1];
                }
            }

        }
        if (!isfood) {//判断地图是否存在食物,如果没有,就会进行一次绘图,把食物绘图在地图上
//                      食物的坐标通过Random置随机种子完成

            foodx = Ran.nextInt(600);
            foody = Ran.nextInt(600);
            System.out.println(foodx);//输出食物的横坐标
            System.out.println(foody);//输出食物的纵坐标
            isfood = !isfood;
        }
           /*
        判断食物是否被吃,因为蛇的单位是每20个单位进行行走,如果判断头的坐标和食物坐标完全相等时不现实的,所以就利用蛇与食物的
        绝对值相差19(蛇每走一步是20单位)以下认为吃到了食物!
        也可以在1-20置随机数,得到的随机数乘以20,保证是20的倍数,这样就存在与小蛇坐标相等的情况
         */
        if (Math.abs(snakex[0] - foodx) < 19 && Math.abs(snakey[0] - foody) < 19) {
            isfood = !isfood;//把状态取反
            lenth++;//蛇的长度自加
        }
        repaint();//清屏
        timer.start();//定时器开始

    }
    /*
    一下两个重写方法对于本项目没有太的用处,所以不再赘述
     */

    @Override
    public void keyReleased(KeyEvent e) {//释放某个键操作

    }

    @Override
    public void keyTyped(KeyEvent e) {//键盘按下弹起操作
    }
}

完整素材及全部代码

链接:https://pan.baidu.com/s/1P5ojXqn4GYVPvAdY4AUmZA
提取码:j6us

发布了2 篇原创文章 · 获赞 3 · 访问量 251

猜你喜欢

转载自blog.csdn.net/qicaixiao/article/details/105317569
今日推荐