preface

Snake is a very classic little game, I 00, the first play is in childhood with nokia mobile phone for play. This is a copy of the classic HHH, Snake is a very simple small game, this article uses Kotlin language development, with Java partners can compare their own code transformation, but 2021 writing Android is still necessary to learn Kotlin. I will also post some Kotlin articles later.

Make ideas

The map

You can create a map using a two-dimensional array, or a one-dimensional array, in this case a two-dimensional array, and then the operation will be done by xy.

The head and the body and the food

Snake heads can be identified by a Point class, as can food. For the snake body, you can use a List to store points, and the snake head also needs to be stored in it.

Map element identification

The map uses a two-dimensional array, so the values in it have to have a certain meaning, so you can create two classes, one for setting the identity, one for declaring the identity, and of course you can write them directly together, so I’m going to separate out the two classes.

mobile

Move is up, down, left, right, you can move by sliding, you can move by button clicking, so I’m going to move by button clicking, slide moving and I’ll fill that in later. One rule of movement is that when the snake is moving in one direction, it cannot turn directly in the opposite direction, such as moving to the right, and cannot move directly to the left when changing direction of movement.

Game over

The end condition is very simple, is to eat their own body to end, some versions and meet the boundary of the end, my version is to meet the boundary does not end, but continue to move.

Results the preview

Started making

Create a logo

Type class

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

It’s not enough to have an identifier. You need a class that sets the identifier and returns a different color depending on the identifier

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

The code is very simple, I won’t go into detail

Directions also need to be identified

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

Once you’ve created these three classes, you can start writing your game class

Create the GameView

class GameView : View.Runnable {

    override fun onDraw(canvas: Canvas) {}
    
    override fun run(a) {}
    
    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)
}
Copy the code

Create the View and implement the Runnable interface, because snake needs to be constantly moving, we can open a thread and do it in a child thread.

Now I’m going to create some variables that I need later, and this is all very simple code and I’m going to comment it so you can understand it

Some variables

private val gameSize = 14 // Map length and width
private var screenHeight = 0 // The overall height of the screen
private var screenWidth = 0 // The overall width of the screen

private val map = arrayListOf<ArrayList<GameStyle>>() // The entire map element
private val snakeLocation = arrayListOf<Point>() // Snake position
private val snakeHead = Point(gameSize / 2, gameSize / 2) // Snake head position
private var foodLocation = Point() // Food location

private var moveSpeed = 4 // Move speed
private var snakeLength = 4 The length of the snake
private var snakeDirection = Direction.UP // Move direction

private var eatCount = 0 // The amount of food eaten

private val thread = Thread(this) // Game threads
private var gameStart = false // Whether the game starts

private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG) / / brush

/** * Inside onSizeChanged we can get the width and height set externally to the GameView, so here we assign the previously created variable */
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    screenHeight = height
    screenWidth = width
}
Copy the code

Initialization function

You also need an initialization function before drawing, which is called in the third constructor

/** * initializes the function */
private fun init(a) {
    // Map initialization
    for (y in 0 until gameSize) {
        val styleList = arrayListOf<GameStyle>()
        for (x in 0 until gameSize) {
            styleList.add(GameStyle(Type.GRID)) // All are grids by default
        }
        map.add(styleList)
    }
    // Random food location
    randomCreateFood()

    // Snake head position updated to snake
    snakeLocation.add(Point(snakeHead.x, snakeHead.y))

    gameStart = true
    thread.start() // Start the game
}

// called in the third constructor
constructor(context: Context? , attrs: AttributeSet? , defStyleAttr:Int) : super(
    context,
    attrs,
    defStyleAttr
) {
    init()}Copy the code

You’ll notice that a new function is called, which is the random food location.

Random food location

The location of the food is supposed to be random, but it can’t be random on the snake, so it needs a loop, and if it is on the snake, it needs to be random again

/** * Randomly generates food */
private fun randomCreateFood(a) {
    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()
}
Copy the code

The code is simple enough to follow the description above

