Android 原生控件打造经典贪吃蛇游戏实战指南

移动开发 Android
贪吃蛇是一款经典的游戏,以其简单易上手、策略性强、挑战性高等特点深受玩家喜爱。下面我们使用Android原生控件来实现这个小游戏。

游戏说明

贪吃蛇是一款经典的游戏,以其简单易上手、策略性强、挑战性高等特点深受玩家喜爱。

游戏玩法:

  • 玩家使用方向键操控一条长长的蛇不断吞下豆子,蛇身随着吞下的豆子不断变长
  • 游戏的目标是尽可能长时间地生存下去,同时避免蛇头撞到自己的身体或屏幕边缘

游戏特点:

  • 简单易上手:游戏操作简单,玩家只需要控制蛇的移动和转向,吃掉食物即可
  • 策略性:虽然游戏看似简单,但需要玩家灵活运用策略,在有限的空间内避免碰撞
  • 挑战性:游戏难度逐渐增加,随着蛇身的增长,玩家需要更加谨慎地操作

下面我们使用Android原生控件来实现这个小游戏(PS:不包含自定义View的方式)。

实现思路

1.游戏场景

使用GridLayout作为游戏板,大小为20x20,同时包含游戏分数和控制按钮,下面是布局文件:

<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>

预览效果

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)
            }
        }
    }
}

初始化游戏板,大小为20*20,使用TextView作为每个单元格,用于表示可移动的范围网格。初始化蛇的位置在游戏板中央,蛇被表示为MutableList<Pair<Int, Int>>,每个Pair代表蛇身体的一个部分的坐标。同时随机在范围中生成食物,最后更新游戏板给蛇和食物生成不同的颜色样式。

2.游戏主循环

此时的游戏是不会动的,需要一个游戏主循环让游戏不断更新才能使游戏画面动起来,使用Handler定期调用游戏更新逻辑,每200毫秒更新一次游戏状态。

private val updateDelay = 200L // 游戏更新间隔,毫秒

private fun startGameLoop() {
    handler.postDelayed(object : Runnable {
        override fun run() {
            moveSnake()
            checkCollision()
            updateBoard()
            handler.postDelayed(this, updateDelay)
        }
    }, updateDelay)
}

每发送一次事件对蛇进行移动,检查游戏是否结束(蛇是否咬到自己),更新GridLayout网格显示,发送下一次更新事件

3.蛇的移动

蛇移动的核心逻辑,计算新的蛇头位置,使用模运算确保蛇能够穿过游戏边界,检查是否吃到食物,如果是,增加分数并生成新食物;否则,移除蛇尾。

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) 获取蛇头位置:

val head = snake.first()

蛇被表示为一个坐标对(Pair)的列表,第一个元素是蛇头。

(2) 计算新的蛇头位置:

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

direction(控制的方向)来移动蛇头,加上 boardSize 并对 boardSize 取模,确保新位置总是在游戏板内

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

(3) 将新的蛇头添加到蛇身列表的开头:

snake.add(0, newHead)

蛇一直是在移动的,蛇头坐标一直在变化。

(4) 检查是否吃到食物:

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

如果新的蛇头位置与食物位置相同,增加分数,更新分数显示,并生成新的食物。如果没有吃到食物,则移除蛇尾,保持蛇的长度不变。

4.碰撞检测

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

检查蛇头是否与蛇身相撞,如果是,游戏结束。

5.生成食物

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

随机生成新的食物位置,确保不与蛇身重叠

6.显示更新

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)
            }
        }
    }
}

遍历游戏板的每个单元格,根据其状态(蛇头、蛇身、食物或空白)设置不同的颜色。

游戏效果

7.游戏开始结束

此时的蛇可以掉头和在游戏场景里穿越,下面我们改进一下,蛇撞到游戏边界游戏结束

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)
    }
}

添加边界检测,检测到坐标在游戏板边界,游戏结束

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()
    }
}

修改方向按钮的点击监听器,使其能够重新开始游戏

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"
}

游戏结束和重新开始,通过isGameRunning变量控制游戏主循环

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

完整代码

游戏效果

Github源码https://github.com/Reathin/Sample-Android/tree/master/module_snake

责任编辑:赵宁宁 来源: 沐雨花飞蝶
相关推荐

2021-06-15 09:18:51

鸿蒙HarmonyOS应用

2012-06-05 14:42:57

Silverlight

2022-10-28 09:33:10

Linux贪吃蛇

2015-07-31 11:26:24

Swift贪吃蛇

2022-11-07 11:27:00

JS游戏开发

2020-08-20 20:30:49

C语言小游戏贪吃蛇

2021-09-02 15:25:53

鸿蒙HarmonyOS应用

2024-01-18 11:22:41

C++Windows开发

2022-07-25 14:17:04

JS应用开发

2023-10-17 10:20:53

VueReact

2021-04-20 11:40:12

Linux图形库curses

2024-12-06 09:20:22

Android游戏新数字

2024-12-09 09:18:21

Android原生控件

2016-09-14 21:17:47

PythonAsyncio游戏

2016-09-19 21:24:08

PythonAsyncio游戏

2016-09-22 21:12:14

2010-02-05 15:00:44

Android 调用u

2018-08-31 15:48:33

2015-07-07 15:47:57

Razer雷蛇

2021-05-27 16:53:09

开发技能代码
点赞
收藏

51CTO技术栈公众号