Android,jetpack,compose,简单模仿水果消消乐 - 详解

(这只是简单案例模仿 做不到真正游戏那样)

这是MainActivity代码

package com.example.myapplicationFruit
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContent {
 StartScreen()
 }
 }
 @Composable
 fun StartScreen() {
 Scaffold(
 containerColor = Color(0xFF1E3C2B)
 ) { padding ->
 Box(
 modifier = Modifier
 .fillMaxSize()
 .padding(padding),
 contentAlignment = Alignment.Center
 ) {
 Column(
 horizontalAlignment = Alignment.CenterHorizontally,
 verticalArrangement = Arrangement.Center,
 modifier = Modifier.padding(16.dp)
 ) {
 // 标题
 Text(
 text = "水果消消乐",
 fontSize = 42.sp,
 fontWeight = FontWeight.Bold,
 color = Color(0xFFFFE599),
 textAlign = TextAlign.Center,
 modifier = Modifier.padding(bottom = 32.dp)
 )
 // 水果图标装饰
 Row {
 val fruitDrawables = listOf(
 R.drawable.apple,
 R.drawable.banana,
 R.drawable.orange,
 R.drawable.grape,
 R.drawable.strawberry,
 R.drawable.watermelon
 )
 for (resId in fruitDrawables) {
 Image(
 painter = painterResource(id = resId),
 contentDescription = null,
 modifier = Modifier.size(40.dp)
 )
 }
 }
 Spacer(modifier = Modifier.height(48.dp))
 // 开始按钮
 Button(
 onClick = {
 startActivity(Intent(this@MainActivity, FruitMatchGameActivity::class.java))
 },
 shape = RoundedCornerShape(12.dp),
 colors = ButtonDefaults.buttonColors(
 containerColor = Color(0xFF4CAF50),
 contentColor = Color.White
 ),
 modifier = Modifier
 .shadow(4.dp)
 .width(200.dp)
 ) {
 Text(text = "开始游戏", fontSize = 20.sp)
 }
 }
 }
 }
 }
 @Preview(showBackground = true, backgroundColor = 0xFF1E3C2B, showSystemUi = false)
 @Composable
 fun PreviewStartScreen() {
 StartScreenPreviewWrapper()
 }
}
@Composable
private fun StartScreenPreviewWrapper() {
 MainActivity().StartScreen()
}

这是FruitMatchGameActivitu代码

