贪吃蛇算是一个非常经典的小游戏了,本人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
}
创建完这三个类就可以开始写游戏类了
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)代码如下:
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方法里面
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和掘金,掘金的地址:个人主页