游戏说明
贪吃蛇是一款经典的游戏,以其简单易上手、策略性强、挑战性高等特点深受玩家喜爱。
游戏玩法:
- 玩家使用方向键操控一条长长的蛇不断吞下豆子,蛇身随着吞下的豆子不断变长
- 游戏的目标是尽可能长时间地生存下去,同时避免蛇头撞到自己的身体或屏幕边缘
游戏特点:
- 简单易上手:游戏操作简单,玩家只需要控制蛇的移动和转向,吃掉食物即可
- 策略性:虽然游戏看似简单,但需要玩家灵活运用策略,在有限的空间内避免碰撞
- 挑战性:游戏难度逐渐增加,随着蛇身的增长,玩家需要更加谨慎地操作
下面我们使用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