最詳細的Android貪喫蛇,人人都學的會

關於我

我是IsCoding,11年開始做 Android 開發 已經做了7年
在創業公司負責過技術,拿到過融資。
想做一些事。這件事我想了很久研究了很久,現在時機成熟了。
QQ羣號 121915371
QQ 號 1400100300 (個人QQ 建議加羣諮詢)

引言

我相信大部分人都應該玩過貪喫蛇。具體規則大家都懂,這裏不一一介紹規則。
在網上有很多貪喫蛇的代碼,爲什麼你還要看我的代碼。看了我的方式有什麼好處。
這裏我跟你保證,只要你有點Java基礎,看半個小時,自己就能獨立寫出來一個完整的貪喫蛇遊戲。而且能真的理解爲什麼這麼寫程序。
如果有不懂的地方可以加上面的羣諮詢

分析

下面來講講如何寫一個程序。
分析界面,界面元素有三個,一個是背景格子,一個是蛇,一個是隨機產生的點。
那麼我們就可以定義三個對象
GridBean 格子對象 就是遊戲的背景表格。
PointBean 點對象,就是隨機產生的點
SnakeBean 蛇的對象
整個遊戲用戶看到的就是這幾個東西,我們要做的就是把這些東西畫到頁面上。

操作,遊戲過程主要的操作就是上下左右。
我們可以用按鈕實現,在頁面上添加四個按鈕,供用戶點擊
也可以用手勢實現,用戶在頁面上滑動來改變方向。

這幾個功能實現了,一個小遊戲就基本完成了,我們還可以添加暫停,開始,添加速度等等操作。

實現

接下來我們一步一步分析,把遊戲實現起來

第一步

我們要定義三個類,就是上面我們說的三個類
先寫第一個PointBean用來表示頁面上的一個格子的位置
我們用想x,y來確定格子的位置,值是從0開始計算的。
比如0,0代表的是左上角第一個格子,2,1代表第三行,第二個格子,爲什麼從0開始,是因爲數組下標從0開始,具體這裏不解釋了。
代碼非常簡單,這裏直接給出來了

public class PointBean {
    private int x;
    private int y;

    public PointBean(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }
}

第二個類SnakeBean 這個類就很有意思了,其實蛇就是由一個個連續的小格子組成的,SnakeBean 裏面就只有一個PointBean的List 代碼如下

public class SnakeBean {
    private List<PointBean> snake  = new LinkedList<PointBean>();
    public List<PointBean> getSnake() {
        return snake;
    }
    public void setSnake(List<PointBean> snake) {
        this.snake = snake;
    } 
}

其實這裏我們只要在View裏面定這個List 也可以去做,但是爲了封裝需求,我們還是定義了一個Bean
接下來我們定義的是格子類
格子類主要包括幾個東西,間距這裏方便起見所有間距設置一樣,也可以爲每個方向設置一個間距,第二個就是格子的總長度,還有每個格子的長度,還有格子的數量
這裏以1080*1920 手機爲例子,如果需要別的手機直接把這個值賦值爲獲取手機值就行

public class GridBean {

    private int   height = 1920; //手機高
    private int   width = 1080; //手機寬

    private int offset = 90 ;//偏移量,就是間距  上 左 右 間距一樣

    private int gridSize = 30;//每行格子的數量
    private int lineLength;//線的長度
    private int gridWidth  ;//格子寬

    public GridBean() {
        lineLength = width - offset * 2;
        gridWidth = lineLength / gridSize;// 格子數量
    }

    public int getOffset() {
        return offset;
    }

    public void setOffset(int offset) {
        this.offset = offset;
    }

    public int getGridSize() {
        return gridSize;
    }

    public void setGridSize(int gridSize) {
        this.gridSize = gridSize;
    }

    public int getLineLength() {
        return lineLength;
    }

    public void setLineLength(int lineLength) {
        this.lineLength = lineLength;
    }

