1. 程式人生 > 實用技巧 >《Java小遊戲實現》:貪吃蛇

《Java小遊戲實現》:貪吃蛇

今天我自己就從零開始來完成這個小遊戲,完成的方式也是一步一步的新增功能這樣的方式來實現。

第一步完成的功能:寫一個介面

大家見到的貪吃蛇小遊戲,介面肯定是少不了的。因此,第一步就是寫一個小介面。

實現程式碼如下:

public class SnakeFrame extends Frame{
    //方格的寬度和長度
    public static final int BLOCK_WIDTH = 15 ;
    public static final int BLOCK_HEIGHT = 15 ;
    //介面的方格的行數和列數
    public static final int ROW = 40;
    public static final int COL = 40;
    public static void main(String[] args) {
        new SnakeFrame().launch();
    }

    public void launch(){

        this.setTitle("Snake");
        this.setSize(ROW*BLOCK_HEIGHT, COL*BLOCK_WIDTH);
        this.setLocation(300, 400);
        this.addWindowListener(new WindowAdapter() {

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

        });
        this.setResizable(false);
        this.setVisible(true);
    }

}

第二步完成的功能:在介面上畫成一格一格的

我們見過的貪吃蛇遊戲,是有一個格子一個格子構成,然後蛇在這個裡面運動。

重寫paint方法,單元格就是橫著畫幾條線豎著畫幾條線即可。

程式碼如下:

@Override
public void paint(Graphics g) {
    Color c = g.getColor();
    g.setColor(Color.GRAY);
    /*
     * 將介面畫成由ROW*COL的方格構成,兩個for迴圈即可解決
     * */
    for(int i = 0;i<ROW;i++){
        g.drawLine(0, i*BLOCK_HEIGHT, COL*BLOCK_WIDTH,i*BLOCK_HEIGHT );
    }
    for(int i=0;i<COL;i++){
        g.drawLine(i*BLOCK_WIDTH, 0 , i*BLOCK_WIDTH ,ROW*BLOCK_HEIGHT);
    }

    g.setColor(c);
}

效果如下:

第三步完成的功能:建立另外的執行緒來控制重畫

由於,蛇的運動就是改變蛇所在的位置,然後進行重畫,就是我們所看到的運動。因此,在這裡,我們單獨用一個執行緒來控制重畫。

1、新建一個MyPaintThread類,實現了Runnable介面

    private class MyPaintThread implements Runnable{

        @Override
        public void run() {
            //每隔50ms重畫一次
            while(true){
                repaint();//會自動呼叫paint方法
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

    }

2、在SnakeFrame的launchFrame方法中新增程式碼:new Thread(new MyPaintThread()).start();即可。

完成功能:利用雙緩衝來解決閃爍的問題

    private Image offScreenImage = null;
    /*
     * 重寫update方法
     * */
    @Override
    public void update(Graphics g) {
        if(offScreenImage==null){
            offScreenImage = this.createImage(ROW*BLOCK_HEIGHT, COL*BLOCK_WIDTH);
        }
        Graphics offg = offScreenImage.getGraphics();
        //先將內容畫在虛擬畫布上
        paint(offg);
        //然後將虛擬畫布上的內容一起畫在畫布上
        g.drawImage(offScreenImage, 0, 0, null);
    }

第四步完成的功能:在介面上畫一個蛇出來

貪吃蛇遊戲中的蛇就是用一系列的點來表示,這裡我們來模擬一個連結串列。連結串列上的每個元素代表一個節點。

首先,我們先新建一個Node類來表示構成蛇的節點,用面向物件的思想,發現,這個類應該有如下的屬性和方法:

1、位置

2、大小,即長度、寬度

3、方向

4、構造方法

5、draw方法

Node類的程式碼如下:

    public class Node {

        private static final int BLOCK_WIDTH = SnakeFrame.BLOCK_WIDTH;
        private static final int BLOCK_HEIGHT = SnakeFrame.BLOCK_HEIGHT;
        /*
         * 每個節點的位置
         * */
        private int row;
        private int col;
        //方向
        private Direction dir ;

        private Node pre;
        private Node next;

        public Node(int row, int col, Direction dir) {
            this.row = row;
            this.col = col;
            this.dir = dir;
        }

        public void draw(Graphics g){
            Color c = g.getColor();
            g.setColor(Color.BLACK);
            g.fillRect(col*BLOCK_WIDTH, row*BLOCK_HEIGHT, BLOCK_WIDTH, BLOCK_HEIGHT);
            g.setColor(c);      
        }
    }

Direction是一個enum,具體如下:

    public enum Direction {
        L,U,R,D
    }

而在Snake類中,用面向物件的思維,可以發現,Snake類中應該有如下的屬性和方法

1、頭結點

2、尾結點

3、建構函式

3、draw方法

具體程式碼如下:

    public class Snake {

        private Node head = null;
        private Node tail = null;   

        private SnakeFrame sf;
        //初始化是蛇的位置
        private Node node = new Node(3,4,Direction.D);

        private int size = 0;
        public Snake(SnakeFrame sf) {
            head = node;
            tail = node;
            size ++;
            this.sf = sf ;      
        }

        public void draw(Graphics g){
            if(head==null){
                return ;
            }
            for(Node node = head;node!=null;node = node.next){
                node.draw(g);
            }   
        }


    }

在SnakeFrame類中new一個Snake物件,然後呼叫Snake物件的draw方法即可。

效果如下:

第五步完成的功能:通過鍵盤控制蛇的上下左右移動

首先想到的是這樣:在Snake類中新增一個keyPressed方法,然後在SnakeFrame的鍵盤事件中呼叫Snake物件的keyPressed方法。

注意:蛇的移動是通過在頭部新增一個單元格,在尾部刪除一個單元格這樣的思想來實現。

具體如下:

Snake類中新增一個keyPressed方法,主要是根據鍵盤的上下左右鍵來確定蛇的頭結點的方向,然後move方法再根據頭結點的方向來在頭部新增一個單元格。

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

    public void move() {
        addNodeInHead();
        deleteNodeInTail();
    }

    private void deleteNodeInTail() {
        Node node = tail.pre;
        tail = null;
        node.next = null;
        tail = node;
    }

    private void addNodeInHead() {
        Node node = null;
        switch(head.dir){
        case L:
            node = new Node(head.row,head.col-1,head.dir);
            break;
        case U:
            node = new Node(head.row-1,head.col,head.dir);
            break;
        case R:
            node = new Node(head.row,head.col+1,head.dir);
            break;
        case D:
            node = new Node(head.row+1,head.col,head.dir);
            break;
        }

        node.next = head;
        head.pre = node;
        head = node;

    }
    //最後,在draw中呼叫move方法即可
    public void draw(Graphics g){
        if(head==null){
            return ;
        }
        move();
        for(Node node = head;node!=null;node = node.next){
            node.draw(g);
        }   
    }

這樣就實現了通過鍵盤來實現蛇的移動。

完成的功能:蛇吃蛋

首先我們新建一個蛋Egg的類。

類的屬性和方法有:

1、位置、大小

2、構造方法

3、draw方法

4、getRect方法:用於碰撞檢測

5、reAppear方法:用於重新產生蛋的方法

程式碼如下:

    public class Egg {
        //所在的位置
        private int row;
        private int col;
        //大小
        private static final int BLOCK_WIDTH = SnakeFrame.BLOCK_WIDTH;
        private static final int BLOCK_HEIGHT = SnakeFrame.BLOCK_HEIGHT;

        private static final Random r = new Random();

        private Color color = Color.RED;

        public Egg(int row, int col) {
            this.row = row;
            this.col = col;
        }

        public Egg() {
            this((r.nextInt(SnakeFrame.ROW-2))+2,(r.nextInt(SnakeFrame.COL-2))+2);
        }
        /*
         * 改變當前物件的位置,即完成蛋的重現
         * */
        public void reAppear(){
            this.row = (r.nextInt(SnakeFrame.ROW-2))+2;
            this.col = (r.nextInt(SnakeFrame.COL-2))+2;
        } 

        public void draw(Graphics g){
            Color c= g.getColor();
            g.setColor(color);
            g.fillOval(col*BLOCK_WIDTH, row*BLOCK_HEIGHT, BLOCK_WIDTH, BLOCK_HEIGHT);
            g.setColor(c);
            //改變下一次的顏色
            if(color==Color.RED){
                color = Color.BLUE;
            }
            else{
                color = Color.RED;
            }

        }
        //用於碰撞檢測
        public Rectangle getRect(){
            return new Rectangle(col*BLOCK_WIDTH, row*BLOCK_HEIGHT, BLOCK_WIDTH, BLOCK_HEIGHT);
        }

    }

蛇吃蛋,怎麼樣才能判斷蛇吃到蛋了呢,這就需要用到碰撞檢測了。

這裡我們在Snake類中新增一個eatEgg方法。當蛇吃到蛋之後,就需要將蛇的長度+1,這裡處理的是在蛇的頭部新增一個節點,當蛋被吃掉之後,就需要再重新隨機產生一個蛋。

程式碼如下:

    public Rectangle getRect(){
        return new Rectangle(head.col*BLOCK_WIDTH, head.row*BLOCK_HEIGHT, BLOCK_WIDTH, BLOCK_HEIGHT);
    }

    public boolean eatEgg(Egg egg){

        if(this.getRect().intersects(egg.getRect())){
            addNodeInHead();
            egg.reAppear();
            return true;
        }
        else{
            return false;
        }
    }

以上就完成了蛇吃蛋的功能。

完成的功能:新增邊界處理

在我們熟悉的貪吃蛇遊戲中,我們一般都知道,當蛇撞到牆或者是撞到自己身體的某一部分,則遊戲就結束。下面我們就來實現這一功能。

在Snake類中,新增checkDead方法

    private void checkDead() {
        //頭結點的邊界檢查
        if(head.row<2||head.row>SnakeFrame.ROW||head.col<0||head.col>SnakeFrame.COL){
            this.sf.gameOver();
        }

        //頭結點與其它結點相撞也是死忙
        for(Node node =head.next;node!=null;node = node.next){
            if(head.row==node.row&&head.col == node.col){
                this.sf.gameOver();
            }
        }
    }

如果蛇撞牆或是撞到自己本身的某一個部分。則呼叫SnakeFrame類中的gameOver()方法來進行一定的處理。

本遊戲的處理方法為:通過設定一個boolean 變數,來停止遊戲並提示相關資訊。

具體程式碼如下:

    private boolean b_gameOver = false;

    public void gameOver(){
        b_gameOver = true;
    }

    @Override
    public void update(Graphics g) {
        //其它程式碼省略
        if(b_gameOver){
            g.drawString("遊戲結束!!!", ROW/2*BLOCK_HEIGHT, COL/2*BLOCK_WIDTH);
        }

    }

以上就完成了蛇是否撞牆或是撞到自身一部分的功能。

小結

以上基本上實現了貪吃蛇的基本功能。剩下的一些功能不再介紹,例如:新增得分記錄、通過鍵盤某按鍵來控制遊戲的停止、重新開始、再來一局等。

以上的功能雖然沒有介紹,但是在程式碼中,我有實現這些相應的功能。