Food to refresh

It’s not enough to generate the food, but you need to refresh it into the map and draw it, so refreshFood() is called at the end, which makes the code simpler and should make sense at first

/** * Food updates to the map */
private fun refreshFood(a) {
    map[foodLocation.y][foodLocation.x].type = Type.FOOD
}
Copy the code

Rewrite the ontouch (canvas, canvas)

Now we need to draw, draw we can use different markers to set whether the brush is hollow or solid, if the mesh is hollow, the food, the body and the head are solid, the color can also be obtained by the logo, draw the shape can draw the rectangle.

The complete onDraw(canvas: canvas) code is as follows:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val blockWidth = screenWidth / gameSize // The width of each grid
    val blockHeight = screenHeight / gameSize // The height of each grid
    
    // Draw map elements
    for (y in 0 until gameSize) {
        for (x in 0 until gameSize) {
            // The range of each rectangle
            val left = x * blockWidth.toFloat()
            val right = (x + 1f) * blockWidth
            val top = y * blockHeight.toFloat()
            val bottom = (y + 1f) * blockHeight
            
            // Set different brush styles for different logos
            when (map[y][x].type) {
                Type.GRID -> mPaint.style = Paint.Style.STROKE
                Type.FOOD, Type.BODY -> mPaint.style = Paint.Style.FILL
            }
            // Set the brush color according to the logo
            mPaint.color = map[y][x].getColor()
            
            // Whether the current position is the head
            if (x == snakeHead.x && y == snakeHead.y) {
                mPaint.style = Paint.Style.FILL
                mPaint.color = GameStyle(Type.HEAD).getColor()
            }
            
            // Draw a rectangle
            canvas.drawRect(left, top, right, bottom, mPaint)
        }
    }
}
Copy the code

The movement of the snake

If the snake is moving up and the head is less than zero on one move, then we need the head to be at gamesize-1 at the next move, or at the current position -1. Finally we add the moved position to the snake position array, and the final code looks like this:

/**
 * 移动
 */
private fun moveSnake(a) {
    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))
        }
    }
}
Copy the code

The when statement determines the direction of the move, the inner if determines whether it is at the edge, and then sets the new value of the move based on this condition, adding to the snake’s position array.

Draw a snake

Moving the snake is now the position of the snake. The snake moves forward, so the original position of the snake needs to be updated to the road surface. At the same time, because the update of the above move position adds a bit, it needs to delete a bit in the snake body

private fun drawSnakeBody(a) {
    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)
        }
    }
}
Copy the code

The refresh snake-body.

/** * Body updates to map */
private fun refreshBody(a) {
    // Minus 1 because you don't need to include the snake head
    for (i in 0 until snakeLocation.size - 1) {
        map[snakeLocation[i].y][snakeLocation[i].x].type = Type.BODY
    }
}
Copy the code

Judgment to eat

Now the last step, the way to determine the eating, if the eating is food, then the length +1, the food will refresh the position, if the eating is body game is over

/** * eat judgment */
private fun judgeEat(a) {
    // If you eat yourself
    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 // Eat until the body game is over}}// Eat food
    if (head.x == foodLocation.x && head.y == foodLocation.y) {
        snakeLength++ / / length + 1
        randomCreateFood() // Refresh the food}}Copy the code

run()

The above moves and draws need to be performed in the thread, so they need to be written to the run method

override fun run(a) {
    while (gameStart) {
        moveSnake() / / move the snake
        drawSnakeBody() // Draw the snake body
        refreshBody() // Refresh the snake
        judgeEat() / / to eat
        postInvalidate() // Refresh the view
        Thread.sleep(1000 / moveSpeed.toLong())
    }
}
Copy the code

Externally set the move function

/** * set the movement direction */
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
}
Copy the code

conclusion

Is now the game is finished, very simple, the snake is actually made up a very simple game, the code is also very few, so I don’t have too much to explain the meaning of each function as well as some of the details, but is explained in the form of comments, you can by making ideas with understanding, finally thank you for watching!