    public int getGridWidth() {
        return gridWidth;
    }

    public void setGridWidth(int gridWidth) {
        this.gridWidth = gridWidth;
    }
} 

第二步,把格子和蛇畫到頁面上

首先你應該瞭解自定義View,可以在自定義View中繪製直線,長方形。
如果不瞭解請看這篇文章
http://www.jianshu.com/p/2c3eb5924389
自定義View這裏不需要多複雜的技術,只用最簡單部分就夠了

接下面我們一步步把這個自定義佈局完成
首先初始化這個View編寫一個init方法
創建格子對象
初始化蛇對象

 private void init() {
        gridBean = new GridBean();//創建格子對象,畫格子時候使用
        snakeBean = new SnakeBean();//創建一個蛇對象。這時候蛇對象是空的,我們需要初始化一個值 
        PointBean pointBean = new PointBean(gridBean.getGridSize()/2,gridBean.getGridSize()/2);
        snakeBean.getSnake().add(pointBean);//定義一箇中心點 ,添加到蛇身上
    }

然後我們根據格子對象在頁面上畫直線就好了

   //畫豎線
        for (int i = 0; i <= gridBean.getGridSize(); i++) {
            int startX = gridBean.getOffset() + gridBean.getGridWidth() * i;
            int stopX = startX;
            int startY = gridBean.getOffset();//+gridBean.getGridWidth() * i
            int stopY = startY + gridBean.getLineLength();//
            canvas.drawLine(startX, startY, stopX, stopY, paint);
        }

這裏給出畫豎線的方法,畫橫線跟豎線基本一樣
然後我們畫蛇,其實就是根據點畫一個方塊

   List<PointBean> snake = snakeBean.getSnake();
        for (PointBean point : snake) {
            int startX = gridBean.getOffset() + gridBean.getGridWidth() * point.getX();
            int stopX = startX + gridBean.getGridWidth();
            int startY = gridBean.getOffset() + gridBean.getGridWidth() * point.getY();
            int stopY = startY + +gridBean.getGridWidth();
            canvas.drawRect(startX, startY, stopX, stopY, paint);
        }

運行程序,貪喫蛇這個項目就做出來了,蛇會背景都繪製完成了。
這裏給出完整代碼,直接運行就能看到效果了。

public class GameView extends View {
    private Paint paint = new Paint();

    private GridBean gridBean;
    private SnakeBean snakeBean;


    public GameView(Context context) {
        super(context);
        init();

    }

    public GameView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        gridBean = new GridBean();//創建格子對象,畫格子時候使用
        snakeBean = new SnakeBean();//創建一個蛇對象。這時候蛇對象是空的,我們需要初始化一個值

        PointBean pointBean = new PointBean(gridBean.getGridSize()/2,gridBean.getGridSize()/2);
        snakeBean.getSnake().add(pointBean);//定義一箇中心點 ,添加到蛇身上
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (gridBean != null) {
            paint.setColor(Color.RED);
            drawGrid(canvas);
        }
        if (snakeBean != null) {
            paint.setColor(Color.GREEN);
            drawSnake(canvas);
        }
    }

    private void drawSnake(Canvas canvas) {
        List<PointBean> snake = snakeBean.getSnake();
        for (PointBean point : snake) {
            int startX = gridBean.getOffset() + gridBean.getGridWidth() * point.getX();
            int stopX = startX + gridBean.getGridWidth();
            int startY = gridBean.getOffset() + gridBean.getGridWidth() * point.getY();
            int stopY = startY + +gridBean.getGridWidth();
            canvas.drawRect(startX, startY, stopX, stopY, paint);
        }
    }

    private void drawGrid(Canvas canvas) {
        //畫豎線
        for (int i = 0; i <= gridBean.getGridSize(); i++) {
            int startX = gridBean.getOffset() + gridBean.getGridWidth() * i;
            int stopX = startX;
            int startY = gridBean.getOffset();//+gridBean.getGridWidth() * i
            int stopY = startY + gridBean.getLineLength();//
            canvas.drawLine(startX, startY, stopX, stopY, paint);
        }
        //畫橫線
        for (int i = 0; i <= gridBean.getGridSize(); i++) {
            int startX = gridBean.getOffset();//+gridBean.getGridWidth() * i
            int stopX = startX + gridBean.getLineLength();

            int startY = gridBean.getOffset() + gridBean.getGridWidth() * i;
            int stopY = startY;
            canvas.drawLine(startX, startY, stopX, stopY, paint);
        }
    }


}

