马士兵说:手把手带你写一个贪吃蛇

今天写了一个贪吃蛇的小游戏,我给他取名叫贪吃蛇无限版。这里开始讲解这个小游戏的编程思路以及代码实现。

游戏分析

我们应该要知道做的这个贪吃蛇的小游戏应该展示的状况:
使用上下左右键来控制蛇的移动,蛇在一个棋盘状的正方形中移动,棋盘中随机出现食物,蛇吃掉一个食物,身体变长一节,当蛇吃到自己的身体或者碰到墙壁,蛇死亡,游戏结束。

蛇的活动空间的设计(Yard)

  • 因为需要显示出来,所以Yard类继承Frame类
    • 可以把游戏的窗口大小看成一个二维坐标,蛇活动的区域就是二维坐标轴里的一个平面图形(默认为正方形)。
    • 在程序主方法执行之前,需要确定离原点最近的图形的点的坐标(这里我们假设图形在坐标轴的第四象限)。我们可以理解为需要确定的点是正方形在坐标轴的左上角的点。
      • 本次代码中我们将蛇活动的区域设置的是游戏窗口的一半大小,自己做的时候可以直接将蛇活动的区域设置为窗口大小
    • 编写Yard的构造方法,在方法中设置Yard的大小(Yard的大小应该根据蛇的每一节的大小来确定)。
      设置游戏窗口的宽度和高度 。
      初始化yard对象并进行显示。
    • 因为游戏是一个可以随时关闭的窗口,所以编写关闭窗口事件的代码,在窗口添加一个Windows事件消息,目的是我们关闭窗口的时候可以正常的退出。
  • 因为游戏中蛇活动在网格中,现在我们来整理编写显示网格的代码的思路。
    • 重写paint方法,并在pain方法中调用drawLine方法,来画蛇活动的网格。网格并不是一条线可以完成的,所以在这里使用for循环来完成画线的循环动作。
    • 循环中,将i设置为i<=NodeCount,这样,就可以画出所需要的网格线,并且不会出现网格有一面没有框住的情况。
    • 在drawLine()方法中 x1,y1; x2,y2分别代表的是横线和竖线的起始点和终点。
      这样,蛇活动的网格就画好了,运行程序的效果就是一个窗口中有一个30*30的网格,并且窗口可以关闭。
public class Yard extends Frame {               //因为蛇活动的空间需要显示出来
    //蛇活动的空间的大小必须由蛇的每一节决定
    public static final int NodeSize = 15;
    public static final int NodeCount = 30;         //count表示的是蛇的活动空间一共有多少行或者多少列

    public static final int AreaSize = NodeSize * NodeCount;//将区域的大小设置成一个常量,以后访问会比较方便

    //这是蛇活动的正方形所在游戏窗口的左上角点的位置
    static int x = AreaSize / 2;
    static int y = AreaSize / 2;

    public static void main(String[] args) {
        new Yard();
    }
    //书写Yard的构造方法
    Yard(){
        //设置蛇活动的正方形的宽度和高度
        this.setSize(2*AreaSize,2*AreaSize);
        this.setVisible(true);                          //初始化yard对象并进行显示
        //在窗口添加一个Windows事件消息,目的是我们关闭窗口的时候可以正常的退出
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });

    //这个模块处理的是蛇活动的正方形的横线竖线问题
    @Override
    public void paint(Graphics g) {
        //每次重画时,要使用白色将整个窗口重画一遍
        //保存现场
        Color c = g.getColor();
        g.setColor(Color.WHITE);
        g.fillRect(0,0,this.getWidth(),this.getHeight());

        //画线
        g.setColor(Color.BLACK);

        //利用for循环来完成循环画线的动作并显示出网格
        for (int i = 0; i <= NodeCount; i++){
            //x1,y1; x2,y2分别代表的是横线和竖线的起始点和终点
            g.drawLine(x,y+NodeSize*i, x+AreaSize, y+NodeSize*i);
            g.drawLine(x+NodeSize*i, y, x+NodeSize*i, y+AreaSize);
        }

    }
}

