欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-18 19:58:58
...

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

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

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

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;
        }
    }