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 管理分数、棋盘、选中格子、处理状态等,实现响应式更新。
效果图


浙公网安备 33010602011771号