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 |