ANDROID,Jetpack Compose, 贪吃蛇小游戏Demo

SnakeGameMainActivity代码如下:

复制代码
package com.example.myapplication

import 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.Down
    Direction.Down -> Direction.Up
    Direction.Left -> Direction.Right
    Direction.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 = score
                gameStarted = 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.current
    val config = LocalConfiguration.current

    // 计算游戏区域大小,适配屏幕
    val screenWidth = config.screenWidthDp.dp
    val 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 = newFood
                return
            }
        }
    }

    // 移动蛇的逻辑
    fun moveSnake() {
        if (!gameRunning || gamePaused) return

        direction = nextDirection
        val 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 = true
                dyingJob = coroutineScope.launch {
                    delay(2000) // 2秒后确认死亡
                    if (isDying) {
                        gameRunning = false
                        onGameEnd(score) // 游戏结束,传递分数
                    }
                }
            }
            return
        } else {
            if (isDying) {
                isDying = false
                dyingJob?.cancel()
                dyingJob = null
            }
        }

        // 检查撞到自己
        if (newHead in snake) {
            gameRunning = false
            onGameEnd(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 = null
        isDying = false
        gameRunning = true
        gamePaused = false
        snake = listOf(Point((gridSize / 2).toInt(), (gridSize / 2).toInt()))
        direction = Direction.Right
        nextDirection = Direction.Right
        score = 0
        placeFood()
    }

    // 切换暂停/继续
    fun togglePause() {
        if (gameRunning && !isDying) {
            gamePaused = !gamePaused
        }
    }

    // 结束游戏 - 修改为调用 onGameEnd 并传递当前分数
    fun endGame() {
        if (gameRunning) {
            gameRunning = false
            isDying = false
            dyingJob?.cancel()
            dyingJob = null
            onGameEnd(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@pointerInput
                    awaitPointerEventScope {
                        while (true) {
                            val down = awaitFirstDown()
                            val start = down.position
                            val upEvent = waitForUpOrCancellation()
                            val end = upEvent?.position ?: start

                            val dx = end.x - start.x
                            val dy = end.y - start.y

                            val 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 * blockSize
                    drawLine(
                        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) {}
    }
}

最终效果如下:

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui