100行代码教你实现贪吃蛇小游戏

最近项目中内置了一些比如贪吃蛇,俄罗斯方块,井字棋等小游戏. 这里逐一将实现步骤分享出来供大家学习. 如有不足或错误,请在评论去指正

因为只是个示例, 界面可能并不好看, 这些你们可以使用更好看的资源来替换方格,最终实现效果如下:

实现效果

其实这些小游戏的实现并不复杂,只要理清楚思路, 一步一步构建下去, 就会发现其实原理很简单.只是我们想复杂了. 完整Demo地址: 贪吃蛇游戏Demo. 自定义视图的源码可以直接看这里. 贪吃蛇游戏视图

实现步骤及说明如下:

1. 自定义游戏界面

游戏的话不用多说,肯定是自定义视图来实现界面的绘制. 这里我们直接继承View, 重写draw方法来绘制我们的游戏界面, 因为小游戏比较轻量级,这里我没有使用SurfaceView来绘制界面

2. 游戏属性值的定义

贪吃蛇应该都是有玩过的, 这里我们简单定义出 方向,行列格子数,蛇的颜色及食物颜色, 蛇运动方向,代码及说明如下:

	   /**
     * 蛇头方向常量
     */
    public final static int S_LEFT = 0;
    public final static int S_TOP = 1;
    public final static int S_RIGHT = 2;
    public final static int S_BOTTOM = 3;
    public final static int S_BASE_SPEED = 300;//基础速度,刷新毫秒
    int mColNum = 18;//列数
    int mRowNum = 18;//列数
    float mColWidth;//列宽

    int mBgColor = Color.parseColor("#a7a6ab");//背景颜色
    int mGridLineColor = Color.parseColor("#838692");//网格线颜色
    int mSnackBodyColor = Color.WHITE;//蛇身颜色
    int mSnackHeadColor = Color.WHITE;//蛇头颜色
    int mFoodColor = Color.parseColor("#fcae14");//食物颜色
    int mSnackDefLen = 3;//蛇身默认长度
    int mDirec = S_LEFT;//蛇头方向
    int mSpeed = S_BASE_SPEED;//移动速度
    boolean mGamePause = false;
    LinkedList<SnackBody> mSnackBodys = new LinkedList<>();//蛇身
    SnackBody mFood = new SnackBody(0, 0);//食物

3. 常量及思路说明.

这里蛇身使用的是双向链表来实现的,简化添加移除操作. 食物和身体都是方块构成, 所以这里直接使用单个SnackBody 类来表示食物

4. 界面绘制.

界面的绘制可以简化成背景 ,网格 食物和蛇的绘制, 代码如下:

   protected void onDraw(Canvas canvas) {
        canvas.drawColor(mBgColor);
        drawFood(canvas);
        drawSnack(canvas);
        drawGridBg(canvas);
    }

5. 界面绘制- 绘制网格.

即画横线和竖线, 这里方便理解使用了两个循环. (可以合并成一个)

  /**
     * 绘制网格背景
     * @param canvas
     */
    private void drawGridBg(Canvas canvas) {
        int colNum = this.mColNum;
        int rowNum = (int) (getHeight() / mColWidth);//计算行数
        //绘制网格
        mPaint.setColor(mGridLineColor);
        //绘制列
        for (int i = 0; i <= colNum; i++) {
            float startX = i * mColWidth;
            canvas.drawLine(startX, 0, startX, getHeight(), mPaint);
        }
        //绘制行
        for (int i = 0; i <= rowNum; i++) {
            float startY = i * mColWidth;
            canvas.drawLine(0, startY, getWidth(), startY, mPaint);
        }
    }

6. 界面绘制- 绘制食物

    /**
     * 绘制食物
     * @param canvas
     */
    private void drawFood(Canvas canvas) {
        SnackBody mFood = this.mFood;
        mPaint.setColor(mFoodColor);
        float left = mFood.x * mColWidth;
        float top = mFood.y * mColWidth;
        canvas.drawRect(left, top, left + mColWidth, top + mColWidth, mPaint);
    }

7. 界面绘制-绘制蛇

    /**
     * 绘制蛇
     * @param canvas
     */
    private void drawSnack(Canvas canvas) {
        LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
        for (int i = mSnackBodys.size() - 1; i >= 0; i--) {
            SnackBody mSnackBody = mSnackBodys.get(i);
            mPaint.setColor(i == 0 ? mSnackHeadColor : mSnackBodyColor);
            float left = mSnackBody.x * mColWidth;
            float top = mSnackBody.y * mColWidth;
            canvas.drawRect(left, top, left + mColWidth, top + mColWidth, mPaint);
        }
    }