好了今天課程講完了。

開個玩笑,寫了這麼半天我們只實現了一個功能就是,繪製頁面。
接下來我們要實現的功能就是如何讓蛇動起來,這樣纔是一個遊戲嘛
那麼如何讓蛇動起來呢,其實很簡單,就是不停的繪製頁面,繪製蛇在不同位置的頁面,這樣就可以繪製出蛇動的效果了。
如何不停的繪製頁面呢,這裏只要啓動一個線程,不停的發送消息就可以了,告訴頁面要重新繪製。

這裏分析下控制流程
貪喫蛇遊戲默認有個移動方向,比如上,用戶可以操作上下左右改變方向。
所以這裏定義一個操作類,用於記錄用戶的操作。
上下左右是默認值,可以用枚舉實現,更適合

public enum  Control {
  UP,DOWN,LEFT,RIGHT
}

這裏簡單分析下用戶操作時候發生的變化。
蛇默認會在收到消息之後往上走一個,如果用戶做了一個轉左的操作,那麼蛇的下一步就是往左走一個,實際上蛇每次都是按一個固定方向行走的,那麼我們寫代碼的時候只要實現一個方法是根據下一步方向來繪製蛇就行,而用戶的操作僅僅只是告訴View下一步的方向是什麼。
這樣做的一個好處就是,把操作分割開了,便於實現,用戶的操作只是告訴了View去改變方向,而蛇每次的真正移動只是根據線程消息移動,這兩個地方就不會相互影響了。

好了現在我們來實現這個步驟
我們定義一個Control對象記錄用戶下一步要走的方向。
我們刷新頁面是根據下一步走的方向去改變蛇的位置的。
private Control control = Control.UP;
然後我們看看刷新頁面時候我們要做什麼。
刷新頁面說明蛇是要往前走一個,根據下一步方向走。
我們如何實現蛇走下一步呢
其實這個規則很簡單就是在蛇的control方向添加一個點,然後刪除最後一個點,這樣蛇就根據方向變化了
然後刷新頁面就ok了

  public void refreshView(boolean isAdd){
        List<PointBean> pointList = snakeBean.getSnake();
        PointBean point = pointList.get(0); 
        PointBean pointNew = null;
        if (control == Control.LEFT) {
            pointNew = new PointBean(point.getX() - 1, point.getY());
        } else if (control == Control.RIGHT) {
            pointNew = new PointBean(point.getX() + 1, point.getY());
        } else if (control == Control.UP) {
            pointNew = new PointBean(point.getX(), point.getY() - 1);
        } else if (control == Control.DOWN) {
            pointNew = new PointBean(point.getX(), point.getY() + 1);
        }
        if (pointNew != null) {
            pointList.add(0, pointNew);
            if(!isAdd){
                pointList.remove(pointList.get(pointList.size() - 1));
            }
        }

        invalidate();
        //此處只是刷新頁面
        //刷新頁面會重新繪製
    }

這個就是刷新頁面的方法了
裏面的規則就是根據位置確定下一個頂點的位置,然後把這個新的頂點添加到蛇的頭部,刪除蛇的尾部就可以了。邏輯是不是很清晰?
這樣蛇就從上一個位置移動到下一個位置了。
現在就開啓線程不停的調用刷新方法蛇就可以動起來了。
下面給出開啓線程的代碼