package com.example.myapplicationFruit
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.random.Random
import androidx.compose.material3.ExperimentalMaterial3Api
class FruitMatchGameActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContent {
 GameScreen(
 onExit = { finish() } // 传入 finish() 回调
 )
 }
 }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GameScreen(
 onExit: () -> Unit // 接收退出回调
) {
 var score by remember { mutableStateOf(0) }
 var board by remember { mutableStateOf(emptyList>()) }
 var isProcessing by remember { mutableStateOf(false) }
 var selectedCell by remember { mutableStateOf?>(null) }
 var showGameOver by remember { mutableStateOf(false) } // 控制游戏结束弹窗显示
 var showDeadlockDialog by remember { mutableStateOf(false) } // 控制死局检测弹窗显示
 var deadlockMessage by remember { mutableStateOf("") } // 存储死局检测消息
 val coroutineScope = rememberCoroutineScope()
 LaunchedEffect(Unit) {
 isProcessing = true
 val stableBoard = generateStableBoard()
 board = stableBoard
 isProcessing = false
 }
 // 检查是否进入死局(无合法移动)
 LaunchedEffect(board, score) {
 if (board.isNotEmpty() && !isProcessing && !hasAnyValidMove(board)) {
 // 在死局时显示游戏结束弹窗
 showGameOver = true
 }
 }
 MaterialTheme(
 colorScheme = lightColorScheme(
 primary = Color(0xFF2D5F3A),
 onPrimary = Color.White,
 secondary = Color(0xFF8BC34A),
 onSecondary = Color.Black,
 background = Color(0xFF2D5F3A),
 onBackground = Color.White,
 surface = Color(0xFF2D5F3A),
 onSurface = Color.White,
 error = Color.Red,
 onError = Color.White,
 tertiary = Color.Yellow,
 onTertiary = Color.Black
 )
 ) {
 Scaffold(
 topBar = {
 TopAppBar(
 title = {
 Text(
 text = "分数: $score",
 fontSize = 18.sp,
 fontWeight = FontWeight.Bold,
 color = MaterialTheme.colorScheme.onPrimary
 )
 },
 actions = {
 // 检测死局按钮
 IconButton(onClick = {
 if (!isProcessing && board.isNotEmpty()) {
 val isDeadlock = !hasAnyValidMove(board)
 deadlockMessage = if (isDeadlock) {
 "当前棋盘已无任何合法移动,进入死局!"
 } else {
 "当前棋盘仍存在合法移动,未进入死局。"
 }
 showDeadlockDialog = true
 }
 }) {
 Icon(
 imageVector = androidx.compose.material.icons.Icons.Default.Info,
 contentDescription = "检查死局",
 tint = MaterialTheme.colorScheme.onPrimary
 )
 }
 // 重置按钮
 IconButton(onClick = {
 if (!isProcessing) {
 isProcessing = true
 coroutineScope.launch {
 val newBoard = generateStableBoard()
 board = newBoard
 score = 0
 selectedCell = null
 isProcessing = false
 }
 }
 }) {
 Icon(
 Icons.Default.Refresh,
 contentDescription = "重置",
 tint = MaterialTheme.colorScheme.onPrimary
 )
 }
 },
 colors = TopAppBarDefaults.topAppBarColors(
 containerColor = MaterialTheme.colorScheme.primary
 )
 )
 },
 containerColor = MaterialTheme.colorScheme.surface
 ) { padding ->
 Column(
 modifier = Modifier
 .padding(padding)
 .fillMaxSize(),
 horizontalAlignment = Alignment.CenterHorizontally
 ) {
 Spacer(modifier = Modifier.height(8.dp))
 if (board.isNotEmpty()) {
 GameBoard(
 board = board,
 selectedCell = selectedCell,
 onCellClick = { row, col ->
 if (isProcessing) return@GameBoard
 coroutineScope.launch {
 handleCellClick(
 row, col, board, selectedCell, score
 ) { newBoard, newScore, newSelectedCell ->
 board = newBoard
 score = newScore
 selectedCell = newSelectedCell
 }
 }
 }
 )
 }
 }
 // 游戏结束弹窗
 if (showGameOver) {
 GameOverDialog(
 finalScore = score,
 onRestart = {
 showGameOver = false
 coroutineScope.launch {
 isProcessing = true
 val newBoard = generateStableBoard()
 board = newBoard
 score = 0
 selectedCell = null
 isProcessing = false
 }
 },
 onExit = {
 showGameOver = false
 onExit() // 调用传入的回调
 }
 )
 }
 // 死局检测结果弹窗
 if (showDeadlockDialog) {
 AlertDialog(
 onDismissRequest = { showDeadlockDialog = false },
 title = {
 Text(
 text = if (deadlockMessage.contains("死局")) "死局检测" else "非死局",
 color = MaterialTheme.colorScheme.onSurface
 )
 },
 text = {
 Text(text = deadlockMessage)
 },
 confirmButton = {
 Button(onClick = { showDeadlockDialog = false }) {
 Text("确定")
 }
 }
 )
 }
 }
 }
}
@Composable
fun GameOverDialog(
 finalScore: Int,
 onRestart: () -> Unit,
 onExit: () -> Unit
) {
 AlertDialog(
 onDismissRequest = { /* 不允许点击外部关闭 */ },
 title = {
 Text(text = "游戏结束!", color = MaterialTheme.colorScheme.onSurface)
 },
 text = {
 Text(text = "你的最终分数是:$finalScore\n\n棋盘上已无可能通过交换形成三连。")
 },
 confirmButton = {
 Button(onClick = onRestart) {
 Text("重新开始")
 }
 },
 dismissButton = {
 Button(onClick = onExit) {
 Text("退出游戏")
 }
 }
 )
}
val fruits = listOf(
 R.drawable.apple,
 R.drawable.banana,
 R.drawable.orange,
 R.drawable.grape,
 R.drawable.strawberry,
 R.drawable.watermelon
)
fun generateStableBoard(size: Int = 8): List> {
 var board = generateRandomBoard(size)
 while (hasAnyMatch(board)) {
 board = generateRandomBoard(size)
 }
 return board
}
fun generateRandomBoard(size: Int): List> {
 return List(size) { List(size) { Random.nextInt(fruits.size) } }
}
fun hasAnyMatch(board: List>): Boolean {
 if (board.isEmpty() || board[0].isEmpty()) return false
 for (i in board.indices) {
 for (j in board[0].indices) {
 if (hasMatch(board, i, j)) {
 return true
 }
 }
 }
 return false
}
@Composable
fun GameBoard(
 board: List>,
 selectedCell: Pair?,
 onCellClick: (Int, Int) -> Unit
) {
 val cellSize = 70.dp
 val spacing = 4.dp
 Column(
 modifier = Modifier.padding(8.dp),
 verticalArrangement = Arrangement.spacedBy(spacing)
 ) {
 for ((rowIndex, row) in board.withIndex()) {
 Row(
 horizontalArrangement = Arrangement.spacedBy(spacing),
 modifier = Modifier.fillMaxWidth().height(cellSize)
 ) {
 for ((colIndex, _) in row.withIndex()) {
 val isSelected = selectedCell?.first == rowIndex && selectedCell?.second == colIndex
 val borderColor = if (isSelected) MaterialTheme.colorScheme.tertiary else Color.Transparent
 val borderWidth = if (isSelected) 3.dp else 0.dp
 Box(
 modifier = Modifier
 .size(cellSize)
 .clip(RoundedCornerShape(8.dp))
 .background(MaterialTheme.colorScheme.secondary)
 .border(borderWidth, borderColor, RoundedCornerShape(8.dp))
 .clickable { onCellClick(rowIndex, colIndex) },
 contentAlignment = Alignment.Center
 ) {
 Image(
 painter = painterResource(id = fruits[board[rowIndex][colIndex]]),
 contentDescription = "Fruit",
 contentScale = ContentScale.Fit,
 modifier = Modifier.fillMaxSize().padding(4.dp)
 )
 }
 }
 }
 }
 }
}
fun isAdjacent(pos1: Pair, pos2: Pair): Boolean {
 return (pos1.first == pos2.first && abs(pos1.second - pos2.second) == 1) ||
 (pos1.second == pos2.second && abs(pos1.first - pos2.first) == 1)
}
fun hasMatch(board: List>, row: Int, col: Int): Boolean {
 if (board.isEmpty() || board[0].isEmpty()) return false
 if (row !in board.indices || col !in board[0].indices) return false
 val target = board[row][col]
 var count = 1
 for (c in col + 1 until board[0].size) if (board[row][c] == target) count++ else break
 for (c in col - 1 downTo 0) if (board[row][c] == target) count++ else break
 if (count >= 3) return true
 count = 1
 for (r in row + 1 until board.size) if (board[r][col] == target) count++ else break
 for (r in row - 1 downTo 0) if (board[r][col] == target) count++ else break
 return count >= 3
}
// 检查任意一次交换是否能形成三连
fun hasAnyValidMove(board: List>): Boolean {
 if (board.isEmpty() || board[0].isEmpty()) return false
 val size = board.size
 for (i in 0 until size) {
 for (j in 0 until size) {
 // 尝试与右边交换
 if (j < size - 1) {
 if (wouldCreateMatch(board, i, j, i, j + 1)) return true
 }
 // 尝试与下边交换
 if (i < size - 1) {
 if (wouldCreateMatch(board, i, j, i + 1, j)) return true
 }
 }
 }
 return false
}
// 模拟交换后是否会形成三连
fun wouldCreateMatch(
 board: List>,
 r1: Int, c1: Int,
 r2: Int, c2: Int
): Boolean {
 val newBoard = board.map { it.toMutableList() }.toMutableList()
 val temp = newBoard[r1][c1]
 newBoard[r1][c1] = newBoard[r2][c2]
 newBoard[r2][c2] = temp
 return hasMatch(newBoard, r1, c1) || hasMatch(newBoard, r2, c2)
}
fun eliminateMatches(board: List>): Pair>, Int> {
 if (board.isEmpty() || board[0].isEmpty()) return board to 0
 val mark = MutableList(board.size) { BooleanArray(board[0].size) }
 var eliminatedCount = 0
 for (i in board.indices) {
 for (j in board[0].indices) {
 if (hasMatch(board, i, j)) {
 mark[i][j] = true
 eliminatedCount++
 }
 }
 }
 if (eliminatedCount == 0) return board to 0
 val newBoard = List(board.size) { MutableList(board[0].size) { 0 } }
 for (j in board[0].indices) {
 var writeRow = board.size - 1
 for (i in board.size - 1 downTo 0) {
 if (!mark[i][j]) {
 newBoard[writeRow][j] = board[i][j]
 writeRow--
 }
 }
 while (writeRow >= 0) {
 newBoard[writeRow][j] = Random.nextInt(fruits.size)
 writeRow--
 }
 }
 return newBoard to eliminatedCount
}
suspend fun processChainElimination(
 initialBoard: List>,
 onUpdate: (List>, Int) -> Unit
) {
 var currentBoard = initialBoard
 var totalScore = 0
 while (true) {
 val (newBoard, count) = eliminateMatches(currentBoard)
 if (count == 0) break
 totalScore += count * 10
 currentBoard = newBoard
 // 可加 delay(200) 实现动画节奏感
 }
 onUpdate(currentBoard, totalScore)
}
suspend fun handleCellClick(
 row: Int,
 col: Int,
 currentBoard: List>,
 selectedCell: Pair?,
 currentScore: Int,
 onResult: (newBoard: List>, newScore: Int, newSelectedCell: Pair?) -> Unit
) {
 val clickPos = row to col
 if (selectedCell == null) {
 onResult(currentBoard, currentScore, clickPos)
 return
 }
 if (clickPos == selectedCell) {
 onResult(currentBoard, currentScore, null)
 return
 }
 if (!isAdjacent(selectedCell, clickPos)) {
 // 点击非相邻格子时,取消选中状态并选中新格子
 onResult(currentBoard, currentScore, clickPos)
 return
 }
 val boardAfterSwap = currentBoard.map { it.toMutableList() }.toMutableList()
 val temp = boardAfterSwap[selectedCell.first][selectedCell.second]
 boardAfterSwap[selectedCell.first][selectedCell.second] = boardAfterSwap[clickPos.first][clickPos.second]
 boardAfterSwap[clickPos.first][clickPos.second] = temp
 if (hasMatch(boardAfterSwap, selectedCell.first, selectedCell.second) || hasMatch(boardAfterSwap, clickPos.first, clickPos.second)) {
 processChainElimination(boardAfterSwap) { finalBoard, chainScore ->
 onResult(finalBoard, currentScore + chainScore, null)
 }
 } else {
 // 交换无效时,取消选中状态
 onResult(currentBoard, currentScore, null)
 }
}