蛇的节点(Node)

在上面我们已经整理好关于蛇的活动区域的整体思路以及代码展示,这时,开始整理画蛇的思路:
在贪吃蛇这个游戏中,蛇的身体是由一节一节组成的,而且蛇的移动也是按照一节一节的进行移动的,所以,我们先来画蛇的身体的节点。

  • 因为蛇的活动区域是一个网格,所以,他一定要有两个属性,一个是他所在的行(row),另一个是他所在的列(col),而且还会有对他的两个属性写一个构造方法Node()。
  • 因为Node是一个双向链表,所以他需要有前面节点和后面节点。
  • 因为蛇是需要画(展现)出来,所以在这里构造一个paint方法。
public class Node {
    //对于蛇的节点,要知道他是位于哪一行那一列
    //row行   col列
    int row,col;

    //计算小方格所在的位置
    //由于Node是一个双向链表,所以他一定会有两个节点
    // prev前面的节点   next后面的节点
    //这两个节点默认都为空
    Node prev, next;
    public Node(int row, int col) {
        this.row = row;
        this.col = col;
    }

    public void paint(Graphics g) {
        //计算小方格所在的位置
        int x = Yard.x + col * Yard.NodeSize;
        int y = Yard.y + row * Yard.NodeSize;
        Color c = g.getColor();

        //设置小方格的颜色
        g.setColor(Color.black);

        //要上色的小方块所在的位置
        g.fillRect(x, y, Yard.NodeSize, Yard.NodeSize);
        //返回之前的颜色
        g.setColor(c);
    }
}

蛇本体(Snake)

怎么表示一条蛇?

贪吃蛇的身体可以使用一段可增长的数据进行表示,这里我们就想到了数组与链表这两种可以表示一段数据的类。

  • 如果使用数组来表示蛇的移动的话
    蛇移动时,蛇的身体移动的每一节都需要数组的所有元素的位置进行移动。
  • 如果使用链表来表示蛇的移动的话
    将链表的尾部删掉,在链表的头部new出来。(将链表的尾部的一部分删掉,然后在链表的头部添加与删除掉的部分等长的元素)
    经过上述分析,我们可以知道数组和链表都可以表示这样的一条蛇,但是相比较而言,链表使用起来更方便,所以选择链表来表示这条蛇。
    作为一条蛇,他一定要有一个脑袋和一个尾巴。
    根据前面的分析,如果没有尾巴,那么蛇进行移动的时候每移动一格就要遍历一次,十分浪费时间, 所以添加尾巴的作用,就是使用空间换取时间。
    设置头和尾 Node head,tail;
    编写snake的构造方法,在方法中,新建一个head对象,将head的初始值设置出来。
  • 已经知道了这条蛇的具体的属性,怎么来画这条蛇?
    因为我们已经在蛇的节点中已经建立了画的paint方法,所以我们这里,假设蛇的长度是3个节点,首先确定蛇的头部,然后利用while循环链表从头到尾,挨个将蛇的节点画出来。
public class Snake {

    //作为一条蛇,他一定要有一个脑袋和一个尾巴
    Node head, tail;
    
    Snake() {
        head = new Node(20,20);             //设置蛇的初始位置
        tail = head;                                    //此时,我们将蛇的长度设置为一节
    }
    
    public void paint(Graphics g) {
        Node n = head;
        while (n != null){
            //根据面向对象编程的思想,只有Node自己知道怎么填充自己
            n.paint(g);
            n = n.next;
        }
    }
}