public class MainActivity extends AppCompatActivity {
    public static final int WHAT_REFRESH = 200;
    private GameView gameView;
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if(WHAT_REFRESH == msg.what){
                gameView.refreshView(false);
                sendControlMessage();
            }
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        gameView = new GameView(this);
        setContentView(gameView);
        sendControlMessage();
    }
    private void sendControlMessage(){
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                handler.sendEmptyMessage(WHAT_REFRESH);
            }
        },300);
    }
} 

這段代碼只是不停的調用線程每300 毫秒告訴一次View要刷新頁面
此時View的完整代碼是這個

public class GameView extends View {
    private Paint paint = new Paint();

    private GridBean gridBean;
    private SnakeBean snakeBean;

    private Control control = Control.UP;
    public GameView(Context context) {
        super(context);
        init();

    }

    public GameView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        gridBean = new GridBean();//創建格子對象,畫格子時候使用
        snakeBean = new SnakeBean();//創建一個蛇對象。這時候蛇對象是空的,我們需要初始化一個值

        PointBean pointBean = new PointBean(gridBean.getGridSize()/2,gridBean.getGridSize()/2);
        snakeBean.getSnake().add(pointBean);//定義一箇中心點 ,添加到蛇身上
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (gridBean != null) {
            paint.setColor(Color.RED);
            drawGrid(canvas);
        }
        if (snakeBean != null) {
            paint.setColor(Color.GREEN);
            drawSnake(canvas);
        }
    }

    private void drawSnake(Canvas canvas) {
        List<PointBean> snake = snakeBean.getSnake();
        for (PointBean point : snake) {
            int startX = gridBean.getOffset() + gridBean.getGridWidth() * point.getX();
            int stopX = startX + gridBean.getGridWidth();
            int startY = gridBean.getOffset() + gridBean.getGridWidth() * point.getY();
            int stopY = startY + +gridBean.getGridWidth();
            canvas.drawRect(startX, startY, stopX, stopY, paint);
        }
    }

    private void drawGrid(Canvas canvas) {
        //畫豎線
        for (int i = 0; i <= gridBean.getGridSize(); i++) {
            int startX = gridBean.getOffset() + gridBean.getGridWidth() * i;
            int stopX = startX;
            int startY = gridBean.getOffset();//+gridBean.getGridWidth() * i
            int stopY = startY + gridBean.getLineLength();//
            canvas.drawLine(startX, startY, stopX, stopY, paint);
        }
        //畫橫線
        for (int i = 0; i <= gridBean.getGridSize(); i++) {
            int startX = gridBean.getOffset();//+gridBean.getGridWidth() * i
            int stopX = startX + gridBean.getLineLength();

            int startY = gridBean.getOffset() + gridBean.getGridWidth() * i;
            int stopY = startY;
            canvas.drawLine(startX, startY, stopX, stopY, paint);
        }
    }

    public void refreshView(boolean isAdd){
        List<PointBean> pointList = snakeBean.getSnake();
        PointBean point = pointList.get(0);
        PointBean pointNew = null;
        if (control == Control.LEFT) {
            pointNew = new PointBean(point.getX() - 1, point.getY());
        } else if (control == Control.RIGHT) {
            pointNew = new PointBean(point.getX() + 1, point.getY());
        } else if (control == Control.UP) {
            pointNew = new PointBean(point.getX(), point.getY() - 1);
        } else if (control == Control.DOWN) {
            pointNew = new PointBean(point.getX(), point.getY() + 1);
        }
        if (pointNew != null) {
            pointList.add(0, pointNew);
            if(!isAdd){
                pointList.remove(pointList.get(pointList.size() - 1));
            }
        }

        invalidate();
        //此處只是刷新頁面
        //刷新頁面會重新繪製
    }
}

