Android,Jetpack Compose,坦克大战游戏案例Demo

代码如下(这只是个简单案例而已):

复制代码
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.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
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.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 kotlin.math.pow
import kotlin.math.sqrt
import kotlin.random.Random

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TankBattleTheme {
                // 使用系统UI设置,确保内容不被状态栏遮挡
                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 // 默认颜色,但会在创建时指定
)

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 = 3
)

data class GameSettings(
    val tankSpeed: Int,
    val pointsPerExtraLife: Int
)

enum class GameState { PLAYING, PAUSED, GAME_OVER, VICTORY, MENU }

// ======================
// 常量 (适配小米手机) - 增大GRID_SIZE使坦克变大
// ======================
const val GRID_SIZE = 45 // 增大网格大小使坦克更大 (原为30)
const val MAP_WIDTH_GRIDS = 20 // 调整地图宽度以适应屏幕
const val MAP_HEIGHT_GRIDS = 20 // 调整地图高度以适应屏幕
const val BULLET_SPEED = 5
const val GAME_LOOP_DELAY = 50L
// 定义子弹半径
const val BULLET_RADIUS = 6f // 增大子弹大小

// ======================
// 主游戏入口
// ======================
@Composable
fun TankBattleGame() {
    var gameState by remember { mutableStateOf(GameState.MENU) }
    var gameSettings by remember {
        mutableStateOf(
            GameSettings(
                tankSpeed = 2,
                pointsPerExtraLife = 10
            )
        )
    }

    when (gameState) {
        GameState.MENU -> {
            GameSettingsScreen(
                initialSettings = gameSettings,
                onStart = { settings ->
                    gameSettings = settings
                    gameState = GameState.PLAYING
                }
            )
        }
        else -> {
            val density = LocalDensity.current
            val gameWidthDp: Dp = with(density) { (MAP_WIDTH_GRIDS * GRID_SIZE).toDp() }
            val gameHeightDp: Dp = with(density) { (MAP_HEIGHT_GRIDS * GRID_SIZE).toDp() }

            RunningGame(
                gameSettings = gameSettings,
                gameState = gameState,
                gameWidthDp = gameWidthDp,
                gameHeightDp = gameHeightDp,
                onBackToMenu = { gameState = GameState.MENU },
                onTogglePause = {
                    gameState = if (gameState == GameState.PLAYING) GameState.PAUSED else GameState.PLAYING
                }
            )
        }
    }
}

// ======================
// 设置界面
// ======================
@Composable
fun GameSettingsScreen(
    initialSettings: GameSettings,
    onStart: (GameSettings) -> Unit
) {
    MaterialTheme {
        Surface(color = Color.Black, modifier = Modifier.fillMaxSize()) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(20.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text("🎮 坦克大战", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold)
                Spacer(Modifier.height(40.dp))

                var speed by remember { mutableStateOf(initialSettings.tankSpeed.toString()) }
                var pointsForLife by remember { mutableStateOf(initialSettings.pointsPerExtraLife.toString()) }

                OutlinedTextField(
                    value = speed,
                    onValueChange = { speed = it },
                    label = { Text("坦克移动速度", 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()
                )

                Spacer(Modifier.height(20.dp))

                OutlinedTextField(
                    value = pointsForLife,
                    onValueChange = { pointsForLife = it },
                    label = { Text("多少分加一条命", 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()
                )

                Spacer(Modifier.height(20.dp))

                val isPointsValid = (pointsForLife.toIntOrNull() ?: 0) >= 5 &&
                        (pointsForLife.toIntOrNull() ?: 0) % 5 == 0

                Button(
                    onClick = {
                        val s = speed.toIntOrNull() ?: 2
                        val p = pointsForLife.toIntOrNull() ?: 10
                        if (isPointsValid) {
                            onStart(GameSettings(tankSpeed = s.coerceIn(1, 8), pointsPerExtraLife = p))
                        }
                    },
                    enabled = isPointsValid
                ) {
                    Text("开始游戏")
                }

                if (!isPointsValid) {
                    Text("⚠️ 必须 ≥5 且是 5 的倍数", color = Color.Red, fontSize = 12.sp)
                }
            }
        }
    }
}

// ======================
// 主游戏逻辑
// ======================
@Composable
fun RunningGame(
    gameSettings: GameSettings,
    gameState: GameState,
    gameWidthDp: Dp,
    gameHeightDp: Dp,
    onBackToMenu: () -> Unit,
    onTogglePause: () -> Unit
) {
    val edgeWalls = remember {
        val wallThickness = GRID_SIZE
        val mapWidthPixels = MAP_WIDTH_GRIDS * GRID_SIZE
        val mapHeightPixels = MAP_HEIGHT_GRIDS * GRID_SIZE

        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 = remember {
        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),
            Wall(15 * GRID_SIZE, 15 * GRID_SIZE),
            Wall(16 * GRID_SIZE, 15 * GRID_SIZE),
            Wall(17 * GRID_SIZE, 15 * GRID_SIZE)
        )
    }

    val walls = remember { 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
            )
        )
    }

    // 1. 修复:敌方坦克不能出现在墙壁内部
    var enemyTanks by remember {
        mutableStateOf(
            generateEnemyTanks(walls) // 使用新函数生成敌方坦克
        )
    }

    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(gameState) {
        currentGameState = gameState
    }

    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.tankSpeed
                        Direction.RIGHT -> gameSettings.tankSpeed
                        else -> 0
                    }
                    val nextY = playerTank.y + when (dir) {
                        Direction.UP -> -gameSettings.tankSpeed
                        Direction.DOWN -> gameSettings.tankSpeed
                        else -> 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 tank
                    var newTank = tank
                    if (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))
                    }
                    val nextX = newTank.x + when (newTank.direction) {
                        Direction.LEFT -> -2
                        Direction.RIGHT -> 2
                        else -> 0
                    }
                    val nextY = newTank.y + when (newTank.direction) {
                        Direction.UP -> -2
                        Direction.DOWN -> 2
                        else -> 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.DOWN
                            Direction.DOWN -> Direction.UP
                            Direction.LEFT -> Direction.RIGHT
                            Direction.RIGHT -> Direction.LEFT
                        }
                        newTank.copy(direction = newDirection)
                    }
                }

                // ... (子弹移动和碰撞检测逻辑保持不变)
                val updatedBullets = bullets.map { bullet ->
                    val newX = bullet.x + when (bullet.direction) {
                        Direction.LEFT -> -BULLET_SPEED
                        Direction.RIGHT -> BULLET_SPEED
                        else -> 0
                    }
                    val newY = bullet.y + when (bullet.direction) {
                        Direction.UP -> -BULLET_SPEED
                        Direction.DOWN -> BULLET_SPEED
                        else -> 0
                    }
                    bullet.copy(x = newX, y = newY)
                }
                bullets.clear()
                bullets.addAll(updatedBullets)

                val bulletsToRemove = mutableStateListOf<Bullet>()
                val enemiesToKill = mutableStateListOf<Tank>()

                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)
                    }
                    for (tank in enemyTanks) {
                        if (tank.isAlive && bullet.ownerId != tank.id && isColliding(bullet.x, bullet.y, tank.x, tank.y, 2, GRID_SIZE)) {
                            bulletsToRemove.add(bullet)
                            enemiesToKill.add(tank)
                        }
                    }
                    if (playerTank.isAlive && bullet.ownerId != playerTank.id && isColliding(bullet.x, bullet.y, playerTank.x, playerTank.y, 2, GRID_SIZE)) {
                        bulletsToRemove.add(bullet)
                        playerTank.isAlive = false
                    }
                }

                if (enemiesToKill.isNotEmpty()) {
                    enemiesToKill.forEach { it.isAlive = false }
                    stats.score += enemiesToKill.size

                    val fullSegments = stats.score / gameSettings.pointsPerExtraLife
                    val prevSegments = lastExtraLifeScore / gameSettings.pointsPerExtraLife
                    if (fullSegments > prevSegments) {
                        stats.lives += 1
                        lastExtraLifeScore = stats.score
                    }
                }

                bullets.removeAll(bulletsToRemove.toSet())

                if (!playerTank.isAlive) {
                    stats.lives--
                    if (stats.lives <= 0) {
                        currentGameState = GameState.GAME_OVER
                    } 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 }) {
                    currentGameState = GameState.VICTORY
                }
            }

            delay(GAME_LOOP_DELAY)
        }
    }

    // 3. 修复:调整布局,将分数和生命值移动到地图和按钮之间
    // 4. 修复:使用系统内边距防止状态栏遮挡
    Box(modifier = Modifier.fillMaxSize()) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.fillMaxSize()
        ) {

            // 游戏画布 - 居中显示
            Box(
                modifier = Modifier
                    .weight(1f) // 占据剩余空间
                    .fillMaxWidth(),
                contentAlignment = Alignment.Center
            ) {
                GameCanvas(
                    playerTank = playerTank,
                    enemyTanks = enemyTanks,
                    bullets = bullets,
                    walls = walls,
                    gameWidthDp = gameWidthDp,
                    gameHeightDp = gameHeightDp
                )
            }

            // 3. 将分数和生命值移动到画布和按钮之间,并减少间距
            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))
                        }
                    }
                )

                // 2. 修复:确保按钮完全显示,调整按钮布局
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp),
                    horizontalArrangement = Arrangement.SpaceEvenly
                ) {
                    // 使用 weight 来平均分配空间,确保按钮完整显示
                    Button(
                        onClick = onTogglePause,
                        modifier = Modifier
                            .weight(1f) // 平均分配宽度
                            .padding(horizontal = 4.dp)
                    ) {
                        Text(if (currentGameState == GameState.PLAYING) "⏸️ 暂停" else "▶️ 继续")
                    }

                    Button(
                        onClick = onBackToMenu,
                        modifier = Modifier
                            .weight(1f) // 平均分配宽度
                            .padding(horizontal = 4.dp),
                        colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
                    ) {
                        Text("🚪 退出")
                    }
                }
            }
        }

        if (currentGameState != GameState.PLAYING && currentGameState != GameState.PAUSED) {
            GameOverOverlay(
                state = currentGameState,
                score = stats.score,
                onRestart = {
                    playerTank = playerTank.copy(isAlive = true, x = MAP_WIDTH_GRIDS / 2 * GRID_SIZE, y = (MAP_HEIGHT_GRIDS - 3) * GRID_SIZE)
                    enemyTanks = generateEnemyTanks(walls) // 重新生成敌方坦克
                    bullets.clear()
                    stats = PlayerStats(score = 0, lives = 3)
                    lastExtraLifeScore = 0
                    currentGameState = GameState.PLAYING
                },
                onExit = onBackToMenu
            )
        }

        if (currentGameState == GameState.PAUSED) {
            Box(
                modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.6f)),
                contentAlignment = Alignment.Center
            ) {
                Text("⏸️ 游戏已暂停", color = Color.White, fontSize = 30.sp, fontWeight = FontWeight.Bold)
            }
        }
    }
}

@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("🏠 返回菜单")
                }
            }
        }
    }
}

// ======================
// 游戏画布
// ======================
@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 / 2f
    val centerY = tank.y + GRID_SIZE / 2f

    // 增大坦克主体
    drawCircle(
        color = tank.color,
        radius = GRID_SIZE / 2.2f, // 稍微调整比例使看起来更好
        center = Offset(centerX, centerY)
    )

    // 增大炮管
    val barrelLength = GRID_SIZE / 1.8f // 增加炮管长度
    val (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.position
                            val centerX = size.width / 2f
                            val centerY = size.height / 2f
                            val dx = position.x - centerX
                            val dy = position.y - centerY

                            val 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 canMove(x: Int, y: Int, width: Int, height: Int, walls: List<Wall>, tanks: List<Tank>, selfId: String): Boolean {
    val objectRight = x + width
    val objectBottom = y + height

    if (x < 0 || y < 0 || objectRight > MAP_WIDTH_GRIDS * GRID_SIZE || objectBottom > MAP_HEIGHT_GRIDS * GRID_SIZE) return false

    for (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 / 2
    val centerY1 = y1 + size1 / 2
    val radius1 = size1 / 2.2f // 使用新的半径计算

    val centerX2 = x2 + size2 / 2
    val centerY2 = y2 + size2 / 2
    val radius2 = size2 / 2.2f // 使用新的半径计算

    val distance = sqrt((centerX1 - centerX2).toDouble().pow(2) + (centerY1 - centerY2).toDouble().pow(2))
    return distance < (radius1 + radius2)
}

// 1. 新增:生成不与墙壁重叠的敌方坦克
fun generateEnemyTanks(walls: List<Wall>): List<Tank> {
    val enemyTankCount = 7
    val enemyTanks = mutableListOf<Tank>()

    repeat(enemyTankCount) {
        var x: Int
        var y: Int
        do {
            // 随机选择一个网格位置
            x = Random.nextInt(0, MAP_WIDTH_GRIDS) * GRID_SIZE
            y = Random.nextInt(0, MAP_HEIGHT_GRIDS) * GRID_SIZE
        } while (
        // 检查该位置是否与任何墙壁重叠
            walls.any { wall ->
                x < wall.x + wall.width &&
                        x + GRID_SIZE > wall.x &&
                        y < wall.y + wall.height &&
                        y + GRID_SIZE > wall.y
            } ||
            // 避免生成在最底部的玩家出生区域
            y >= (MAP_HEIGHT_GRIDS - 3) * GRID_SIZE
        )

        enemyTanks.add(
            Tank(
                x = x,
                y = y,
                direction = Direction.entries.random(),
                color = Color.Red
            )
        )
    }

    return enemyTanks
}

// 新增:计算子弹从炮管发射的确切位置
fun getBulletSpawnPosition(tankX: Int, tankY: Int, tankDirection: Direction): Pair<Int, Int> {
    val centerX = tankX + GRID_SIZE / 2
    val centerY = tankY + GRID_SIZE / 2
    val 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)
    }

    // 将 Float 转换为 Int 以匹配 Bullet 的构造函数
    return Pair(centerX + offsetX, centerY + offsetY)
}


