ANDROID,Jetpack Compose, 贪吃蛇小游戏Demo
SnakeGameMainActivity代码如下:
package com.example.myapplicationimport android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.*
import kotlin.math.abs// 数据类定义游戏中的点
data class Point(val x: Int, val y: Int)// 方向枚举
enum class Direction {Up, Right, Down, Left
}// 获取相反方向的辅助函数
fun getOppositeDirection(dir: Direction): Direction = when (dir) {Direction.Up -> Direction.DownDirection.Down -> Direction.UpDirection.Left -> Direction.RightDirection.Right -> Direction.Left
}// 自定义主题,使用深色模式
@Composable
fun SnakeGameTheme(content: @Composable () -> Unit) {MaterialTheme(colorScheme = darkColorScheme(),content = content)
}class SnakeGameMainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {SnakeGameTheme {SnakeGameApp()}}}
}// 新增一个包装 Composable 来管理游戏状态和开始界面
@Composable
fun SnakeGameApp() {var gameStarted by remember { mutableStateOf(false) }var useButtonControl by remember { mutableStateOf(false) } // 在开始前设置控制模式var initialSpeed by remember { mutableStateOf(200L) } // 在开始前设置初始速度var finalScore by remember { mutableStateOf(0) } // 用于存储最终得分if (!gameStarted) {// 游戏开始前的设置界面或游戏结束后的成绩界面if (finalScore >= 0) {// 显示最终成绩GameOverScreen(score = finalScore,onRestart = {finalScore = -1 // 重置分数状态,回到开始界面gameStarted = false})} else {// 显示开始界面GameStartScreen(useButtonControl = useButtonControl,onControlModeChange = { useButtonControl = it },initialSpeed = initialSpeed,onSpeedChange = { initialSpeed = it },onStartGame = { gameStarted = true })}} else {// 游戏主界面SnakeGame(useButtonControl = useButtonControl,initialSpeed = initialSpeed,onGameEnd = { score -> // 接收游戏结束时的分数finalScore = scoregameStarted = false // 触发显示 GameOverScreen})}
}@Composable
fun GameStartScreen(useButtonControl: Boolean,onControlModeChange: (Boolean) -> Unit,initialSpeed: Long,onSpeedChange: (Long) -> Unit,onStartGame: () -> Unit
) {Box(modifier = Modifier.fillMaxSize().background(Color.Black).padding(16.dp),contentAlignment = Alignment.Center) {Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.spacedBy(24.dp)) {Text(text = "贪吃蛇游戏",color = Color.White,fontSize = 32.sp)// 控制模式切换Row(verticalAlignment = Alignment.CenterVertically,horizontalArrangement = Arrangement.spacedBy(16.dp)) {Text("滑动控制", color = Color.White)Switch(checked = useButtonControl,onCheckedChange = onControlModeChange,colors = SwitchDefaults.colors(checkedThumbColor = Color(0xFF4CAF50),checkedTrackColor = Color(0xFF81C784),uncheckedThumbColor = Color.LightGray,uncheckedTrackColor = Color.Gray))Text("按钮控制", color = Color.White)}// --- 速度控制 ---Column(modifier = Modifier.fillMaxWidth()) {// 速度值显示Text(text = "初始速度: ${String.format("%.1f", (500 - initialSpeed) / 100.0)}",color = Color.Yellow,fontSize = 16.sp,modifier = Modifier.align(Alignment.Start))// 滚动条Slider(value = (500 - initialSpeed).toFloat(),onValueChange = { newValue ->onSpeedChange((500 - newValue).toLong().coerceIn(50L, 500L))},valueRange = 0f..450f,steps = 8, // 9个档位modifier = Modifier.fillMaxWidth(),colors = SliderDefaults.colors(thumbColor = Color(0xFF4CAF50),activeTrackColor = Color(0xFF81C784),inactiveTrackColor = Color.Gray))}// --- 速度控制结束 ---Button(onClick = onStartGame,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4CAF50)),modifier = Modifier.fillMaxWidth(0.6f) // 按钮宽度为屏幕的60%) {Text("开始游戏", color = Color.White, fontSize = 20.sp)}}}
}// 新增:游戏结束后的成绩显示界面
@Composable
fun GameOverScreen(score: Int, onRestart: () -> Unit) {Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.8f)) // 半透明黑色背景.padding(16.dp),contentAlignment = Alignment.Center) {Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center) {Text(text = "游戏结束",color = Color.White,fontSize = 32.sp,modifier = Modifier.padding(bottom = 16.dp))Text(text = "最终得分: $score",color = Color.Yellow,fontSize = 24.sp,modifier = Modifier.padding(bottom = 32.dp))Button(onClick = onRestart,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4CAF50))) {Text("返回主菜单", color = Color.White, fontSize = 18.sp)}}}
}@Composable
fun SnakeGame(useButtonControl: Boolean, initialSpeed: Long, onGameEnd: (Int) -> Unit) {// 游戏状态管理var gameRunning by remember { mutableStateOf(true) }var gamePaused by remember { mutableStateOf(false) }var snake by remember { mutableStateOf(listOf(Point(10, 10))) }var direction by remember { mutableStateOf(Direction.Right) }var nextDirection by remember { mutableStateOf(Direction.Right) }var food by remember { mutableStateOf(Point(5, 5)) }var score by remember { mutableStateOf(0) }// 新增功能状态var speed by remember { mutableStateOf(initialSpeed) } // 使用传入的初始速度var isDying by remember { mutableStateOf(false) } // 是否处于濒死状态var dyingJob by remember { mutableStateOf<Job?>(null) } // 濒死状态的计时器val gridSize = 20f // 游戏网格大小val coroutineScope = rememberCoroutineScope()val density = LocalDensity.currentval config = LocalConfiguration.current// 计算游戏区域大小,适配屏幕val screenWidth = config.screenWidthDp.dpval screenHeight = config.screenHeightDp.dp// 尝试为游戏区域分配更多空间,但不超过屏幕的一定比例val gameAreaSize = (screenWidth * 0.8f).coerceAtMost(screenHeight * 0.6f).coerceAtMost(400.dp)// 计算每个格子的像素大小val blockSize by remember(gridSize, gameAreaSize) {derivedStateOf {with(density) { gameAreaSize.toPx() / gridSize }}}// 生成食物,确保不在蛇身上fun placeFood() {while (true) {val newFood = Point(x = (0 until gridSize.toInt()).random(),y = (0 until gridSize.toInt()).random())if (newFood !in snake) {food = newFoodreturn}}}// 移动蛇的逻辑fun moveSnake() {if (!gameRunning || gamePaused) returndirection = nextDirectionval head = snake.first()val newHead = when (direction) {Direction.Up -> Point(head.x, head.y - 1)Direction.Right -> Point(head.x + 1, head.y)Direction.Down -> Point(head.x, head.y + 1)Direction.Left -> Point(head.x - 1, head.y)}// 检查撞墙val isWallCollision = newHead.x < 0 || newHead.x >= gridSize.toInt() ||newHead.y < 0 || newHead.y >= gridSize.toInt()if (isWallCollision) {if (!isDying) {isDying = truedyingJob = coroutineScope.launch {delay(2000) // 2秒后确认死亡if (isDying) {gameRunning = falseonGameEnd(score) // 游戏结束,传递分数}}}return} else {if (isDying) {isDying = falsedyingJob?.cancel()dyingJob = null}}// 检查撞到自己if (newHead in snake) {gameRunning = falseonGameEnd(score) // 游戏结束,传递分数return}// 更新蛇身val newSnake = mutableListOf(newHead)newSnake.addAll(snake)// 检查是否吃到食物if (newHead == food) {score++placeFood()} else {newSnake.removeAt(newSnake.lastIndex)}snake = newSnake}// 游戏主循环suspend fun startGameLoop() {while (gameRunning) {delay(speed)moveSnake()}}// 重新开始游戏fun restartGame() {dyingJob?.cancel()dyingJob = nullisDying = falsegameRunning = truegamePaused = falsesnake = listOf(Point((gridSize / 2).toInt(), (gridSize / 2).toInt()))direction = Direction.RightnextDirection = Direction.Rightscore = 0placeFood()}// 切换暂停/继续fun togglePause() {if (gameRunning && !isDying) {gamePaused = !gamePaused}}// 结束游戏 - 修改为调用 onGameEnd 并传递当前分数fun endGame() {if (gameRunning) {gameRunning = falseisDying = falsedyingJob?.cancel()dyingJob = nullonGameEnd(score) // 主动结束游戏时,也传递分数}}// 启动游戏循环LaunchedEffect(gameRunning, gamePaused) {if (gameRunning && !gamePaused) {startGameLoop()}}// 初始放置食物LaunchedEffect(Unit) {placeFood()}// 主UI布局 - 使用 Column 并设置最大高度为屏幕高度Column(modifier = Modifier.fillMaxSize().background(Color.Black).padding(16.dp),horizontalAlignment = Alignment.CenterHorizontally) {// 游戏画布 (本身不滚动)Box(modifier = Modifier.size(gameAreaSize).background(Color(0xFF1E1E1E)).pointerInput(useButtonControl) {if (useButtonControl) return@pointerInputawaitPointerEventScope {while (true) {val down = awaitFirstDown()val start = down.positionval upEvent = waitForUpOrCancellation()val end = upEvent?.position ?: startval dx = end.x - start.xval dy = end.y - start.yval newDirection = when {abs(dx) > abs(dy) -> {if (dx > 50f) Direction.Right else if (dx < -50f) Direction.Left else null}else -> {if (dy > 50f) Direction.Down else if (dy < -50f) Direction.Up else null}}newDirection?.let {if (it != getOppositeDirection(direction)) {nextDirection = it}}}}},contentAlignment = Alignment.Center) {Canvas(modifier = Modifier.fillMaxSize()) {// 绘制网格线for (i in 0..gridSize.toInt()) {val pos = i * blockSizedrawLine(color = Color.Gray.copy(alpha = 0.2f),start = Offset(pos, 0f),end = Offset(pos, size.height),strokeWidth = 1.dp.toPx())drawLine(color = Color.Gray.copy(alpha = 0.2f),start = Offset(0f, pos),end = Offset(size.width, pos),strokeWidth = 1.dp.toPx())}// 绘制蛇身snake.forEachIndexed { index, point ->val color = if (index == 0) Color(0xFF00FF00) else Color(0xFF00CC00)drawRect(color = color,topLeft = Offset(point.x * blockSize, point.y * blockSize),size = androidx.compose.ui.geometry.Size(blockSize, blockSize))}// 绘制食物drawCircle(color = Color.Red,radius = blockSize * 0.4f,center = Offset(x = food.x * blockSize + blockSize / 2,y = food.y * blockSize + blockSize / 2))// 绘制暂停遮罩if (gamePaused && gameRunning) {drawRect(color = Color.Gray.copy(alpha = 0.5f),topLeft = Offset.Zero,size = size)}// 绘制濒死状态遮罩if (isDying) {drawRect(color = Color.Red.copy(alpha = 0.3f),topLeft = Offset.Zero,size = size)}}}// 方向控制按钮区域 - 仅在 useButtonControl 为 true 时显示if (useButtonControl) {Column(modifier = Modifier.padding(top = 8.dp),horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = { if (nextDirection != Direction.Down) nextDirection = Direction.Up },enabled = gameRunning && !gamePaused && !isDying,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF333333)),contentPadding = PaddingValues(12.dp),modifier = Modifier.size(60.dp)) {Text("上", color = Color.White, fontSize = 18.sp)}Row(modifier = Modifier.padding(vertical = 4.dp),horizontalArrangement = Arrangement.spacedBy(16.dp)) {Button(onClick = { if (nextDirection != Direction.Right) nextDirection = Direction.Left },enabled = gameRunning && !gamePaused && !isDying,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF333333)),contentPadding = PaddingValues(12.dp),modifier = Modifier.size(60.dp)) {Text("左", color = Color.White, fontSize = 18.sp)}Box(modifier = Modifier.size(60.dp))Button(onClick = { if (nextDirection != Direction.Left) nextDirection = Direction.Right },enabled = gameRunning && !gamePaused && !isDying,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF333333)),contentPadding = PaddingValues(12.dp),modifier = Modifier.size(60.dp)) {Text("右", color = Color.White, fontSize = 18.sp)}}Button(onClick = { if (nextDirection != Direction.Up) nextDirection = Direction.Down },enabled = gameRunning && !gamePaused && !isDying,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF333333)),contentPadding = PaddingValues(12.dp),modifier = Modifier.size(60.dp)) {Text("下", color = Color.White, fontSize = 18.sp)}}}// 底部控制按钮(暂停、结束)Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),horizontalArrangement = Arrangement.SpaceEvenly) {Button(onClick = { togglePause() },enabled = gameRunning && !isDying,colors = ButtonDefaults.buttonColors(containerColor = if (gamePaused) Color(0xFF2196F3) else Color(0xFFFF9800))) {Text(if (gamePaused) "继续" else "暂停", color = Color.White)}Button(onClick = { endGame() },enabled = gameRunning,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF44336))) {Text("结束游戏", color = Color.White)}}// 得分显示Text("得分: $score", color = Color.Cyan, fontSize = 20.sp, modifier = Modifier.padding(top = 8.dp))} // End of Column (无滚动的游戏界面内容)// 注意:原先在这里的 if (!gameRunning) { ... } 游戏结束界面已被移除// 现在由 SnakeGameApp 组件统一处理游戏结束状态和界面显示
}// --- 预览 ---
@androidx.compose.ui.tooling.preview.Preview(showBackground = true)
@Composable
fun PreviewSnakeGame() {SnakeGameTheme {SnakeGame(useButtonControl = false, initialSpeed = 200L, onGameEnd = {})}
}@androidx.compose.ui.tooling.preview.Preview(showBackground = true)
@Composable
fun PreviewGameStartScreen() {SnakeGameTheme {GameStartScreen(useButtonControl = false,onControlModeChange = {},initialSpeed = 200L,onSpeedChange = {},onStartGame = {})}
}@androidx.compose.ui.tooling.preview.Preview(showBackground = true)
@Composable
fun PreviewGameOverScreen() {SnakeGameTheme {GameOverScreen(score = 15) {}}
}
最终效果如下: