Android 300行代码实现经典小游戏贪吃蛇

前言

贪吃蛇算是一个非常经典的小游戏了,本人00后,初次游玩是在小时候用诺基亚手机进行游玩的。这次算是复刻一下经典hhh,贪吃蛇算是一个制作起来非常简单的小游戏,本文使用Kotlin语言进行开发,用Java的小伙伴可以自己对照代码转化一下,不过2021年了写Android还是有必要学学Kotlin的。后面我也会出一些Kotlin知识点文章。

制作思路

地图

地图的创建可以使用一个二维数组,或者一维数组,这里使用二维数组,到时候操作就通过xy进行操作。

蛇头蛇身和食物

蛇头可以通过一个Point类进行标识,食物同理。蛇身呢就可以用一个List,里面存放Point,蛇头也需要存放进去。

地图元素标识

地图使用了二维数组,那么里面存放的值就必须要有一定的含义,所以可以创建两个类,一个用来设置标识,一个类用来声明标识,一当然也可以直接写在一起,这里我会抽离出来两个类。

移动

移动就是上下左右,可以通过滑动进行移动,也可以通过按钮点击进行移动,这里我使用按钮点击进行移动,滑动移动我后续也会补充。移动的话有个规则,就是当蛇在一个方向移动时,它不能直接转向到它的反方向,比如正在向右移动,改变移动方向时不能直接就向左移动。

游戏结束

结束条件很简单,就是吃到自己身体就结束,有些版本还有碰到边界就结束,我这版本是碰到边界不结束,而是继续移动。

效果预览

开始制作

创建标识

Type类

object Type {
    const val GRID = 0
    const val FOOD = 1
    const val HEAD = 2
    const val BODY = 3
}

有了标识其实还不够,还需要一个类用来设置标识,根据标识的不同返回不同的颜色才行

class GameStyle(var type: Int) {
    fun getColor() = when (type) {
        Type.BODY -> Color.BLUE
        Type.HEAD -> Color.RED
        Type.GRID -> Color.GRAY
        Type.FOOD -> Color.YELLOW
        else -> Color.GRAY
    }
}

代码都很简单,就不细说了

方向也需要标识

object Direction {
    const val LEFT = 0
    const val RIGHT = 1
    const val UP = 2
    const val DOWN = 3
}

创建完这三个类就可以开始写游戏类了

创建GameView

class GameView : View, Runnable {

    override fun onDraw(canvas: Canvas) {}
    
    override fun run() {}
    
    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int): super(context,attrs,defStyleAttr)
}

创建完即成View,同时实现Runnable接口,因为贪吃蛇需要不停的移动,我们可以开一个线程在子线程中进行。

现在创建一些后续需要的变量,这块代码都很简单同时都写了注释可以根据注释进行理解

一些变量

private val gameSize = 14 // 地图的长宽
private var screenHeight = 0 // 屏幕的整体高度
private var screenWidth = 0 // 屏幕的整体宽度

private val map = arrayListOf<ArrayList<GameStyle>>() // 整个地图的元素
private val snakeLocation = arrayListOf<Point>() // 蛇的位置
private val snakeHead = Point(gameSize / 2, gameSize / 2) // 蛇头位置
private var foodLocation = Point() // 食物位置

private var moveSpeed = 4 // 移动速度
private var snakeLength = 4 // 蛇的长度
private var snakeDirection = Direction.UP // 移动方向

private var eatCount = 0 // 吃的食物数量

private val thread = Thread(this) // 游戏线程
private var gameStart = false // 游戏是否开始

private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG) // 画笔

/**
* 在onSizeChanged可以获取到外部给GameView设置的宽高,所以这里给先前创建的变量进行赋值
*/
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    screenHeight = height
    screenWidth = width
}

初始化函数

绘制之前还需要一个初始化函数,然后再第三个构造函数中调用

/**
 * 初始化函数
 */
private fun init() {
    // 地图初始化
    for (y in 0 until gameSize) {
        val styleList = arrayListOf<GameStyle>()
        for (x in 0 until gameSize) {
            styleList.add(GameStyle(Type.GRID)) // 默认全部为格子
        }
        map.add(styleList)
    }
    // 随机食物的位置
    randomCreateFood()

    // 蛇头位置更新到蛇身上
    snakeLocation.add(Point(snakeHead.x, snakeHead.y))

    gameStart = true
    thread.start() // 开始游戏
}

// 第三个构造函数中调用
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
    context,
    attrs,
    defStyleAttr
) {
    init()
}

你会发现这里调用了一个新的函数,就是随机食物位置。

随机食物位置

食物的位置理应是随机的,但是它不能随机到蛇的身上,所以需要一个循环判断,如果到蛇身上了就重新随机

/**
 * 随机生成食物
 */
private fun randomCreateFood() {
    var food = Point(Random.nextInt(gameSize), Random.nextInt(gameSize))
    var index = 0
    while (index < snakeLocation.size - 1) {
        if (food.x == snakeLocation[index].x && food.y == snakeLocation[index].y) {
            food = Point(Random.nextInt(gameSize), Random.nextInt(gameSize))
            index = 0
        }
        index++
    }

    foodLocation = food
    refreshFood()
}

代码很简单,可以根据上述描述理代码

食物刷新

生成了食物还不够,还需要刷新到地图中,最后绘制出来,所以在末尾调用了refreshFood(),这一块代码就更加简单了,一看应该就能懂

/**
 * 食物更新到地图上
 */
private fun refreshFood() {
    map[foodLocation.y][foodLocation.x].type = Type.FOOD
}

重写onDraw(canvas: Canvas)

现在就需要绘制了,绘制我们可以通过不同的标识设置画笔是否是空心还是实心,如果是网格就空心,食物身体头都为实心,颜色一样可以通过标识进行获取,绘制的形状就可以绘制矩形.

完整的onDraw(canvas: Canvas)代码如下:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val blockWidth = screenWidth / gameSize // 每个网格的宽度
    val blockHeight = screenHeight / gameSize // 每个网格的高度
    
    // 绘制地图元素
    for (y in 0 until gameSize) {
        for (x in 0 until gameSize) {
            // 每个矩形的范围
            val left = x * blockWidth.toFloat()
            val right = (x + 1f) * blockWidth
            val top = y * blockHeight.toFloat()
            val bottom = (y + 1f) * blockHeight
            
            // 不同的标识设置不同的画笔样式
            when (map[y][x].type) {
                Type.GRID -> mPaint.style = Paint.Style.STROKE
                Type.FOOD, Type.BODY -> mPaint.style = Paint.Style.FILL
            }
            // 根据标识设置画笔颜色
            mPaint.color = map[y][x].getColor()
            
            // 当前的位置是否为头部
            if (x == snakeHead.x && y == snakeHead.y) {
                mPaint.style = Paint.Style.FILL
                mPaint.color = GameStyle(Type.HEAD).getColor()
            }
            
            // 绘制矩形
            canvas.drawRect(left, top, right, bottom, mPaint)
        }
    }
}

蛇的移动

如果蛇向上移动并且头部在移动一次的时候小于0,这时候我们就需要蛇头部在下一个移动的位置到gameSize - 1的位置上,不然就是直接当前的位置-1,最后我们将移动后的位置加入到蛇的位置数组中,最后的代码就是这样:

/**
 * 移动
 */
private fun moveSnake() {
    when (snakeDirection) {
        Direction.LEFT -> {
            if (snakeHead.x - 1 < 0) {
                snakeHead.x = gameSize - 1
            } else {
                snakeHead.x = snakeHead.x - 1
            }
            snakeLocation.add(Point(snakeHead.x, snakeHead.y))
        }
        Direction.RIGHT -> {
            if (snakeHead.x + 1 >= gameSize) {
                snakeHead.x = 0
            } else {
                snakeHead.x = snakeHead.x + 1
            }
            snakeLocation.add(Point(snakeHead.x, snakeHead.y))
        }
        Direction.UP -> {
            if (snakeHead.y - 1 < 0) {
                snakeHead.y = gameSize - 1
            } else {
                snakeHead.y = snakeHead.y - 1
            }
            snakeLocation.add(Point(snakeHead.x, snakeHead.y))
        }
        Direction.DOWN -> {
            if (snakeHead.y + 1 >= gameSize) {
                snakeHead.y = 0
            } else {
                snakeHead.y = snakeHead.y + 1
            }
            snakeLocation.add(Point(snakeHead.x, snakeHead.y))
        }
    }
}

when语句判断移动的方向,内部if判断是否到边界,然后根据这个条件进行设置移动后的新值,最后添加到蛇的位置数组。

绘制蛇

移动有了现在就是绘制蛇的位置了,蛇向前移动,那么移动后的原位置就需要更新成路面,同时因为上述移动位置的更新添加了一位,就需要在绘制蛇身删除一位

private fun drawSnakeBody() {
    var length = snakeLength
    for (i in snakeLocation.indices.reversed()) {
        if (length > 0) {
            length--
        } else {
            val body = snakeLocation[i]
            map[body.y][body.x].type = Type.GRID
        }
    }

    length = snakeLength
    for (i in snakeLocation.indices.reversed()) {
        if (length > 0) {
            length--
        } else {
            snakeLocation.removeAt(i)
        }
    }
}

刷新蛇身

/**
 * 身体更新到地图上
 */
private fun refreshBody() {
    // 减1是因为不需要包括蛇头
    for (i in 0 until snakeLocation.size - 1) {
        map[snakeLocation[i].y][snakeLocation[i].x].type = Type.BODY
    }
}

判断吃

现在就是最后一步了,判断吃的方法,如果吃的是食物,那么长度+1,食物重新刷新位置,如果吃的是身体游戏直接结束

/**
 * 吃判断
 */
private fun judgeEat() {
    // 是否吃到自己
    val head = snakeLocation[snakeLocation.size - 1]
    for (i in 0 until snakeLocation.size - 2) {
        val body = snakeLocation[i]
        if (body.x == head.x && body.y == head.y) {
            gameStart = false // 吃到身体游戏结束
        }
    }

    // 吃到食物
    if (head.x == foodLocation.x && head.y == foodLocation.y) {
        snakeLength++ // 长度+1
        randomCreateFood() // 刷新食物
    }
}

run()

上述的移动和绘制需要在自线程中执行,所以需要写到run方法里面

override fun run() {
    while (gameStart) {
        moveSnake() // 移动蛇
        drawSnakeBody() // 绘制蛇身
        refreshBody() // 刷新蛇身
        judgeEat() // 判断吃
        postInvalidate() // 刷新视图
        Thread.sleep(1000 / moveSpeed.toLong())
    }
}

外部设置移动函数

/**
 * 设置移动方向
 */
fun setMove(direction: Int) {
    when {
        snakeDirection == Direction.LEFT && direction == Direction.RIGHT -> return
        snakeDirection == Direction.RIGHT && direction == Direction.LEFT -> return
        snakeDirection == Direction.UP && direction == Direction.DOWN -> return
        snakeDirection == Direction.DOWN && direction == Direction.UP -> return
    }
    snakeDirection = direction
}

总结

现在游戏就做完啦,是不是很简单,其实贪吃蛇算是制作起来非常简单的的一款游戏,同时代码量也很少,所以我没有过多解释每个函数的含义以及一些细节,而是以注释的形式进行了解释,您可以通过制作思路配合理解,最后谢谢您的观看!

最后欢迎关注我的csdn和掘金,掘金的地址:个人主页

你可能感兴趣的:(Android,Canvas游戏制作,kotlin,canvas,android)