// ======================
// 预览
// ======================
@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_SIZE
            val mapWidthPixels = MAP_WIDTH_GRIDS * GRID_SIZE
            val mapHeightPixels = MAP_HEIGHT_GRIDS * GRID_SIZE
            val 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 { generateEnemyTanks(previewWalls) }

        // 预览中的子弹也使用新的颜色和大小
        val previewBullets = remember {
            listOf(
                Bullet(x = 5 * GRID_SIZE, y = 10 * GRID_SIZE, direction = Direction.UP, ownerId = "player", color = Color.Red),
                Bullet(x = 8 * GRID_SIZE, y = 5 * GRID_SIZE, direction = Direction.RIGHT, ownerId = "enemy_1", color = Color.Blue)
            )
        }

        val previewStats = remember { PlayerStats(score = 15, lives = 2) }

        val density = LocalDensity.current
        val 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(
                        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("⏸️ 暂停")
                        }
                        Button(
                            onClick = { },
                            modifier = Modifier
                                .weight(1f)
                                .padding(horizontal = 4.dp),
                            colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
                        ) {
                            Text("🚪 退出")
                        }
                    }
                }
            }

            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 SettingsPreview() {
    GameSettingsScreen(
        initialSettings = GameSettings(tankSpeed = 2, pointsPerExtraLife = 10),
        onStart = {}
    )
}

// ======================
// 主题
// ======================
@Composable
fun TankBattleTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = lightColorScheme(
            primary = Color(0xFF006064),
            secondary = Color(0xFF00B8D4)
        ),
        typography = Typography(
            bodyLarge = androidx.compose.material3.Typography().bodyLarge.copy(fontSize = 16.sp)
        ),
        content = content
    )
}

最终效果如下:

相关推荐
好家伙VCC25 分钟前
# 发散创新:用 Rust构建高性能游戏日系统,从零实现事件驱动架构 在现代游戏开发中,**性能与可扩展性**是核心命题。传统基于
java·python·游戏·架构·rust
魔士于安36 分钟前
氛围感游戏场景,天空盒,带地形,附赠一个空要塞
游戏·unity·游戏引擎·贴图
上海云盾-小余2 小时前
CC 攻击与 DDoS 联动防护:如何构建一体化流量清洗架构
网络·安全·游戏·架构·ddos
2501_937189232 小时前
莫凡电视:地方台专属聚合 稳定直播播放工具
android·源码·源代码管理
耶叶4 小时前
Android 新权限申请模型(Activity Result API)
android
阿拉斯攀登4 小时前
【RK3576 安卓 JNI/NDK 系列 04】JNI 核心语法(下):字符串、数组与对象操作
android·驱动开发·rk3568·瑞芯微·rk安卓驱动·jni字符串操作
2501_915909064 小时前
不用越狱就看不到 iOS App 内部文件?使用 Keymob 查看和导出应用数据目录
android·ios·小程序·https·uni-app·iphone·webview
llxxyy卢4 小时前
web部分中等题目
android·前端
轩情吖4 小时前
MySQL之事务管理
android·后端·mysql·adb·事务·隔离性·原子性
万物得其道者成4 小时前
uni-app Android 离线打包:多环境(prod/dev)配置
android·opencv·uni-app