Android 原生控件打造經(jīng)典貪吃蛇游戲?qū)崙?zhàn)指南
游戲說明
貪吃蛇是一款經(jīng)典的游戲,以其簡單易上手、策略性強(qiáng)、挑戰(zhàn)性高等特點(diǎn)深受玩家喜愛。
游戲玩法:
- 玩家使用方向鍵操控一條長長的蛇不斷吞下豆子,蛇身隨著吞下的豆子不斷變長
- 游戲的目標(biāo)是盡可能長時間地生存下去,同時避免蛇頭撞到自己的身體或屏幕邊緣
游戲特點(diǎn):
- 簡單易上手:游戲操作簡單,玩家只需要控制蛇的移動和轉(zhuǎn)向,吃掉食物即可
- 策略性:雖然游戲看似簡單,但需要玩家靈活運(yùn)用策略,在有限的空間內(nèi)避免碰撞
- 挑戰(zhàn)性:游戲難度逐漸增加,隨著蛇身的增長,玩家需要更加謹(jǐn)慎地操作
下面我們使用Android原生控件來實(shí)現(xiàn)這個小游戲(PS:不包含自定義View的方式)。
實(shí)現(xiàn)思路
1.游戲場景
使用GridLayout作為游戲板,大小為20x20,同時包含游戲分?jǐn)?shù)和控制按鈕,下面是布局文件:
<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="分?jǐn)?shù): 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>
預(yù)覽效果
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作為每個單元格,用于表示可移動的范圍網(wǎng)格。初始化蛇的位置在游戲板中央,蛇被表示為MutableList<Pair<Int, Int>>,每個Pair代表蛇身體的一個部分的坐標(biāo)。同時隨機(jī)在范圍中生成食物,最后更新游戲板給蛇和食物生成不同的顏色樣式。
2.游戲主循環(huán)
此時的游戲是不會動的,需要一個游戲主循環(huán)讓游戲不斷更新才能使游戲畫面動起來,使用Handler定期調(diào)用游戲更新邏輯,每200毫秒更新一次游戲狀態(tài)。
private val updateDelay = 200L // 游戲更新間隔,毫秒
private fun startGameLoop() {
handler.postDelayed(object : Runnable {
override fun run() {
moveSnake()
checkCollision()
updateBoard()
handler.postDelayed(this, updateDelay)
}
}, updateDelay)
}
每發(fā)送一次事件對蛇進(jìn)行移動,檢查游戲是否結(jié)束(蛇是否咬到自己),更新GridLayout網(wǎng)格顯示,發(fā)送下一次更新事件
3.蛇的移動
蛇移動的核心邏輯,計算新的蛇頭位置,使用模運(yùn)算確保蛇能夠穿過游戲邊界,檢查是否吃到食物,如果是,增加分?jǐn)?shù)并生成新食物;否則,移除蛇尾。
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 = "分?jǐn)?shù): $score"
generateFood()
} else {
snake.removeAt(snake.size - 1)
}
}
(1) 獲取蛇頭位置:
val head = snake.first()
蛇被表示為一個坐標(biāo)對(Pair)的列表,第一個元素是蛇頭。
(2) 計算新的蛇頭位置:
val newHead = Pair(
(head.first + direction.first + boardSize) % boardSize,
(head.second + direction.second + boardSize) % boardSize
)
direction(控制的方向)來移動蛇頭,加上 boardSize 并對 boardSize 取模,確保新位置總是在游戲板內(nèi)
direction = Pair(-1, 0) //上
direction = Pair(1, 0) //下
direction = Pair(0, -1) //左
direction = Pair(0, 1) //右
(3) 將新的蛇頭添加到蛇身列表的開頭:
snake.add(0, newHead)
蛇一直是在移動的,蛇頭坐標(biāo)一直在變化。
(4) 檢查是否吃到食物:
if (newHead == food) {
score++
scoreTextView.text = "Score: $score"
generateFood()
} else {
snake.removeAt(snake.size - 1)
}
如果新的蛇頭位置與食物位置相同,增加分?jǐn)?shù),更新分?jǐn)?shù)顯示,并生成新的食物。如果沒有吃到食物,則移除蛇尾,保持蛇的長度不變。
4.碰撞檢測
private fun checkCollision() {
val head = snake.first()
if (snake.subList(1, snake.size).contains(head)) {
// 游戲結(jié)束
handler.removeCallbacksAndMessages(null)
}
}
檢查蛇頭是否與蛇身相撞,如果是,游戲結(jié)束。
5.生成食物
private fun generateFood() {
do {
food = Pair(Random.nextInt(boardSize), Random.nextInt(boardSize))
} while (snake.contains(food))
}
隨機(jī)生成新的食物位置,確保不與蛇身重疊
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)
}
}
}
}
遍歷游戲板的每個單元格,根據(jù)其狀態(tài)(蛇頭、蛇身、食物或空白)設(shè)置不同的顏色。
游戲效果
7.游戲開始結(jié)束
此時的蛇可以掉頭和在游戲場景里穿越,下面我們改進(jìn)一下,蛇撞到游戲邊界游戲結(jié)束
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 = "分?jǐn)?shù): $score"
generateFood()
} else {
snake.removeAt(snake.size - 1)
}
}
添加邊界檢測,檢測到坐標(biāo)在游戲板邊界,游戲結(jié)束
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()
}
}
修改方向按鈕的點(diǎn)擊監(jiān)聽器,使其能夠重新開始游戲
private fun endGame() {
isGameRunning = false
handler.removeCallbacksAndMessages(null)
scoreTextView.text = "游戲結(jié)束!最終分?jǐn)?shù): $score\n點(diǎ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 = "分?jǐn)?shù): 0"
}
游戲結(jié)束和重新開始,通過isGameRunning變量控制游戲主循環(huán)
private fun startGameLoop() {
handler.post(object : Runnable {
override fun run() {
if (isGameRunning) {
moveSnake()
if (isGameRunning) { // 再次檢查,因?yàn)?moveSnake 可能會結(jié)束游戲
checkCollision()
updateBoard()
handler.postDelayed(this, updateDelay)
}
}
}
})
}
完整代碼
游戲效果
Github源碼https://github.com/Reathin/Sample-Android/tree/master/module_snake