代码逻辑(逻辑主要是FruitMatchGameActivity)

核心逻辑概要

1. 游戏界面(GameScreen) - 使用 Scaffold 构建页面结构,顶部是分数栏和按钮(重置、检测死局)。 - 主体显示游戏棋盘 GameBoard,每个格子代表一个水果。

2. 棋盘初始化 - 调用 generateStableBoard() 生成一个 没有初始三连匹配 的随机棋盘,避免一上来就有可消除项。

3. 用户交互 - 点击格子:选中一个水果。 - 再点击相邻格子:尝试交换。 - 只有交换后能形成三连,才执行交换并触发消除;否则还原。

4. 消除机制 - 每次有效交换后,检查是否有新的三连出现(hasMatch)。 - 消除后空位下落填充,上方随机生成新水果(eliminateMatches)。 - 连锁消除(连消)会持续处理直到无新匹配产生(processChainElimination)。

5. 死局检测 - 使用 hasAnyValidMove() 遍历所有相邻交换可能,判断是否还有合法走法。 - 若无合法移动,弹出“游戏结束”对话框(GameOverDialog)。

6. 游戏结束条件 - 当棋盘处于 死局(无任何可行交换) 时自动触发游戏结束。 - 提供“重新开始”和“退出”选项。

7. UI 弹窗功能 - 游戏结束弹窗:显示最终分数,可重开或退出。 - 死局检测弹窗:手动点击“i”按钮可查看当前是否为死局。

8. 状态管理 - 使用 mutableStateOf 管理分数、棋盘、选中格子、处理状态等,实现响应式更新。

效果图在这里插入图片描述

在这里插入图片描述

posted @ 2025-10-23 14:20  wzzkaifa  阅读(4)  评论(0)    收藏  举报