怎么使用链表来实现蛇的移动?
  • 首先我们考虑 链表从头开始一个个向后推,移动到要删除的元素(链表尾)前,删除链表尾,链表尾前面的元素成为新的链表尾。
    但是这个方法的缺点很明显:蛇的每次移动,链表都需要进行一次遍历,十分麻烦,而且效率不高。
    所以我们对这个方法进行改进。
  • 将链表中的所有元素都设置为互相指向(前后元素都可以互相指向,A->B的同时B->A)。将链表尾指向的上一个元素找出,然后删除链表尾,原链表尾指向的元素就是新的链表尾。
    这样,蛇移动时就不需要每移动一格就遍历一次链表了。
在代码中考虑如何书写蛇的移动

可以考虑一下电影,电影的播放是一帧一帧完成的,从而完成人物的动作等等,让蛇动起来的原理和放电影一样,只需要 让窗口进行不断的重画,就可以让蛇动起来。

  • 首先重画这个操作需要在Yard类中完成。
    调用repaint方法,就能进行重画,但是一次重画就能让蛇动起来是不现实的,所以,使用 while循环让repaint方法不断的进行重复。但是重画的频率太快了也不行,所以使用Thread.sleep(),来进行间隔。
    ===>>这里有个小扩展,我们这样编写好程序后会出现窗口闪烁的问题,而且频率很快,这里我们使用一段代码来解决。
    Image offScreenImage = null;
	public void update(Graphics g) {
		if (offScreenImage == null){
		offScreenImage = this.createImage(this.getWidth(),this.getHeight());
		}
		Graphics gOff = offScreenImage.getGraphics();
		paint(gOff);
		g.drawImage(offScreenImage,0,0,null);
	}

经过前面的操作,已经可以把蛇画出来了,现在我们编写让蛇动起来的代码。

  • 首先,写一个添加头部的方法addToHead
    • 这时,有一个问题,我们不知道这个新添加的头部应该放到哪个位置,上下左右?所以设计一个枚举对象来存储蛇的动作。
  • 新建一个Node对象,代表添加到头部的Node,利用switch选择判断一下当前的蛇的动作朝向,并且计算出新添加的Node将要添加到哪里。新添加的元素作为链表的头部进行展示。
    • 将新元素添加到链表头部,首先,将新建元素n作为新的head的值。将原来的链表头作为链表的下一个,指向新的元素。
  • 因为蛇在移动过程中他的长度应该保持一定,在分析过程中我们已经知道蛇的移动大体上概括为"删头去尾"。所以,在这里我们新建一个deleteTail()方法来实现删除尾部的操作。
    • 先进行一个判断,如果蛇的长度是一个空值,删除的操作毫无意义,所以进行一个判断
    • 最后感谢大家的关注 欢迎添加qq 1723082823 进入我们的粉丝群 获取更多更新视频资料!。
    • 将tail指向的上一节指向的下一节的元素的值设置为空。(也就是将原来的tail删除)在这个过程中需要将新tail与老tail的连接完全打断。如果不完全打断的话会出现内存泄漏的问题。
    //添加头部的动作
    public void addToHead(){
        //新建一个对象,代表将要添加到头部的Node
        Node n = null;
        //利用switch判断蛇的动作,并且计算出新添加节点的位置
        switch (dir){
            case D:
                n = new Node(head.row+1, head.col);
                break;
            case L:
                n = new Node(head.row, head.col-1);
                break;
            case R:
                n = new Node(head.row, head.col+1);
                break;
            case U:
                n = new Node(head.row-1, head.col);
                break;
        }
             /* 将原来的链表头作为链表的 下一个,指向新的元素
                将新建元素n作为新的head的值*/
        n.next = head;
        head.prev = n;
        head = n;
    }
    public void paint(Graphics g) {
        Node n = head;
        while (n != null){
            //根据面向对象编程的思想,只有Node自己知道怎么填充自己
            n.paint(g);
            n = n.next;
        }
        //每重画一次,都需要让蛇移动一次
        move();
    }
    private void move() {
        addToHead();
        deleteTail();
        boundaryCheck();
    }
  • 两个方法都写完了,理论上蛇的移动就可以完成了,但是在我们的测试过程中发现蛇是无限延长而不是移动。我们可以使用背景重画来解决这个问题。将背景重画写到Yard类的paint方法中。
