Android,Jetpack Compose,坦克大战游戏案例Demo(随机生成地图)
MainActivity代码:
package com.example.myapplicationimport android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
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.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.random.Randomclass MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {TankBattleTheme {Box(modifier = Modifier.fillMaxSize()) {Column(modifier = Modifier.fillMaxSize().padding(top = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() },bottom = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }),horizontalAlignment = Alignment.CenterHorizontally) {TankBattleGame()}}}}}
}// ======================
// 数据模型
// ======================
data class Tank(val id: String = "tank_${Random.nextLong()}",var x: Int,var y: Int,var direction: Direction,val color: Color,val isPlayer: Boolean = false,var isAlive: Boolean = true
)data class Bullet(var x: Int,var y: Int,val direction: Direction,val ownerId: String,val color: Color = Color.Yellow,val speed: Int // 添加子弹速度属性
)data class Wall(val x: Int, val y: Int, val width: Int = GRID_SIZE, val height: Int = GRID_SIZE)enum class Direction { UP, DOWN, LEFT, RIGHT }data class PlayerStats(var score: Int = 0,var lives: Int = 1 // 基础生命值改为1
)// 优化:扩展游戏设置,包含更多参数
data class GameSettings(val playerTankSpeed: Int, // 玩家坦克速度val enemyTankSpeed: Int, // 敌方坦克速度val playerBulletSpeed: Int, // 玩家子弹速度val enemyBulletSpeed: Int, // 敌方子弹速度val pointsPerExtraLife: Int // 加命所需分数
)// 修改:GameState 添加 RESTARTING 状态
enum class GameState { PLAYING, PAUSED, GAME_OVER, VICTORY, MENU, RESTARTING }// ======================
// 常量 (适配小米手机)
// ======================
const val GRID_SIZE = 45
const val MAP_WIDTH_GRIDS = 20
const val MAP_HEIGHT_GRIDS = 20
const val DEFAULT_BULLET_SPEED = 5
const val GAME_LOOP_DELAY = 50L
const val BULLET_RADIUS = 6f// ======================
// 主游戏入口 (优化后)
// ======================
@Composable
fun TankBattleGame() {val context = LocalContext.current// 优化:创建 ScoreRepository 实例val scoreRepository = remember { ScoreRepository(context) }var gameState by remember { mutableStateOf(GameState.MENU) }// 优化:初始化设置包含所有速度参数var gameSettings by remember {mutableStateOf(GameSettings(playerTankSpeed = 2,enemyTankSpeed = 2,playerBulletSpeed = DEFAULT_BULLET_SPEED,enemyBulletSpeed = DEFAULT_BULLET_SPEED,pointsPerExtraLife = 10))}// 优化:用于存储游戏结束时的最终分数var finalScore by remember { mutableStateOf(0) }// 优化:用于存储游戏结束原因var gameEndReason by remember { mutableStateOf<GameEndReason?>(null) }// 优化:处理分数保存的副作用// *** 修改:这是唯一保存分数的地方 ***LaunchedEffect(finalScore, gameEndReason) {if (gameEndReason != null) {// 当 finalScore 或 gameEndReason 更新时,保存分数scoreRepository.addScore(finalScore)// 显示 Toast 提示val message = when (gameEndReason) {GameEndReason.VICTORY -> "恭喜胜利!最终得分: $finalScore"GameEndReason.DEFEAT -> "游戏结束!最终得分: $finalScore"GameEndReason.QUIT -> "游戏结束,得分: $finalScore"null -> ""}if (message.isNotEmpty()) {Toast.makeText(context, message, Toast.LENGTH_SHORT).show()}// 重置原因,防止重复保存gameEndReason = null}}when (gameState) {GameState.MENU -> {// 优化:传递 ScoreRepository 到菜单界面GameMenuScreen(scoreRepository = scoreRepository,onStartGame = { settings ->gameSettings = settingsgameState = GameState.PLAYING})}GameState.PLAYING, GameState.PAUSED, GameState.GAME_OVER, GameState.VICTORY, GameState.RESTARTING -> {val density = LocalDensity.currentval gameWidthDp: Dp = with(density) { (MAP_WIDTH_GRIDS * GRID_SIZE).toDp() }val gameHeightDp: Dp = with(density) { (MAP_HEIGHT_GRIDS * GRID_SIZE).toDp() }RunningGame(scoreRepository = scoreRepository, // 传递 scoreRepositorygameSettings = gameSettings,gameState = gameState,gameWidthDp = gameWidthDp,gameHeightDp = gameHeightDp,onBackToMenu = { score, reason ->// *** 修改:更新最终分数和原因,触发保存 ***finalScore = scoregameEndReason = reasongameState = GameState.MENU},onTogglePause = {gameState = if (gameState == GameState.PLAYING) GameState.PAUSED else GameState.PLAYING},onGameEnd = { score, reason -> // 新增回调// *** 修改:只负责更新主游戏状态和显示提示,不保存分数 ***// 更新主游戏状态gameState = when (reason) {GameEndReason.VICTORY -> GameState.VICTORYGameEndReason.DEFEAT -> GameState.GAME_OVERelse -> gameState // QUIT 不改变主状态机,由 onBackToMenu 处理}// 注意:Toast 移到了 LaunchedEffect 中统一处理},// 新增 onRestart 回调onRestart = {gameState = GameState.RESTARTING})}}
}// ======================
// 菜单界面 (优化后)
// ======================
@Composable
fun GameMenuScreen(scoreRepository: ScoreRepository,onStartGame: (GameSettings) -> Unit
) {var initialSettings by remember {mutableStateOf(GameSettings(playerTankSpeed = 2,enemyTankSpeed = 2,playerBulletSpeed = DEFAULT_BULLET_SPEED,enemyBulletSpeed = DEFAULT_BULLET_SPEED,pointsPerExtraLife = 10))}var playerSpeed by remember { mutableStateOf(initialSettings.playerTankSpeed.toString()) }var enemySpeed by remember { mutableStateOf(initialSettings.enemyTankSpeed.toString()) }var playerBulletSpeed by remember { mutableStateOf(initialSettings.playerBulletSpeed.toString()) }var enemyBulletSpeed by remember { mutableStateOf(initialSettings.enemyBulletSpeed.toString()) }var pointsForLife by remember { mutableStateOf(initialSettings.pointsPerExtraLife.toString()) }val playerSpeedInt = playerSpeed.toIntOrNull() ?: 2val enemySpeedInt = enemySpeed.toIntOrNull() ?: 2val playerBulletSpeedInt = playerBulletSpeed.toIntOrNull() ?: DEFAULT_BULLET_SPEEDval enemyBulletSpeedInt = enemyBulletSpeed.toIntOrNull() ?: DEFAULT_BULLET_SPEEDval pointsForLifeInt = pointsForLife.toIntOrNull() ?: 10val isPlayerSpeedValid = playerSpeedInt in 1..8val isEnemySpeedValid = enemySpeedInt in 1..8val isPlayerBulletSpeedValid = playerBulletSpeedInt in 1..15val isEnemyBulletSpeedValid = enemyBulletSpeedInt in 1..15val isPointsValid = pointsForLifeInt >= 5 && pointsForLifeInt % 5 == 0val allValid = isPlayerSpeedValid && isEnemySpeedValid && isPlayerBulletSpeedValid && isEnemyBulletSpeedValid && isPointsValid// 优化:获取历史成绩的 Flow 并收集状态val scores by scoreRepository.scoresFlow.collectAsState(initial = emptyList())val coroutineScope = rememberCoroutineScope()MaterialTheme {Surface(color = Color.Black, modifier = Modifier.fillMaxSize()) {Column(modifier = Modifier.fillMaxSize().padding(20.dp),horizontalAlignment = Alignment.CenterHorizontally,) {Text("🎮 坦克大战", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold)Spacer(Modifier.height(10.dp))// 优化:使用 LazyColumn 包裹设置项和历史记录LazyColumn(modifier = Modifier.fillMaxWidth().weight(1f),verticalArrangement = Arrangement.spacedBy(8.dp)) {// 设置项item {OutlinedTextField(value = playerSpeed,onValueChange = { playerSpeed = it },label = { Text("玩家坦克速度 (1-8)", color = Color.White) },colors = OutlinedTextFieldDefaults.colors(focusedTextColor = Color.White,unfocusedTextColor = Color.White,cursorColor = Color.White,focusedLabelColor = Color.White,unfocusedLabelColor = Color.Gray,focusedBorderColor = Color.White,unfocusedBorderColor = Color.Gray),modifier = Modifier.fillMaxWidth(),isError = !isPlayerSpeedValid)if (!isPlayerSpeedValid) {Text("⚠️ 玩家速度必须在 1-8 之间", color = MaterialTheme.colorScheme.error, fontSize = 12.sp)}}item {OutlinedTextField(value = enemySpeed,onValueChange = { enemySpeed = it },label = { Text("敌方坦克速度 (1-8)", color = Color.White) },colors = OutlinedTextFieldDefaults.colors(focusedTextColor = Color.White,unfocusedTextColor = Color.White,cursorColor = Color.White,focusedLabelColor = Color.White,unfocusedLabelColor = Color.Gray,focusedBorderColor = Color.White,unfocusedBorderColor = Color.Gray),modifier = Modifier.fillMaxWidth(),isError = !isEnemySpeedValid)if (!isEnemySpeedValid) {Text("⚠️ 敌方速度必须在 1-8 之间", color = MaterialTheme.colorScheme.error, fontSize = 12.sp)}}item {OutlinedTextField(value = playerBulletSpeed,onValueChange = { playerBulletSpeed = it },label = { Text("玩家子弹速度 (1-15)", color = Color.White) },colors = OutlinedTextFieldDefaults.colors(focusedTextColor = Color.White,unfocusedTextColor = Color.White,cursorColor = Color.White,focusedLabelColor = Color.White,unfocusedLabelColor = Color.Gray,focusedBorderColor = Color.White,unfocusedBorderColor = Color.Gray),modifier = Modifier.fillMaxWidth(),isError = !isPlayerBulletSpeedValid)if (!isPlayerBulletSpeedValid) {Text("⚠️ 玩家子弹速度必须在 1-15 之间", color = MaterialTheme.colorScheme.error, fontSize = 12.sp)}}item {OutlinedTextField(value = enemyBulletSpeed,onValueChange = { enemyBulletSpeed = it },label = { Text("敌方子弹速度 (1-15)", color = Color.White) },colors = OutlinedTextFieldDefaults.colors(focusedTextColor = Color.White,unfocusedTextColor = Color.White,cursorColor = Color.White,focusedLabelColor = Color.White,unfocusedLabelColor = Color.Gray,focusedBorderColor = Color.White,unfocusedBorderColor = Color.Gray),modifier = Modifier.fillMaxWidth(),isError = !isEnemyBulletSpeedValid)if (!isEnemyBulletSpeedValid) {Text("⚠️ 敌方子弹速度必须在 1-15 之间", color = MaterialTheme.colorScheme.error, fontSize = 12.sp)}}item {OutlinedTextField(value = pointsForLife,onValueChange = { pointsForLife = it },label = { Text("多少分加一条命 (≥5 且 5 的倍数)", color = Color.White) },colors = OutlinedTextFieldDefaults.colors(focusedTextColor = Color.White,unfocusedTextColor = Color.White,cursorColor = Color.White,focusedLabelColor = Color.White,unfocusedLabelColor = Color.Gray,focusedBorderColor = Color.White,unfocusedBorderColor = Color.Gray),modifier = Modifier.fillMaxWidth(),isError = !isPointsValid)if (!isPointsValid) {Text("⚠️ 分数必须 ≥5 且是 5 的倍数", color = MaterialTheme.colorScheme.error, fontSize = 12.sp)}}// 历史记录标题item {Spacer(Modifier.height(10.dp))Text("📜 历史战绩", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium)Divider(color = Color.Gray, thickness = 1.dp)}// 历史记录列表items(scores) { gameScore ->Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),horizontalArrangement = Arrangement.SpaceBetween) {Text("得分: ${gameScore.score}", color = Color.Yellow)Text(gameScore.timestamp, color = Color.LightGray, fontSize = 12.sp)}}// 清除按钮 (放在列表末尾)item {Spacer(Modifier.height(10.dp))Button(onClick = {coroutineScope.launch {scoreRepository.clearScores()}},colors = ButtonDefaults.buttonColors(containerColor = Color.Red),modifier = Modifier.align(Alignment.End)) {Icon(Icons.Default.Delete, contentDescription = "清除", tint = Color.White)Spacer(Modifier.width(4.dp))Text("清除所有记录")}}}Spacer(Modifier.height(10.dp))Button(onClick = {if (allValid) {onStartGame(GameSettings(playerTankSpeed = playerSpeedInt,enemyTankSpeed = enemySpeedInt,playerBulletSpeed = playerBulletSpeedInt,enemyBulletSpeed = enemyBulletSpeedInt,pointsPerExtraLife = pointsForLifeInt))}},enabled = allValid,modifier = Modifier.fillMaxWidth()) {Text("开始游戏")}}}}
}// ======================
// 主游戏逻辑 (修改后的版本)
// ======================
@Composable
fun RunningGame(scoreRepository: ScoreRepository, // 新增参数gameSettings: GameSettings,gameState: GameState,gameWidthDp: Dp,gameHeightDp: Dp,onBackToMenu: (score: Int, reason: GameEndReason) -> Unit, // 修改:此回调只负责通知,不负责保存onTogglePause: () -> Unit,onGameEnd: (score: Int, reason: GameEndReason) -> Unit, // 新增回调参数onRestart: () -> Unit // 新增回调参数
) {val context = LocalContext.current// 移除了内部的 scoreRepository 创建// 优化:生成随机内部墙体// 保留边缘墙体val edgeWalls = remember {val wallThickness = GRID_SIZEval mapWidthPixels = MAP_WIDTH_GRIDS * GRID_SIZEval mapHeightPixels = MAP_HEIGHT_GRIDS * GRID_SIZEbuildList {for (x in 0 until MAP_WIDTH_GRIDS) {add(Wall(x * GRID_SIZE, 0))}for (x in 0 until MAP_WIDTH_GRIDS) {add(Wall(x * GRID_SIZE, mapHeightPixels - wallThickness))}for (y in 1 until MAP_HEIGHT_GRIDS - 1) {add(Wall(0, y * GRID_SIZE))}for (y in 1 until MAP_HEIGHT_GRIDS - 1) {add(Wall(mapWidthPixels - wallThickness, y * GRID_SIZE))}}}// 初始化内部墙体(随机生成)// 修改:调用随机墙体生成函数,排除玩家初始位置周围的格子val initialInnerWalls = remember {val playerStartX = MAP_WIDTH_GRIDS / 2val playerStartY = MAP_HEIGHT_GRIDS - 3val excludedCoords = setOf(Pair(playerStartX - 1, playerStartY - 1), Pair(playerStartX, playerStartY - 1), Pair(playerStartX + 1, playerStartY - 1),Pair(playerStartX - 1, playerStartY), Pair(playerStartX, playerStartY), Pair(playerStartX + 1, playerStartY),Pair(playerStartX - 1, playerStartY + 1), Pair(playerStartX, playerStartY + 1), Pair(playerStartX + 1, playerStartY + 1))generateRandomInnerWalls(30, excludedCoords) // 生成30个随机内部墙体}var innerWalls by remember { mutableStateOf(initialInnerWalls) }var walls by remember { mutableStateOf(edgeWalls + innerWalls) }var playerTank by remember {mutableStateOf(Tank(x = MAP_WIDTH_GRIDS / 2 * GRID_SIZE,y = (MAP_HEIGHT_GRIDS - 3) * GRID_SIZE,direction = Direction.UP,color = Color.Green,isPlayer = true,isAlive = true))}// 优化:使用 generateUniqueEnemyTanks 生成不重叠的坦克var enemyTanks by remember {mutableStateOf(generateUniqueEnemyTanks(walls, 10))}val bullets = remember { mutableStateListOf<Bullet>() }var currentGameState by remember { mutableStateOf(gameState) }var playerMovingDirection by remember { mutableStateOf<Direction?>(null) }var stats by remember { mutableStateOf(PlayerStats()) }var lastExtraLifeScore by remember { mutableStateOf(0) }// 修复:将LaunchedEffect移到@Composable函数内部,并使用状态变量控制// 修改:这个 LaunchedEffect 监听 currentGameState 的变化来控制游戏循环LaunchedEffect(currentGameState) {while (currentGameState == GameState.PLAYING || currentGameState == GameState.PAUSED) {if (currentGameState == GameState.PLAYING) {// 玩家移动playerMovingDirection?.let { dir ->val nextX = playerTank.x + when (dir) {Direction.LEFT -> -gameSettings.playerTankSpeedDirection.RIGHT -> gameSettings.playerTankSpeedelse -> 0}val nextY = playerTank.y + when (dir) {Direction.UP -> -gameSettings.playerTankSpeedDirection.DOWN -> gameSettings.playerTankSpeedelse -> 0}playerTank = playerTank.copy(direction = dir)if (canMove(nextX, nextY, GRID_SIZE, GRID_SIZE, walls, listOf(playerTank) + enemyTanks, playerTank.id)) {playerTank = playerTank.copy(x = nextX, y = nextY)}}// 敌方坦克移动enemyTanks = enemyTanks.map { tank ->if (!tank.isAlive) return@map tankvar newTank = tankif (Random.nextInt(100) < 5) {newTank = newTank.copy(direction = Direction.entries.random())}// 敌方坦克开火,子弹为蓝色,使用敌方子弹速度设置if (Random.nextInt(100) < 2) {val (bulletX, bulletY) = getBulletSpawnPosition(tank.x, tank.y, tank.direction)bullets.add(Bullet(x = bulletX, y = bulletY, direction = tank.direction, ownerId = tank.id, color = Color.Blue, speed = gameSettings.enemyBulletSpeed))}val nextX = newTank.x + when (newTank.direction) {Direction.LEFT -> -gameSettings.enemyTankSpeedDirection.RIGHT -> gameSettings.enemyTankSpeedelse -> 0}val nextY = newTank.y + when (newTank.direction) {Direction.UP -> -gameSettings.enemyTankSpeedDirection.DOWN -> gameSettings.enemyTankSpeedelse -> 0}if (canMove(nextX, nextY, GRID_SIZE, GRID_SIZE, walls, listOf(playerTank) + enemyTanks, newTank.id)) {newTank.copy(x = nextX, y = nextY)} else {val newDirection = when(newTank.direction) {Direction.UP -> Direction.DOWNDirection.DOWN -> Direction.UPDirection.LEFT -> Direction.RIGHTDirection.RIGHT -> Direction.LEFT}newTank.copy(direction = newDirection)}}// 子弹移动val updatedBullets = bullets.map { bullet ->val newX = bullet.x + when (bullet.direction) {Direction.LEFT -> -bullet.speedDirection.RIGHT -> bullet.speedelse -> 0}val newY = bullet.y + when (bullet.direction) {Direction.UP -> -bullet.speedDirection.DOWN -> bullet.speedelse -> 0}bullet.copy(x = newX, y = newY)}bullets.clear()bullets.addAll(updatedBullets)// 碰撞检测val bulletsToRemove = mutableStateListOf<Bullet>()val enemiesToKill = mutableStateListOf<Tank>()// 修改:创建一个集合来跟踪本帧已被标记为击毁的坦克IDval killedTankIds = mutableSetOf<String>()for (bullet in bullets) {// 子弹与墙碰撞if (walls.any { wall ->bullet.x >= wall.x && bullet.x < wall.x + wall.width &&bullet.y >= wall.y && bullet.y < wall.y + wall.height}) {bulletsToRemove.add(bullet)continue // 处理完墙碰撞后,跳过后续检查}// 子弹与坦克碰撞// 玩家子弹击中敌方坦克if (bullet.ownerId == playerTank.id) {for (tank in enemyTanks) {// 修改:检查坦克是否还活着且本帧未被标记为击毁if (tank.isAlive && !killedTankIds.contains(tank.id) &&isColliding(bullet.x, bullet.y, tank.x, tank.y, 2, GRID_SIZE)) {bulletsToRemove.add(bullet)enemiesToKill.add(tank)// 修改:将坦克ID添加到已击毁集合中killedTankIds.add(tank.id)break // 一颗子弹只能击毁一个目标}}}// 敌方子弹击中玩家坦克else if (playerTank.isAlive && isColliding(bullet.x, bullet.y, playerTank.x, playerTank.y, 2, GRID_SIZE)) {bulletsToRemove.add(bullet)playerTank.isAlive = false// 不需要 break,因为敌方子弹不能击中其他敌方坦克}// 敌方子弹击中敌方坦克的情况被忽略(即敌方坦克不会自相残杀)}// 优化:只有玩家消灭敌方坦克才加分if (enemiesToKill.isNotEmpty()) {enemiesToKill.forEach { it.isAlive = false }// 修改:确保每辆坦克只计1分,不管被多少子弹击中stats.score += enemiesToKill.size // 加分// 优化:检查是否达到加命分数val fullSegments = stats.score / gameSettings.pointsPerExtraLifeval prevSegments = lastExtraLifeScore / gameSettings.pointsPerExtraLifeif (fullSegments > prevSegments) {stats.lives += 1 // 生命值可以无限增加lastExtraLifeScore = stats.score}}bullets.removeAll(bulletsToRemove.toSet())// --- 修改开始 ---if (!playerTank.isAlive) {stats.lives-- // 减命if (stats.lives <= 0) {// *** 修改:游戏结束时调用回调,但不保存分数 ***onGameEnd(stats.score, GameEndReason.DEFEAT)} else {// 优化:不立即复活,而是等待重新开始// playerTank = playerTank.copy(isAlive = true, x = MAP_WIDTH_GRIDS / 2 * GRID_SIZE, y = (MAP_HEIGHT_GRIDS - 3) * GRID_SIZE)}} else if (enemyTanks.all { !it.isAlive }) {// *** 修改:胜利时也调用回调,但不保存分数 ***onGameEnd(stats.score, GameEndReason.VICTORY)}// --- 修改结束 ---}delay(GAME_LOOP_DELAY)}}// --- 优化:监听 gameState 变化来更新 currentGameState ---LaunchedEffect(gameState) {currentGameState = gameState// 新增:处理 RESTARTING 状态if (gameState == GameState.RESTARTING) {// 重置游戏状态和数据playerTank = Tank(x = MAP_WIDTH_GRIDS / 2 * GRID_SIZE,y = (MAP_HEIGHT_GRIDS - 3) * GRID_SIZE,direction = Direction.UP,color = Color.Green,isPlayer = true,isAlive = true)// 修改:重新生成随机内部墙体,并更新总墙体列表val playerStartX = MAP_WIDTH_GRIDS / 2val playerStartY = MAP_HEIGHT_GRIDS - 3val excludedCoords = setOf(Pair(playerStartX - 1, playerStartY - 1), Pair(playerStartX, playerStartY - 1), Pair(playerStartX + 1, playerStartY - 1),Pair(playerStartX - 1, playerStartY), Pair(playerStartX, playerStartY), Pair(playerStartX + 1, playerStartY),Pair(playerStartX - 1, playerStartY + 1), Pair(playerStartX, playerStartY + 1), Pair(playerStartX + 1, playerStartY + 1))innerWalls = generateRandomInnerWalls(30, excludedCoords) // 重新生成30个随机内部墙体walls = edgeWalls + innerWalls // 更新总墙体列表enemyTanks = generateUniqueEnemyTanks(walls, 10) // 重新生成敌方坦克bullets.clear()// 优化:重置生命值为1stats = PlayerStats(lives = 1, score = 0) // 重置生命值和分数lastExtraLifeScore = 0currentGameState = GameState.PLAYING}}Box(modifier = Modifier.fillMaxSize()) {Column(horizontalAlignment = Alignment.CenterHorizontally,modifier = Modifier.fillMaxSize()) {Box(modifier = Modifier.weight(1f).fillMaxWidth(),contentAlignment = Alignment.Center) {// --- 优化:移除 Canvas 上的按钮绘制 ---// 绘制游戏画布GameCanvas(playerTank = playerTank,enemyTanks = enemyTanks,bullets = bullets,walls = walls, // 使用更新后的 wallsgameWidthDp = gameWidthDp,gameHeightDp = gameHeightDp)// --- 优化:移除在 Canvas 上绘制的按钮 ---// --- 新增:使用 GameOverOverlay ---// 游戏结束或胜利时显示覆盖层if (currentGameState == GameState.GAME_OVER || currentGameState == GameState.VICTORY) {GameOverOverlay(state = currentGameState,score = stats.score,onRestart = {// 调用 onRestart 回调onRestart()},onExit = {// *** 修改:调用原始的返回菜单函数并传递分数和原因 ***// 这会触发 TankBattleGame 中的 LaunchedEffect 来保存分数onBackToMenu(stats.score, if(currentGameState == GameState.VICTORY) GameEndReason.VICTORY else GameEndReason.DEFEAT)})}// --- 新增结束 ---}Row(modifier = Modifier.fillMaxWidth().background(Color.DarkGray).padding(8.dp),horizontalArrangement = Arrangement.SpaceBetween) {Text("分数: ${stats.score}", color = Color.Yellow, fontSize = 16.sp)Text("生命: ${stats.lives}", color = Color.Red, fontSize = 16.sp)}Column(modifier = Modifier.fillMaxWidth()) {GameControls(onMove = { playerMovingDirection = it },onStopMove = { playerMovingDirection = null },onFire = {if (currentGameState == GameState.PLAYING) {val (bulletX, bulletY) = getBulletSpawnPosition(playerTank.x, playerTank.y, playerTank.direction)// 玩家坦克开火,子弹为红色,使用玩家子弹速度设置bullets.add(Bullet(x = bulletX, y = bulletY, direction = playerTank.direction, ownerId = playerTank.id, color = Color.Red, speed = gameSettings.playerBulletSpeed))}})// --- 新增开始 ---// 按钮位于开火按钮下方Row(modifier = Modifier.fillMaxWidth().padding(8.dp),horizontalArrangement = Arrangement.SpaceEvenly) {Button(onClick = onTogglePause,modifier = Modifier.weight(1f).padding(horizontal = 4.dp)) {Text(if (currentGameState == GameState.PAUSED) "▶️ 继续游戏" else "⏸️ 暂停游戏",fontSize = 14.sp)}// 修改:添加确认弹窗val openDialog = remember { mutableStateOf(false) }Button(onClick = { openDialog.value = true }, // 点击时打开弹窗modifier = Modifier.weight(1f).padding(horizontal = 4.dp),colors = ButtonDefaults.buttonColors(containerColor = Color.Red)) {Text("🏠 结束游戏", fontSize = 14.sp)}// 弹窗内容if (openDialog.value) {AlertDialog(onDismissRequest = { openDialog.value = false },title = { Text("确认退出") },text = { Text("确定要结束游戏并返回菜单吗?") },confirmButton = {TextButton(onClick = {openDialog.value = false// *** 修改:确认后调用回调 ***// 这会触发 TankBattleGame 中的 LaunchedEffect 来保存分数onBackToMenu(stats.score, GameEndReason.QUIT)}) {Text("确定")}},dismissButton = {TextButton(onClick = { openDialog.value = false }) {Text("取消")}})}}// --- 新增结束 ---}}}
}@Composable
fun GameOverOverlay(state: GameState,score: Int,onRestart: () -> Unit,onExit: () -> Unit
) {Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.8f)),contentAlignment = Alignment.Center) {Column(horizontalAlignment = Alignment.CenterHorizontally) {Text(text = when (state) {GameState.GAME_OVER -> "💀 GAME OVER"GameState.VICTORY -> "🎉 VICTORY!"else -> ""},fontSize = 40.sp,fontWeight = FontWeight.Bold,color = if (state == GameState.GAME_OVER) Color.Red else Color.Green)Text("最终得分: $score", color = Color.White, fontSize = 20.sp)Spacer(Modifier.height(20.dp))Row {Button(onClick = onRestart, modifier = Modifier.padding(4.dp)) {Text("🔄 重新开始")}Button(onClick = onExit, modifier = Modifier.padding(4.dp), colors = ButtonDefaults.buttonColors(containerColor = Color.Gray)) {Text("🏠 返回菜单")}}}}
}// ======================
// 游戏画布 (优化后的版本 - 移除了按钮)
// ======================
// 移除了 onBackToMenu, isPaused, onTogglePause 三个参数
@Composable
fun GameCanvas(playerTank: Tank,enemyTanks: List<Tank>,bullets: List<Bullet>,walls: List<Wall>,gameWidthDp: Dp,gameHeightDp: Dp// 移除了按钮相关的参数
) {Canvas(modifier = Modifier.size(gameWidthDp, gameHeightDp).border(2.dp, Color.Gray)) {walls.forEach { wall ->drawRect(color = Color.Gray,topLeft = Offset(wall.x.toFloat(), wall.y.toFloat()),size = androidx.compose.ui.geometry.Size(wall.width.toFloat(), wall.height.toFloat()))}if (playerTank.isAlive) {drawTank(playerTank)}enemyTanks.forEach { tank ->if (tank.isAlive) {drawTank(tank)}}bullets.forEach { bullet ->drawCircle(color = bullet.color,radius = BULLET_RADIUS,center = Offset(bullet.x.toFloat(), bullet.y.toFloat()))}}
}fun androidx.compose.ui.graphics.drawscope.DrawScope.drawTank(tank: Tank) {val centerX = tank.x + GRID_SIZE / 2fval centerY = tank.y + GRID_SIZE / 2fdrawCircle(color = tank.color,radius = GRID_SIZE / 2.2f,center = Offset(centerX, centerY))val barrelLength = GRID_SIZE / 1.8fval (dx, dy) = when (tank.direction) {Direction.UP -> Pair(0f, -barrelLength)Direction.DOWN -> Pair(0f, barrelLength)Direction.LEFT -> Pair(-barrelLength, 0f)Direction.RIGHT -> Pair(barrelLength, 0f)}drawLine(color = Color.Black,start = Offset(centerX, centerY),end = Offset(centerX + dx, centerY + dy),strokeWidth = 8f)
}// ======================
// 控制按钮
// ======================
@Composable
fun GameControls(onMove: (Direction) -> Unit,onStopMove: () -> Unit,onFire: () -> Unit
) {Row(modifier = Modifier.fillMaxWidth().padding(16.dp),horizontalArrangement = Arrangement.SpaceAround,verticalAlignment = Alignment.CenterVertically) {Box(modifier = Modifier.size(150.dp).background(Color.LightGray.copy(alpha = 0.5f), shape = CircleShape).pointerInput(Unit) {detectDragGestures(onDragStart = { },onDragEnd = { onStopMove() },onDragCancel = { onStopMove() },onDrag = { change, _ ->change.consume()val position = change.positionval centerX = size.width / 2fval centerY = size.height / 2fval dx = position.x - centerXval dy = position.y - centerYval absDx = kotlin.math.abs(dx)val absDy = kotlin.math.abs(dy)if (absDx > 20 || absDy > 20) {val direction = if (absDx > absDy) {if (dx > 0) Direction.RIGHT else Direction.LEFT} else {if (dy > 0) Direction.DOWN else Direction.UP}onMove(direction)}})},contentAlignment = Alignment.Center) {Box(modifier = Modifier.size(50.dp).background(Color.DarkGray, shape = CircleShape))}Button (onClick = onFire,modifier = Modifier.size(100.dp).padding(start = 20.dp),shape = CircleShape,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2196F3))) {Text("开火", fontSize = 18.sp)}}
}// ======================
// 辅助函数 (优化后)
// ======================// 新增:生成随机内部墙体的函数
// 修改:确保不生成在指定坐标集合中的墙体
fun generateRandomInnerWalls(count: Int, excludedCoords: Set<Pair<Int, Int>>): List<Wall> {val walls = mutableSetOf<Wall>()val maxAttempts = count * 100 // 防止无限循环var attempts = 0while (walls.size < count && attempts < maxAttempts) {attempts++// 生成随机坐标 (排除边缘和玩家初始区域)val x = Random.nextInt(1, MAP_WIDTH_GRIDS - 1)val y = Random.nextInt(1, MAP_HEIGHT_GRIDS - 1)// 检查是否在排除列表中if (Pair(x, y) in excludedCoords) {continue // 如果在排除列表,则跳过本次循环}val newWall = Wall(x * GRID_SIZE, y * GRID_SIZE)walls.add(newWall)}return walls.toList()
}fun canMove(x: Int, y: Int, width: Int, height: Int, walls: List<Wall>, tanks: List<Tank>, selfId: String): Boolean {val objectRight = x + widthval objectBottom = y + heightif (x < 0 || y < 0 || objectRight > MAP_WIDTH_GRIDS * GRID_SIZE || objectBottom > MAP_HEIGHT_GRIDS * GRID_SIZE) return falsefor (wall in walls) {if (x < wall.x + wall.width &&objectRight > wall.x &&y < wall.y + wall.height &&objectBottom > wall.y) {return false}}for (tank in tanks) {if (tank.id != selfId && tank.isAlive) {if (x < tank.x + GRID_SIZE &&objectRight > tank.x &&y < tank.y + GRID_SIZE &&objectBottom > tank.y) {return false}}}return true
}fun isColliding(x1: Int, y1: Int, x2: Int, y2: Int, size1: Int, size2: Int): Boolean {val centerX1 = x1 + size1 / 2val centerY1 = y1 + size1 / 2val radius1 = size1 / 2.2fval centerX2 = x2 + size2 / 2val centerY2 = y2 + size2 / 2val radius2 = size2 / 2.2fval distance = sqrt((centerX1 - centerX2).toDouble().pow(2) + (centerY1 - centerY2).toDouble().pow(2))return distance < (radius1 + radius2)
}// 优化:新函数,生成指定数量且位置不重叠的敌方坦克
fun generateUniqueEnemyTanks(walls: List<Wall>, count: Int): List<Tank> {val enemyTanks = mutableSetOf<Tank>() // 使用 Set 避免自身重复val maxAttempts = count * 100 // 防止无限循环var attempts = 0while (enemyTanks.size < count && attempts < maxAttempts) {attempts++var x: Intvar y: Intvar isValidPosition = falsevar newTank: Tank? = null// 尝试生成一个有效位置的坦克repeat(100) { // 为单个坦克尝试100次找到不重叠的位置x = Random.nextInt(0, MAP_WIDTH_GRIDS) * GRID_SIZEy = Random.nextInt(0, MAP_HEIGHT_GRIDS) * GRID_SIZE// 检查是否与墙碰撞或在出生点区域val wallCollision = walls.any { wall ->x < wall.x + wall.width &&x + GRID_SIZE > wall.x &&y < wall.y + wall.height &&y + GRID_SIZE > wall.y}val inSpawnArea = y >= (MAP_HEIGHT_GRIDS - 3) * GRID_SIZEif (!wallCollision && !inSpawnArea) {// 检查是否与已生成的坦克重叠val tankCollision = enemyTanks.any { existingTank ->x < existingTank.x + GRID_SIZE &&x + GRID_SIZE > existingTank.x &&y < existingTank.y + GRID_SIZE &&y + GRID_SIZE > existingTank.y}if (!tankCollision) {isValidPosition = truenewTank = Tank(x = x,y = y,direction = Direction.entries.random(),color = Color.Red)return@repeat // 找到有效位置,跳出循环}}}// 如果找到了有效位置且不与现有坦克重叠,则添加if (isValidPosition && newTank != null) {// 再次检查是否与集合中已有的坦克重叠(虽然上面检查了,但Set检查更安全)val finalCollision = enemyTanks.any { existingTank ->newTank.x < existingTank.x + GRID_SIZE &&newTank.x + GRID_SIZE > existingTank.x &&newTank.y < existingTank.y + GRID_SIZE &&newTank.y + GRID_SIZE > existingTank.y}if(!finalCollision) {enemyTanks.add(newTank)}}}return enemyTanks.toList()
}fun getBulletSpawnPosition(tankX: Int, tankY: Int, tankDirection: Direction): Pair<Int, Int> {val centerX = tankX + GRID_SIZE / 2val centerY = tankY + GRID_SIZE / 2val barrelLength = (GRID_SIZE / 1.8f).toInt()val (offsetX, offsetY) = when (tankDirection) {Direction.UP -> Pair(0, -barrelLength)Direction.DOWN -> Pair(0, barrelLength)Direction.LEFT -> Pair(-barrelLength, 0)Direction.RIGHT -> Pair(barrelLength, 0)}return Pair(centerX + offsetX, centerY + offsetY)
}// 新增枚举类,用于标识游戏结束的原因
enum class GameEndReason {VICTORY,DEFEAT,QUIT
}// ======================
// 预览
// ======================
@Preview(showBackground = true, backgroundColor = 0xFF000000, widthDp = 400, heightDp = 800)
@Composable
fun GameWithHUDPreview() {MaterialTheme {val previewPlayerTank = remember {Tank(x = MAP_WIDTH_GRIDS / 2 * GRID_SIZE,y = (MAP_HEIGHT_GRIDS - 3) * GRID_SIZE,direction = Direction.UP,color = Color.Green,isPlayer = true,isAlive = true)}val previewWalls = remember {val wallThickness = GRID_SIZEval mapWidthPixels = MAP_WIDTH_GRIDS * GRID_SIZEval mapHeightPixels = MAP_HEIGHT_GRIDS * GRID_SIZEval edgeWalls = buildList {for (x in 0 until MAP_WIDTH_GRIDS) {add(Wall(x * GRID_SIZE, 0))}for (x in 0 until MAP_WIDTH_GRIDS) {add(Wall(x * GRID_SIZE, mapHeightPixels - wallThickness))}for (y in 1 until MAP_HEIGHT_GRIDS - 1) {add(Wall(0, y * GRID_SIZE))}for (y in 1 until MAP_HEIGHT_GRIDS - 1) {add(Wall(mapWidthPixels - wallThickness, y * GRID_SIZE))}}val innerWalls = listOf(Wall(5 * GRID_SIZE, 5 * GRID_SIZE),Wall(6 * GRID_SIZE, 5 * GRID_SIZE),Wall(7 * GRID_SIZE, 5 * GRID_SIZE),Wall(9 * GRID_SIZE, 5 * GRID_SIZE),Wall(10 * GRID_SIZE, 5 * GRID_SIZE),Wall(7 * GRID_SIZE, 10 * GRID_SIZE),Wall(7 * GRID_SIZE, 11 * GRID_SIZE),Wall(7 * GRID_SIZE, 12 * GRID_SIZE),Wall(15 * GRID_SIZE, 8 * GRID_SIZE),Wall(15 * GRID_SIZE, 9 * GRID_SIZE),Wall(15 * GRID_SIZE, 10 * GRID_SIZE),Wall(3 * GRID_SIZE, 15 * GRID_SIZE),Wall(4 * GRID_SIZE, 15 * GRID_SIZE),Wall(5 * GRID_SIZE, 15 * GRID_SIZE))edgeWalls + innerWalls}val previewEnemyTanks = remember { generateUniqueEnemyTanks(previewWalls, 10) }// 预览中的子弹也使用新的速度属性val previewBullets = remember {listOf(Bullet(x = 5 * GRID_SIZE, y = 10 * GRID_SIZE, direction = Direction.UP, ownerId = "player", color = Color.Red, speed = DEFAULT_BULLET_SPEED),Bullet(x = 8 * GRID_SIZE, y = 5 * GRID_SIZE, direction = Direction.RIGHT, ownerId = "enemy_1", color = Color.Blue, speed = DEFAULT_BULLET_SPEED))}val previewStats = remember { PlayerStats(score = 15, lives = 2) }val density = LocalDensity.currentval previewGameWidthDp: Dp = with(density) { (MAP_WIDTH_GRIDS * GRID_SIZE).toDp() }val previewGameHeightDp: Dp = with(density) { (MAP_HEIGHT_GRIDS * GRID_SIZE).toDp() }Box(modifier = Modifier.fillMaxSize()) {Column(horizontalAlignment = Alignment.CenterHorizontally,modifier = Modifier.fillMaxSize()) {Box(modifier = Modifier.weight(1f).fillMaxWidth(),contentAlignment = Alignment.Center) {// 预览时只绘制 GameCanvas,不绘制按钮GameCanvas(playerTank = previewPlayerTank,enemyTanks = previewEnemyTanks,bullets = previewBullets,walls = previewWalls,gameWidthDp = previewGameWidthDp,gameHeightDp = previewGameHeightDp)}Row(modifier = Modifier.fillMaxWidth().background(Color.DarkGray).padding(8.dp),horizontalArrangement = Arrangement.SpaceBetween) {Text("分数: ${previewStats.score}", color = Color.Yellow, fontSize = 16.sp)Text("生命: ${previewStats.lives}", color = Color.Red, fontSize = 16.sp)}Column(modifier = Modifier.fillMaxWidth()) {Row(modifier = Modifier.padding(16.dp),horizontalArrangement = Arrangement.SpaceAround,verticalAlignment = Alignment.CenterVertically) {Box(modifier = Modifier.size(150.dp).background(Color.LightGray.copy(alpha = 0.5f), shape = CircleShape),contentAlignment = Alignment.Center) {Box(modifier = Modifier.size(50.dp).background(Color.DarkGray, shape = CircleShape))}Button (onClick = { },modifier = Modifier.size(100.dp).padding(start = 20.dp),shape = CircleShape,colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2196F3))) {Text("开火", fontSize = 18.sp)}}Row(modifier = Modifier.fillMaxWidth().padding(8.dp),horizontalArrangement = Arrangement.SpaceEvenly) {Button(onClick = { }, modifier = Modifier.weight(1f).padding(horizontal = 4.dp)) {Text("⏸️ 暂停游戏", fontSize = 14.sp)}Button(onClick = { },modifier = Modifier.weight(1f).padding(horizontal = 4.dp),colors = ButtonDefaults.buttonColors(containerColor = Color.Red)) {Text("🏠 结束游戏", fontSize = 14.sp)}}}}Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.3f)),contentAlignment = Alignment.TopStart) {Text("🖼️ UI 预览模式",color = Color.White,fontSize = 16.sp,modifier = Modifier.padding(8.dp))}}}
}@Preview(showBackground = true, widthDp = 400, heightDp = 800)
@Composable
fun MenuPreview() {// 预览更新后的菜单界面GameMenuScreen(scoreRepository = ScoreRepository(LocalContext.current), // 预览中创建实例onStartGame = {})
}// ======================
// 主题
// ======================
@Composable
fun TankBattleTheme(content: @Composable () -> Unit) {MaterialTheme(colorScheme = lightColorScheme(primary = Color(0xFF006064),secondary = Color(0xFF00B8D4),// error = Color.Red // 可以自定义错误颜色),typography = Typography(bodyLarge = androidx.compose.material3.Typography().bodyLarge.copy(fontSize = 16.sp)),content = content)
}
ScoreRepository代码:
package com.example.myapplicationimport android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter// 使用扩展属性创建 DataStore
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "game_scores")// 定义 Preferences Key
private val SCORES_KEY = stringPreferencesKey("scores_list")/*** 表示单个游戏成绩*/
data class GameScore(val score: Int, val timestamp: String)/*** 成绩管理仓库*/
class ScoreRepository(private val context: Context) {private val dataStore = context.dataStore/*** 流式返回所有历史成绩(按时间倒序)*/val scoresFlow: Flow<List<GameScore>> = dataStore.data.map { preferences ->val scoresString = preferences[SCORES_KEY] ?: ""parseScores(scoresString)}/*** 解析存储的字符串为 GameScore 列表* 格式:score1@timestamp1|score2@timestamp2* 使用 '@' 分隔 score 和 timestamp,避免 ISO 时间中的 ':' 冲突*/private fun parseScores(scoresString: String): List<GameScore> {if (scoresString.isEmpty()) return emptyList()return scoresString.split('|').mapNotNull { entry ->val index = entry.indexOf('@')if (index == -1) return@mapNotNull nullval scoreStr = entry.substring(0, index)val timestamp = entry.substring(index + 1)val score = scoreStr.toIntOrNull() ?: return@mapNotNull nullGameScore(score, timestamp)}.sortedByDescending { it.timestamp } // 按时间倒序}/*** 添加新成绩* 修改:时间戳格式精确到秒*/suspend fun addScore(score: Int) {// 修改:使用精确到秒的格式val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))val newEntry = "$score@$timestamp" // 使用 '@' 分隔,避免 ':' 冲突dataStore.edit { preferences ->val current = preferences[SCORES_KEY] ?: ""val updated = if (current.isEmpty()) {newEntry} else {"$newEntry|$current" // 最新在前}preferences[SCORES_KEY] = updated}}/*** 清空所有成绩*/suspend fun clearScores() {dataStore.edit { preferences ->preferences.remove(SCORES_KEY)}}
}
其中随机生成地图墙体函数:
fun generateRandomInnerWalls(count: Int, excludedCoords: Set<Pair<Int, Int>>): List<Wall> {val walls = mutableSetOf<Wall>()val maxAttempts = count * 100 // 防止无限循环var attempts = 0while (walls.size < count && attempts < maxAttempts) {attempts++// 生成随机坐标 (排除边缘和玩家初始区域)val x = Random.nextInt(1, MAP_WIDTH_GRIDS - 1)val y = Random.nextInt(1, MAP_HEIGHT_GRIDS - 1)// 检查是否在排除列表中if (Pair(x, y) in excludedCoords) {continue // 如果在排除列表,则跳过本次循环}val newWall = Wall(x * GRID_SIZE, y * GRID_SIZE)walls.add(newWall)}return walls.toList()
}