最详细的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上,如有问题请指正。
欢迎加上面的群来交流

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容