//让蛇动起来的原理就是不断地进行重画
        while (true){
            try {//重画的频率太高,所以加上限制
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.repaint();
        }
蛇的控制

到了这里,我们的代码可以实现蛇的移动,蛇活动区域的显示。但是,我们发现,蛇的移动不受我们控制,所以我们现在开始编写控制蛇移动的代码。
在Yard类中添加键盘监听事件,添加键盘事件来控制蛇的移动。

this.addKeyListener(new KeyAdapter() {//KeyListener键盘监听器
	public void keyPressed(KeyEvent e) {//keyPressed事件
	s.keyPressed(e);      //这里让snake自己处理键盘事件-->在Snake中重keyPressed
	}								
});

最后感谢大家的关注 欢迎添加qq 1723082823 进入我们的粉丝群 获取更多更新视频资料!
然后在Snake中重写keyPressed方法,使用switch判断按下的方向键,并且在方向键中添加蛇的动作。

    public void keyPressed(KeyEvent e) {
        int key = e.getKeyCode();           //判断按下的键
        switch (key){
            case KeyEvent.VK_LEFT:          //按下的方向键
                dir = Dir.L;                //蛇行动的方向
                break;
            case KeyEvent.VK_UP:
                dir = Dir.U;
                break;
            case KeyEvent.VK_RIGHT:
                dir = Dir.R;
                break;
            case KeyEvent.VK_DOWN:
                dir = Dir.D;
                break;
        }
    }
蛇吃食物的动作

首先判断蛇是否吃到了食物,蛇吃到食物的结果我们可以理解为蛇的头部与食物重合,当蛇的头部与食物重合后,蛇的身体变长一节,同时原食物消失,在一个新的随机的位置出现一个新的食物,这个新食物出现的方法在egg的reAppear中实现。

    //蛇吃掉食物的动作
    public void eat(Egg egg) {
        //判断蛇是否吃掉了食物
        if (head.row == egg.row && head.col == egg.col){
            //蛇的身体增加一节
            addToHead();
            //食物被吃掉,让食物在其他的地方再次出现
            egg.reAppear();
        }
    }

蛇的食物(egg)

这个时候,我们的代码已经完成了蛇活动区域的展现,使用键盘对蛇移动的进行控制,这时,整个游戏就差蛇的食物没有完成了。
创建食物的属性,用来确定食物出现的位置。因为食物要出现在网格中,所以重写paint方法,让他把自己画出来。
这里,我们回到Yard类,设置食物的初始位置,在蛇的paint方法之前,调用食物的paint方法把食物画出来。
因为食物在被蛇吃掉后会消失,而且游戏还需要进行下去,所以我们写一个在原食物消失后,新食物随机出现的方法reAppear。

public class Egg {
    //蛇的食物必须的属性
    int row, col;
    Random r = new Random();

    //需要把这个食物画出来,新建一个构造方法构造Egg
    public Egg(int row, int col) {
        this.row = row;
        this.col = col;
    }

    //画食物的方法
    public void paint(Graphics g) {
        //计算小方格所在的位置
        int x = Yard.x + col * Yard.NodeSize;
        int y = Yard.y + row * Yard.NodeSize;
        Color c = g.getColor();

        //设置小方格的颜色
        g.setColor(Color.RED);

        //要上色的小方块所在的位置
        g.fillOval(x, y, Yard.NodeSize, Yard.NodeSize);
        //返回之前的颜色
        g.setColor(c);
    }
    //这时一个食物被吃掉后随即显示在其他位置的方法
    public void reAppear() {
        this.row = r.nextInt(Yard.NodeCount);
        this.col = r.nextInt(Yard.NodeCount);
    }
}

最后感谢大家的关注
欢迎添加qq 1723082823 进入我们的粉丝群 获取更多更新视频资料!

猜你喜欢

转载自blog.csdn.net/yxxylucy/article/details/95081073