A practical guide to creating a classic snake game with Android native controls

A practical guide to creating a classic snake game with Android native controls

Game Instructions

Snake is a classic game, which is loved by players for its simplicity, strong strategy and high challenge.

Gameplay:

  • The player uses the arrow keys to control a long snake to swallow beans continuously. The snake's body grows longer as it swallows beans.
  • The goal of the game is to survive as long as possible while avoiding the snake's head from hitting its own body or the edge of the screen.

Game Features:

  • Easy to play: The game is simple to operate. Players only need to control the movement and turning of the snake and eat the food.
  • Strategy: Although the game seems simple, it requires players to use strategies flexibly to avoid collisions in a limited space.
  • Challenge: The game difficulty gradually increases. As the snake grows, players need to operate more carefully.

Next, we use Android native controls to implement this small game (PS: does not include custom View methods).

Implementation ideas

1. Game scene

Use GridLayout as the game board, with a size of 20x20, and also include game scores and control buttons. Here is the layout file:

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/scoreTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:text="分数: 0" android:textSize="18sp" /> <GridLayout android:id="@+id/gameBoard" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:columnCount="20" android:rowCount="20" /> <RelativeLayout android:layout_width="160dp" android:layout_height="160dp" android:layout_gravity="center"> <Button android:id="@+id/upButton" android:layout_width="60dp" android:layout_height="60dp" android:layout_centerHorizontal="true" android:text="↑" /> <Button android:id="@+id/leftButton" android:layout_width="60dp" android:layout_height="60dp" android:layout_centerVertical="true" android:text="←" /> <Button android:id="@+id/rightButton" android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:text="→" /> <Button android:id="@+id/downButton" android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:text="↓" /> </RelativeLayout> </LinearLayout>

Preview effect

 private fun initializeGame() { // 初始化蛇snake.add(Pair(boardSize / 2, boardSize / 2)) // 生成食物generateFood() // 初始化游戏板for (i in 0 until boardSize) { for (j in 0 until boardSize) { val cell = TextView(this) cell.width = 50 cell.height = 50 cell.setBackgroundColor(Color.WHITE) gameBoard.addView(cell) } } updateBoard() } private fun generateFood() { do { food = Pair(Random.nextInt(boardSize), Random.nextInt(boardSize)) } while (snake.contains(food)) } private fun updateBoard() { for (i in 0 until boardSize) { for (j in 0 until boardSize) { val cell = gameBoard.getChildAt(i * boardSize + j) as TextView when { Pair(i, j) == snake.first() -> cell.setBackgroundColor(Color.RED) snake.contains(Pair(i, j)) -> cell.setBackgroundColor(Color.GREEN) Pair(i, j) == food -> cell.setBackgroundColor(Color.BLUE) else -> cell.setBackgroundColor(Color.WHITE) } } } }

Initialize the game board to a size of 20*20, use TextView as each cell to represent a movable range grid. Initialize the snake's position in the center of the game board. The snake is represented as a MutableList<Pair<Int, Int>>, and each Pair represents the coordinates of a part of the snake's body. At the same time, randomly generate food in the range, and finally update the game board to generate different color styles for the snake and food.

2. Game main loop

The game will not move at this time. A game main loop is needed to keep the game updated to make the game screen move. Use Handler to regularly call the game update logic and update the game status every 200 milliseconds.

 private val updateDelay = 200L // 游戏更新间隔,毫秒private fun startGameLoop() { handler.postDelayed(object : Runnable { override fun run() { moveSnake() checkCollision() updateBoard() handler.postDelayed(this, updateDelay) } }, updateDelay) }

Each time an event is sent, the snake is moved, the game is checked to see if it is over (whether the snake has bitten itself), the GridLayout grid display is updated, and the next update event is sent.

3. Movement of the snake

The core logic of snake movement, calculate the new snake head position, use modulo operation to ensure that the snake can cross the game boundary, check whether it has eaten food, if so, increase the score and generate new food; otherwise, remove the snake's tail.

 private fun moveSnake() { val head = snake.first() val newHead = Pair( (head.first + direction.first + boardSize) % boardSize, (head.second + direction.second + boardSize) % boardSize ) snake.add(0, newHead) if (newHead == food) { score++ scoreTextView.text = "分数: $score" generateFood() } else { snake.removeAt(snake.size - 1) } }

(1) Get the snake head position:

 val head = snake.first()

A snake is represented as a list of coordinate pairs, with the first element being the snake's head.

(2) Calculate the new snake head position:

 val newHead = Pair( (head.first + direction.first + boardSize) % boardSize, (head.second + direction.second + boardSize) % boardSize )

direction (the direction of control) to move the snake head, plus boardSize and modulo boardSize to ensure that the new position is always within the game board

 direction = Pair(-1, 0) //上direction = Pair(1, 0) //下direction = Pair(0, -1) //左direction = Pair(0, 1) //右

(3) Add the new snake head to the beginning of the snake body list:

 snake.add(0, newHead)

The snake is always moving, and the coordinates of the snake head are always changing.

(4) Check whether food has been eaten:

 if (newHead == food) { score++ scoreTextView.text = "Score: $score" generateFood() } else { snake.removeAt(snake.size - 1) }

If the new snake head position is the same as the food position, increase the score, update the score display, and generate new food. If the food is not eaten, remove the snake tail to keep the snake's length unchanged.

4. Collision Detection

 private fun checkCollision() { val head = snake.first() if (snake.subList(1, snake.size).contains(head)) { // 游戏结束handler.removeCallbacksAndMessages(null) } }

Check if the snake head collides with the snake body, if so, the game is over.

5. Generate Food

 private fun generateFood() { do { food = Pair(Random.nextInt(boardSize), Random.nextInt(boardSize)) } while (snake.contains(food)) }

Randomly generate new food positions to ensure they do not overlap with the snake's body

6. Display Updates

 private fun updateBoard() { for (i in 0 until boardSize) { for (j in 0 until boardSize) { val cell = gameBoard.getChildAt(i * boardSize + j) as TextView when { Pair(i, j) == snake.first() -> cell.setBackgroundColor(Color.RED) snake.contains(Pair(i, j)) -> cell.setBackgroundColor(Color.GREEN) Pair(i, j) == food -> cell.setBackgroundColor(Color.BLUE) else -> cell.setBackgroundColor(Color.WHITE) } } } }

Iterate over each cell of the game board and set it to a different color depending on its state (head, body, food, or empty).

Game Effects

7. Game Start and End

At this point the snake can turn around and cross the game scene. Let's improve it so that the game ends when the snake hits the game boundary.

 private fun moveSnake() { val head = snake.first() val newHead = Pair( head.first + direction.first, head.second + direction.second ) // 检查是否撞到边界if (newHead.first < 0 || newHead.first >= boardSize || newHead.second < 0 || newHead.second >= boardSize) { endGame() return } snake.add(0, newHead) if (newHead == food) { score++ scoreTextView.text = "分数: $score" generateFood() } else { snake.removeAt(snake.size - 1) } }

Add boundary detection, detect the coordinates at the boundary of the game board, and the game ends

 findViewById<Button>(R.id.upButton).setOnClickListener { if (isGameRunning) { direction = Pair(-1, 0) } else { restartGame() } } findViewById<Button>(R.id.downButton).setOnClickListener { if (isGameRunning) { direction = Pair(1, 0) } else { restartGame() } } findViewById<Button>(R.id.leftButton).setOnClickListener { if (isGameRunning) { direction = Pair(0, -1) } else { restartGame() } } findViewById<Button>(R.id.rightButton).setOnClickListener { if (isGameRunning) { direction = Pair(0, 1) } else { restartGame() } }

Modify the click listener of the direction button to restart the game

 private fun endGame() { isGameRunning = false handler.removeCallbacksAndMessages(null) scoreTextView.text = "游戏结束!最终分数: $score\n点击任意方向键重新开始" } private fun restartGame() { snake.clear() snake.add(Pair(boardSize / 2, boardSize / 2)) direction = Pair(0, 1) score = 0 generateFood() isGameRunning = true startGameLoop() updateBoard() scoreTextView.text = "分数: 0" }

The game ends and restarts, and the isGameRunning variable controls the main game loop

 private fun startGameLoop() { handler.post(object : Runnable { override fun run() { if (isGameRunning) { moveSnake() if (isGameRunning) { // 再次检查,因为moveSnake 可能会结束游戏checkCollision() updateBoard() handler.postDelayed(this, updateDelay) } } } }) }

Complete code

Game Effects

Github source code https://github.com/Reathin/Sample-Android/tree/master/module_snake

<<:  Code reuse rate is 99%, Ctrip market insight platform Donut cross-terminal high-performance technology practice

>>:  Mini Program Development Tools: Comprehensive Analysis of Popular Development Tools and Frameworks

Recommend

I didn’t spend a penny on promotion and achieved 23 million app downloads!

The author of this article spent 6 hours to creat...

Row-based waterfall view

Source code introduction The demo shows a row-bas...

Such a marketing landing page is a bit of a waste of promotion costs!

Students who have listened to my speech must be i...

Virtual Reality to Reach $30 Billion by 2020

According to a recent report from Digi-Capital, t...

Even Durex can't save you! How to create copywriting that satisfies customers!

It is difficult for us to find a way to satisfy an...