贪吃蛇

介绍

相信大家小时候都玩过贪吃蛇,对这很熟悉,所以不多说废话,直接来分析思路。
游戏中主要有这几个类,院子(Yard)、蛇(Snake)、给蛇吃的蛋(Egg)、构成蛇的结点类(Node)。当然,构成蛇结点的类最好作为蛇的内部类,但为了好具体操作,我将构成蛇的结点类没有设置成蛇的内部类。
接下来便是对游戏的功能进行分析。首先,我们得有一个院子,这是最基础的部分,接着得画出一条蛇,让它能够在键盘控制下移动。最后,蛇挂掉之后我们得让游戏画面停止刷新,并提示游戏结束,按相关键后又可以重新游戏。


核心

设计这个游戏的难点就是实现蛇的移动。
这是贪吃蛇向***右***移动,我们可以发现,蛇变动的只有头结点和尾结点,进一步看,蛇的移动相当于将尾结点直接放到了头结点
这是贪吃蛇向***上***移动,我们可以发现,蛇变动的只有头结点和尾结点,进一步看,蛇的移动相当于将尾结点直接放到了头结点
其余情况不一一列举了,综上我们可以发现,蛇的移动相当于将尾结点直接放到了头结点,所以我们可以用一个双向链表来存储蛇的结点,每当蛇移动一格时,我们就先将双向链表的尾结点加到头结点前,成为新的头结点,再删除尾结点,此时的尾结点相当于原来尾结点的前一个结点。又由于蛇吃蛋时需要插入结点到头部或者尾部,移动时必须头插法。所以,我最终选择头插法,当然也可以加上尾插法。


具体过程

  • 第一步:绘制出院子(Yard),主要绘制出有多少行多少列格子,并定义枚举类型变量Direction,分别用来指示四个方向U, D, L, R(上下左右)

  • 第二步:设计结点Node类,写出move方法, draw方法,绘制一个Node结点到屏幕上,并能通过键盘控制一个Node结点在屏幕上移动,其中Node的move方法仅做测试用,第二部完成即可删除。

  • 第三步:设计蛇(Snake)类,最重要的便是头插法建立一个带头结点和带尾结点的双向链表,蛇的移动概括为先将尾结点加到头结点(此时还没删除尾结点),再来删除尾结点,新生成的双向链表的尾结点为原来尾部结点的前一个结点。最后,让蛇在键盘控制下移动。

  • 第四步:设计蛋(Egg)类,让Egg被Snake吃掉后能够随机出现在屏幕上另外一个地方。当然,蛇吃掉蛋后通过头插法增加自己的长度

  • 第五步:设计检测蛇是否死了的方法,一是越界,二是咬到自己。当蛇死了的时候,我们就将线程中repaint的方法挂起,当按F2时,又启用repaint方法,使得游戏重新开始。

  • 第六步:当然是愉快的玩耍,测试bug罗。

是否觉得废话太多,那就直接看代码吧


/**
 * 用来指示方向的枚举变量,分别为上、下、左、右
 * @author lu
 *
 */
public enum Direction {

    U, D, L, R
}


import java.awt.Color;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class Yard extends Frame {

    /**
     * 屏幕行数
     */
    public static final int ROWS = 40;

    /**
     * 屏幕列数
     */
    public static final int COLS = 40;

    /**
     * 屏幕方块宽度
     */
    public static final int BLOCK_SIZE = 15;


    //flag,start用来控制线程中rePaint方法执行
    private boolean flag = true;
    private boolean start = true;


    //记录得分
    private int score = 0;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }


    //用4个结点初始化蛇
    Node node1 = new Node(25, 25, Direction.R);
    Node node2 = new Node(30, 30, Direction.U);
    Node node3 = new Node(40, 40, Direction.R);
    Node node4 = new Node(50, 50, Direction.L);

    Snake snake = new Snake(this);
    Egg egg = new Egg();

    //用于双缓冲,可以跳过不看
    Image offScreenImage = null;


    /**
     * 双缓冲,可以不用看,对最终结果影响不大
     */
    @Override
    public void update(Graphics g) {

        if (offScreenImage == null)
            offScreenImage = this.createImage(Yard.COLS * Yard.BLOCK_SIZE,
                    Yard.ROWS * Yard.BLOCK_SIZE);
        Graphics gOffScreen = offScreenImage.getGraphics();
        gOffScreen.setColor(Color.DARK_GRAY);
        gOffScreen.fillRect(0, 0, Yard.COLS * Yard.BLOCK_SIZE, Yard.ROWS
                * Yard.BLOCK_SIZE);
        paint(gOffScreen);
        g.drawImage(offScreenImage, 0, 0, null);

    }

    public static void main(String[] args) {
        new Yard().launchFrame();
    }

    public void launchFrame() {

        this.setBackground(Color.DARK_GRAY);
        this.setBounds(200, 50, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE);
        this.setResizable(false);

        this.addKeyListener(new KeyMonitor());
        this.addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent arg0) {
                System.exit(0);
            }

        });

        this.setVisible(true);


        //构建4个结点的蛇
        snake.addToHead(node1);
        snake.addToHead(node2);
        snake.addToHead(node3);
        snake.addToHead(node4);

        //启动重画的线程
        new Thread(new myPaint()).start();
    }



    /**
     * 将所有对象画在院子里面画出来
     */
    @Override
    public void paint(Graphics g) {
        Color c = g.getColor();

        //画线画笔颜色为黑色
        g.setColor(Color.black);

        // 画横线
        for (int i = 1; i < ROWS; i++) {
            g.drawLine(0, i * BLOCK_SIZE, COLS * BLOCK_SIZE, i * BLOCK_SIZE);
        }

        // 画竖线
        for (int i = 1; i < COLS; i++) {
            g.drawLine(i * BLOCK_SIZE, 0, i * BLOCK_SIZE, ROWS * BLOCK_SIZE);
        }

        //画得到的分数,画笔颜色设置为红色
        g.setColor(Color.red);
        g.drawString("Score:" + score, 30, 50);

        //将画笔颜色设置为默认颜色
        g.setColor(c);

        //当吃掉一个蛋的时候,分数加5
        if (snake.eat(egg))
            score += 5;

        //将蛋和蛇画到屏幕上
        egg.draw(g);
        snake.draw(g);

        //flag默认值为true,当蛇死掉时候,flag = false,此时,执行下面语句,使得结束提示在屏幕上出现
        if (!flag) {
            Font f = g.getFont();
            g.setFont(new Font("宋体", Font.BOLD, 50));
            g.setColor(Color.red);
            g.drawString("Game Over", 150, 200);
            g.setFont(new Font("宋体",Font.BOLD, 30));
            g.drawString("请按F2重新开始", 150, 250);
            start = false;
        }
        g.setColor(c);

    }

    /**
     * 当蛇死掉时候,将flag设置为false的函数
     */
    public void stop() {
        this.setFlag(false);
    }


    /**
     * 重新开始游戏的函数,当按F2时,执行这个方法,将snake指向一只新构建的蛇对象,再将该对象设置为与一开始蛇一样大小,一样的位置
     */
    public void reStart() {
        start = true;
        flag = true;
        snake = new Snake(this);
        snake.addToHead(node1);
        snake.addToHead(node2);
        snake.addToHead(node3);
        snake.addToHead(node4);
    }

    /**
     * 用来监听键盘上的按键,主要为上下左右, F2
     * @author Lu
     *
     */
    class KeyMonitor extends KeyAdapter {

        /**
         * 重写KeyPressed方法,监听蛇上下左右,重新开始F2
         */
        @Override
        public void keyPressed(KeyEvent e) {
            snake.keyPressed(e);
            int keyCode = e.getKeyCode();
            if(keyCode == KeyEvent.VK_F2) {
                reStart();
            }
        }

    }


    /**
     * 线程类,对屏幕上的图不停进行重画
     * @author lu
     *
     */

    class myPaint implements Runnable {

        public void run() {
            while (true) {
                if (start) {
                    repaint();
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
            }
        }

    }

}

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;

/**
 * 构成蛇的结点类
 * @author lu
 *
 */
public class Node {

    /**
     * 结点宽度
     */
    int w = Yard.BLOCK_SIZE;

    /**
     * 结点高度
     */
    int h = Yard.BLOCK_SIZE;

    /**
     * 结点方向
     */
    Direction dir = Direction.D;

    /**
     * 结点行数
     */
    int row;

    /**
     * 结点列数
     */
    int col;

    /**
     * 指向后一个结点的指针
     */

    Node next = null;

    /**
     * 指向前一个结点的指针
     */
    Node pre = null;

    /**
     * 因为结点长度和宽度都已经确定,故构造方法中给出结点所在行数、列数和方向即可
     * @param row 行数
     * @param col 列数
     * @param dir 方向
     */
    public Node(int row, int col, Direction dir) {
        this.row = row;
        this.col = col;
        this.dir = dir;
    }

    /**
     * 画出结点
     * @param g 画结点的画笔
     */
    public void draw(Graphics g) {
        Color c = g.getColor();
        g.setColor(Color.red);
        g.fillRect(col * Yard.BLOCK_SIZE, row * Yard.BLOCK_SIZE, w, h);
        g.setColor(c);
    }

    /**
     * 为了进行碰撞检验的函数
     * @return 返回包围结点的矩形
     */
    public Rectangle getRect() {
        return new Rectangle(this.col * Yard.BLOCK_SIZE, this.row * Yard.BLOCK_SIZE, Yard.BLOCK_SIZE, Yard.BLOCK_SIZE);
    }



}

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;


/**
 * 蛇是由一个一个结点构成,为了处理方便,结点单独作为一个类,注意蛇的头结点存放数据,并不是头结点的下一个结点存放数据
 * @author Lu
 *
 */

public class Snake {

    //头结点
    private Node head = null;
    //尾结点
    private Node tail = null;

    //得到Yard的一个引用,方便调用Yard里面的方法
    private Yard y = null;

    /**
     * 将Yard对象引用传进来
     * @param y Yard对象的引用
     */
    public Snake(Yard y) {
        this.y = y;
    }

    //记录蛇是否还活着
    private boolean live = true;



    /**
     * 头插法构建双向链表,将新生成的结点插入到蛇头
     * @param node 待插入的结点
     */
    public void addToHead(Node node) {
        //当蛇还是空的时候,插入第一个结点的处理
        if(head == null) { 
            head = tail = node;
            head.next = null;
            tail.next = null;
            head.pre = null;
            tail.pre = null;
            return;
        }


         /**
          * 下面的为处理的具体过程
          * 当头结点向下时,此时加入的结点应加在头结点的正下方
          * 当头结点向上时,此时加入的结点位置为头结点的正上方
          * 当头结点向左时,此时加入的结点位置为头结点的正左方
          * 当头结点向右时,此时加入的结点位置为头结点的正右方
          */

        Node nTemp = null;
        if(head.dir == Direction.D)
            nTemp = new Node(head.row + 1, head.col, Direction.D);
        else
            if(head.dir == Direction.U)
                nTemp = new Node(head.row - 1, head.col, Direction.U);
            else
                if(head.dir == Direction.L)
                    nTemp = new Node(head.row, head.col - 1, Direction.L);
                else
                    if(head.dir == Direction.R)
                        nTemp = new Node(head.row, head.col + 1, Direction.R);


        //插入结点,注意是双向链表
        nTemp.next = head;
        head.pre = nTemp;
        head = nTemp;

    }


    /**
     * 蛇移动时改变方向
     * 蛇移动的核心就是将蛇尾结点加入到蛇头,再将蛇头结点删除
     */
    public void changeDiretion() {
        //将蛇尾结点插入到蛇头
        this.addToHead(tail);

        //将蛇尾结点删除
        tail = tail.pre;
        tail.next = null;
    }

    /**
     * 将蛇画出来
     * @param g 画蛇的画笔
     */

    public void draw(Graphics g) {

        //如果蛇死掉了,直接返回
        if(!this.live) return;

        //当蛇没有一个结点时,直接返回
        if(head == null) return;

        //检查蛇是否越过Yard的四边,如果越过,会调用y.stop方法,将线程中rePaint方法跳过
        this.check();

        //进行移动
        this.changeDiretion();

        //依次画出蛇的每一个结点
        Node n = head;
        while(n != null) {
            n.draw(g);
            n = n.next;
        }


    }

    /**
     * 此方法用来监听键盘按键,改变蛇头方向,在Yard方法中调用
     * @param e 监听键盘的类
     */
    public void keyPressed(KeyEvent e) {

        int keyCode = e.getKeyCode();
        switch(keyCode) {
        case KeyEvent.VK_UP:
            if(head.dir != Direction.D)
                head.dir = Direction.U;
            break;
        case KeyEvent.VK_DOWN:
            if(head.dir != Direction.U)
                head.dir = Direction.D;
            break;
        case KeyEvent.VK_LEFT:
            if(head.dir != Direction.R)
                head.dir = Direction.L;
            break;
        case KeyEvent.VK_RIGHT:
            if(head.dir != Direction.L)
                head.dir = Direction.R;
            break;
        }




    }


    /**
     * 得到蛇头结点的矩形位置和大小,用于碰撞检测,
     * @return 返回头结点的矩形具体位置和大小
     */
    public Rectangle getRect() {
        return new Rectangle(head.col * Yard.BLOCK_SIZE, head.row * Yard.BLOCK_SIZE, Yard.BLOCK_SIZE, Yard.BLOCK_SIZE);
    }

    /**
     * 检测蛋是否被吃掉
     * @param egg 蛇要吃的蛋
     * @return 如果吃掉,就
     */
    public boolean eat(Egg egg) {
        //判断蛋是否被吃用碰撞检测
        if(this.live && egg.isLive() && this.getRect().intersects(egg.getRect())) {
            egg.setLive(false);
            this.addToHead(new Node(egg.row * Yard.BLOCK_SIZE, egg.col * Yard.BLOCK_SIZE, Direction.D));
            return true;
        }
        return false;
    }


    /**
     * 检测蛇头是否出界,蛇是否咬到自己
     * 
     */
    public void check() {
        //出界检测
        if(this.head.col < 1 || this.head.row < 3 || this.head.col > Yard.COLS - 2 || this.head.row > Yard.ROWS - 2) {
            y.stop();
        }


        //是否咬到自己检测
        Node temp = head.next;
        while(temp != null) {
            if(temp.col == head.col && temp.row == head.row)
                y.stop();
            temp = temp.next;
        }

    }


}

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.util.Random;


public class Egg {
    //蛋所在的行数和列数
    int row, col;

    //蛋的长和宽
    int w = Yard.BLOCK_SIZE;
    int h = Yard.BLOCK_SIZE;

    //产生随机整数的类
    private static Random r = new Random();

    private boolean live = true;


    public boolean isLive() {
        return live;
    }


    public void setLive(boolean live) {
        this.live = live;
    }

    /**
     * 蛋的构造方法
     * @param row 蛋所在的行数
     * @param col 蛋所在的列数
     */
    public Egg(int row, int col) {
        this.row = row;
        this.col = col;
    }

    /**
     * 蛋的另外一种构造方法,当蛋被吃掉时,调用此构造方法,使蛋出现在另外一个地方
     */
    public Egg() {
        this(r.nextInt(Yard.ROWS - 4) + 3, r.nextInt(Yard.COLS - 4) + 3);

    }

    /**
     * 将蛋画出来
     * @param g 画蛋的画笔
     */

    public void draw(Graphics g) {
        this.reAppear();
        Color c = g.getColor();
        g.setColor(Color.blue);
        g.fillOval(col * Yard.BLOCK_SIZE, row * Yard.BLOCK_SIZE, w, h);
        g.setColor(c);

    }


    /**
     * 当蛋被蛇吃掉时,将蛋随机画到屏幕上另外一个地方
     * @return 如果被蛇吃掉,重复出现在屏幕上另外一处的时候返回true, 否则返回false
     */
    public boolean reAppear() {

        if(!this.live ) {
            this.row = r.nextInt(Yard.ROWS - 4) + 3;
            this.col = r.nextInt(Yard.COLS - 4) + 3;
            this.live = true;
            return true;
        }
        return false;
    }


    /**
     * 得到包围蛋的矩形,用于碰撞检测
     * @return 返回包围蛋的矩形
     */
    public Rectangle getRect() {
        return new Rectangle(col * Yard.BLOCK_SIZE, row * Yard.BLOCK_SIZE, w, h);
    }



}










  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值