8. 相关逻辑处理.

贪吃蛇的逻辑比较简单.可以分为三个块. 食物随机逻辑 蛇移动逻辑 游戏状态检查

9. 食物随机逻辑

    //随机食物
    private void randomFood() {
        LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
        if (mSnackBodys.size() >= mColNum * mRowNum) {
            return;//满了,这么吊的么
        }
        SnackBody first = mSnackBodys.getFirst();
        SnackBody food = new SnackBody(0, 0);
        do {
        //随机一个点,不在蛇头上就行. 这里允许食物和身体重叠
            food.setPositon(random(0, mColNum), random(0, mRowNum));
        } while (first.equals(food));
        this.mFood = food;
    }

10. 蛇移动逻辑

/**
     * 蛇的移动
     */
    private void snackMove() {
        if (mGamePause) return;
        LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
        SnackBody nextHead = new SnackBody(0, 0);
        SnackBody snackHead = mSnackBodys.getFirst();
        switch (mDirec) {
            case S_LEFT:
                nextHead.setPositon(snackHead.x - 1, snackHead.y);
                break;
            case S_TOP:
                nextHead.setPositon(snackHead.x, snackHead.y - 1);
                break;
            case S_RIGHT:
                nextHead.setPositon(snackHead.x + 1, snackHead.y);
                break;
            case S_BOTTOM:
                nextHead.setPositon(snackHead.x, snackHead.y + 1);
                break;
        }
        mSnackBodys.addFirst(nextHead);
        mSnackBodys.removeLast();
        //检查是否吃到食物了,
        SnackBody mFood = this.mFood;
        if (nextHead.equals(mFood)) {
            mSnackBodys.addFirst(mFood);
            mSpeed = getSpeed();//计算新速度
            if (onEatFoodListener != null) {
                onEatFoodListener.eatFood(mSpeed);
            }
            //追加到头部,重新随机食物
            randomFood();
        }
        checkGameEnd();
    }

11. 游戏状态检查逻辑

  private void checkGameEnd() {
        LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
        SnackBody headBody = mSnackBodys.getFirst();
        //查询头部与身体的交集
        int indexOf = mSnackBodys.lastIndexOf(headBody);
        int cellNum = mColNum * mRowNum;
        int snackLen = mSnackBodys.size();
        //边界判断,咬到自己的判断,和占满了
        if (headBody.x < 0 ||
                headBody.y < 0 ||
                headBody.x >= mColNum ||
                headBody.y >= mRowNum ||
                //0 是蛇头 1是刚吃到的食物 不可能咬到脖子(第二格)
                (indexOf != 0 &&indexOf != 1 && indexOf != -1)||
                    snackLen>=cellNum
                ) {
            mGamePause = true;
            if (onGameOverListener != null) {
                onGameOverListener.gameOver(snackLen, cellNum);
            }
            Logger.D("游戏结束");
        }
    }

12. 游戏界面刷新重绘逻辑

   private void beginGameRun() {
        removeCallbacks(runnable);
        final Runnable localRun = new Runnable() {
            @Override
            public void run() {
                int speed = mIsQuickMove ? mQuickSpeed : mSpeed;
                snackMove();//自动移动
                invalidate();
                postDelayed(runnable, speed);
            }
        };
        postDelayed(this.runnable = localRun, mSpeed);
    }

13.方向控制逻辑

public void turnTo(int direc) {
        switch (direc) {
            case S_LEFT:
                if (mDirec != S_RIGHT) {
                    mDirec = direc;
                }
                break;
            case S_TOP:
                if (mDirec != S_BOTTOM) {
                    mDirec = direc;
                }
                break;
            case S_RIGHT:
                if (mDirec != S_LEFT) {
                    mDirec = direc;
                }
                break;
            case S_BOTTOM:
                if (mDirec != S_TOP) {
                    mDirec = direc;
                }
                break;
        }
        beginGameRun();//重置重绘时间
        snackMove();//移动蛇
        invalidate();//刷新界面
    }

14.蛇身体方块实体类

class SnackBody {
        public int x, y;//坐标

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

        public SnackBody(int x, int y) {
            this.x = x;
            this.y = y;
        }
//以下为自动生成代码,由坐标来判断方块是否一样
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            SnackBody snackBody = (SnackBody) o;
            if (x != snackBody.x) return false;
            return y == snackBody.y;
        }

        @Override
        public int hashCode() {
            int result = x;
            result = 31 * result + y;
            return result;
        }
    }