運行的小夥伴有沒有發現,蛇跑出格子了,沒錯這裏沒有加任何的判斷,蛇會一直往上走。
這裏我們要做的就是添加一個判斷結束的方法
這裏只簡單寫一個判斷蛇越界算輸的方法,小夥伴可以自己完善項目做更多的判斷輸贏

  private boolean isFailed( PointBean point){
        if (point.getX() == 0 && control == Control.UP ) { 
            return true;
        } else if ( point.getY()  == 0 && control == Control.LEFT) {
            return true;
        } else if (point.getX() == gridBean.getGridSize() - 1 && control == Control.DOWN  ) {
            return true;
        } else if (point.getY() == gridBean.getGridSize() - 1 && control == Control.RIGHT ) {
            return true;
        }
        return false;
    }

下面我們要爲遊戲添加手勢了喲
這個地方其實網上很多地方都有,就是獲取滑動方向的,
獲取完方向如何使用呢。因爲之前用了個小技巧,這裏其實做的就是改變之前control的值而已,沒有其他任何操作,直接上代碼

  int x;
    int y;
    @Override
    public boolean onTouchEvent(MotionEvent event) { //通過手勢來改變蛇體運動方向 
        int action = event.getAction()  & MotionEvent.ACTION_MASK;
        LogUtil.e("x =" + x + " y = " + y + " action() = " + action);
        // TODO Auto-generated method stub
        if (action == KeyEvent.ACTION_DOWN) {
            //每次Down事件,都置爲Null
            x = (int) (event.getX());
            y = (int) (event.getY());
        }
        if (action== KeyEvent.ACTION_UP) {
            //每次Down事件,都置爲Null
            int x = (int) (event.getX());
            int y = (int) (event.getY());

//            新建一個滑動方向,
            Control control  = null ;
            // 滑動方向x軸大說明滑動方向爲 左右
            if (Math.abs(x - this.x) > Math.abs(y - this.y)) {
                if (x > this.x) {
                    control = Control.RIGHT;
                    LogUtil.i("用戶右劃了");
                }
                if (x < this.x) {
                    control = Control.LEFT;
                    LogUtil.i("用戶左劃了");
                }
            }else{
                if (y < this.y) {
                    control = Control.UP;
                    LogUtil.i("用戶上劃了");
                }
                if (y > this.y) {
                    control = Control.DOWN;
                    LogUtil.i("用戶下劃了");
                }
            }

            if (this.control == Control.UP || this.control == Control.DOWN) {
                if(control==Control.UP ||Control.UP==Control.DOWN ){
                    LogUtil.i("已經是上下移動了,滑動無效");
                }else{
                    this.control = control;
                }
            } else if (this.control == Control.LEFT || this.control == Control.RIGHT) {
                if(control==Control.LEFT ||Control.UP==Control.RIGHT ){
                    LogUtil.i("已經是左右移動了,滑動無效");
                }else{
                    this.control = control;
                }
            }
        }
        //Log.e(TAG, "after adjust mSnakeDirection = " + mSnakeDirection);
        return super.onTouchEvent(event);
    }

代碼有點多
簡單分析下,
根據用戶手指落下 和擡起位置的差值,計算用戶是哪個方向滑動了。
如果用戶滑滑動方向是左 判斷用戶是否可以左滑,比如用戶之前是往右是不允許左劃的 ,滑動也不會有任何處理。這裏就是幾個簡單判斷,實際使用的就是 把Control的方向改爲用戶滑動成功的方向。
這樣整個程序基本就做完了。
剩下的就是優化項目了,第一個就是現在程序的蛇只有一個點,我們希望蛇是不斷增加的
那麼我們可以設置一個方法讓蛇 在一段時間就增加一個格子 也可以隨機生成一個 點,然後蛇碰到這個點就增加一個一個點,第二個就是速度問題,我們希望隨着時間的推移蛇越來越快
這裏我先設置個計步器,count 每20步 我就讓蛇添加1格

https://github.com/zujianhua/Snake
完整源碼發佈到Github上,如有問題請指正。
歡迎加上面的羣來交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章