用Compose实现一个简单的弹球消除小游戏

By

很简单的逻辑,下面有预览视频,实现了失败,成功和积分。待实现的逻辑:

  1. 积分越大,速度越快,横板越小
  2. 关卡概念
  3. 随机分布砖块概念
  4. 添加不可消除的砖块增加可玩性

简单的预览:

实现代码:


data class GameState(
    val ballPos: Offset = Offset(0f, 0f),
    val ballVelocity: Offset = Offset(10f, -10f),
    val paddleX: Float = 0f,
    val bricks: List<Brick> = emptyList(),
    val score: Int = 0,
    val isGameOver: Boolean = false,
    val isWon: Boolean = false
)

data class Brick(
    val id: Int,
    val rect: androidx.compose.ui.geometry.Rect,
    val color: Color,
    val active: Boolean = true
)

private const val BALL_RADIUS = 20f
private const val PADDLE_WIDTH = 200f
private const val PADDLE_HEIGHT = 40f
private const val BRICK_ROWS = 6
private const val BRICK_COLS = 8
private const val BRICK_HEIGHT = 60f

@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun BreakoutGame() {
    BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
        val density = LocalDensity.current
        val screenWidth = with(density) { maxWidth.toPx() }
        val screenHeight = with(density) { maxHeight.toPx() }

        val initialBricks = remember(screenWidth) {
            val bricks = mutableListOf<Brick>()
            val brickWidth = screenWidth / BRICK_COLS
            var idCounter = 0
            for (row in 0 until BRICK_ROWS) {
                for (col in 0 until BRICK_COLS) {
                    val color = Color.hsv((row * 360f / BRICK_ROWS), 0.8f, 1f)
                    bricks.add(
                        Brick(
                            id = idCounter++,
                            rect = androidx.compose.ui.geometry.Rect(
                                left = col * brickWidth + 5f,
                                top = row * BRICK_HEIGHT + 100f,
                                right = (col + 1) * brickWidth - 5f,
                                bottom = (row + 1) * BRICK_HEIGHT + 100f - 5f
                            ),
                            color = color
                        )
                    )
                }
            }
            bricks
        }

        var gameState by remember {
            mutableStateOf(
                GameState(
                    ballPos = Offset(screenWidth / 2, screenHeight / 2),
                    paddleX = (screenWidth - PADDLE_WIDTH) / 2,
                    bricks = initialBricks
                )
            )
        }

        LaunchedEffect(Unit) {
            while (isActive) {
                withFrameNanos { _ ->
                    if (!gameState.isGameOver && !gameState.isWon) {
                        gameState = updateGameLogic(gameState, screenWidth, screenHeight)
                    }
                }
            }
        }

        Canvas(
            modifier = Modifier
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consume()
                        val newPaddleX = (gameState.paddleX + dragAmount.x)
                            .coerceIn(0f, screenWidth - PADDLE_WIDTH)
                        gameState = gameState.copy(paddleX = newPaddleX)
                    }
                }
        ) {
            drawRect(color = Color(0xFF121212))

            gameState.bricks.forEach { brick ->
                if (brick.active) {
                    drawRect(
                        color = brick.color,
                        topLeft = brick.rect.topLeft,
                        size = brick.rect.size
                    )
                }
            }

            drawRect(
                color = Color.Cyan,
                topLeft = Offset(gameState.paddleX, screenHeight - PADDLE_HEIGHT - 50f),
                size = Size(PADDLE_WIDTH, PADDLE_HEIGHT)
            )

            drawCircle(
                color = Color.White,
                radius = BALL_RADIUS,
                center = gameState.ballPos
            )

            drawGameUI(gameState, size.width, size.height)
        }

        if (gameState.isGameOver || gameState.isWon) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .pointerInput(Unit) {
                        detectDragGestures { _, _ -> }
                    }
                    .pointerInput(Unit) {
                        awaitPointerEventScope {
                            while (true) {
                                val event = awaitPointerEvent()
                                if (event.changes.any { it.pressed }) {
                                    gameState = GameState(
                                        ballPos = Offset(screenWidth / 2, screenHeight / 2),
                                        ballVelocity = Offset(
                                            if (Random.nextBoolean()) 10f else -10f,
                                            -10f
                                        ),
                                        paddleX = (screenWidth - PADDLE_WIDTH) / 2,
                                        bricks = initialBricks.map { it.copy(active = true) },
                                        score = 0,
                                        isGameOver = false,
                                        isWon = false
                                    )
                                }
                            }
                        }
                    }
            )
        }
    }
}

fun updateGameLogic(state: GameState, width: Float, height: Float): GameState {
    var newPos = state.ballPos + state.ballVelocity
    var newVel = state.ballVelocity
    var newScore = state.score
    var gameOver = false
    var won = false

    if (newPos.x - BALL_RADIUS < 0) {
        newPos = newPos.copy(x = BALL_RADIUS)
        newVel = newVel.copy(x = -newVel.x)
    } else if (newPos.x + BALL_RADIUS > width) {
        newPos = newPos.copy(x = width - BALL_RADIUS)
        newVel = newVel.copy(x = -newVel.x)
    }

    if (newPos.y - BALL_RADIUS < 0) {
        newPos = newPos.copy(y = BALL_RADIUS)
        newVel = newVel.copy(y = -newVel.y)
    }

    if (newPos.y - BALL_RADIUS > height) {
        gameOver = true
    }

    val paddleRect = androidx.compose.ui.geometry.Rect(
        left = state.paddleX,
        top = height - PADDLE_HEIGHT - 50f,
        right = state.paddleX + PADDLE_WIDTH,
        bottom = height - 50f
    )

    if (newPos.y + BALL_RADIUS >= paddleRect.top &&
        newPos.y - BALL_RADIUS <= paddleRect.bottom &&
        newPos.x >= paddleRect.left &&
        newPos.x <= paddleRect.right &&
        newVel.y > 0
    ) {
        newVel = newVel.copy(y = -newVel.y)
        val hitPercent = (newPos.x - paddleRect.center.x) / (PADDLE_WIDTH / 2)
        newVel = newVel.copy(x = newVel.x + hitPercent * 5f)
    }

    val newBricks = state.bricks.toMutableList()
    var hitBrick = false

    for (i in newBricks.indices) {
        val brick = newBricks[i]
        if (brick.active) {
            val ballRect = androidx.compose.ui.geometry.Rect(
                newPos.x - BALL_RADIUS, newPos.y - BALL_RADIUS,
                newPos.x + BALL_RADIUS, newPos.y + BALL_RADIUS
            )

            if (brick.rect.overlaps(ballRect)) {
                newBricks[i] = brick.copy(active = false)
                newVel = newVel.copy(y = -newVel.y)
                newScore += 10
                hitBrick = true
                break
            }
        }
    }

    if (newBricks.none { it.active }) {
        won = true
    }

    return state.copy(
        ballPos = newPos,
        ballVelocity = newVel,
        bricks = newBricks,
        score = newScore,
        isGameOver = gameOver,
        isWon = won
    )
}

fun DrawScope.drawGameUI(state: GameState, width: Float, height: Float) {
    val paint = android.graphics.Paint().apply {
        textSize = 60f
        color = android.graphics.Color.WHITE
        textAlign = android.graphics.Paint.Align.LEFT
    }

    drawContext.canvas.nativeCanvas.drawText("Score: ${state.score}", 50f, 80f, paint)

    if (state.isGameOver) {
        paint.textAlign = android.graphics.Paint.Align.CENTER
        paint.textSize = 100f
        paint.color = android.graphics.Color.RED
        drawContext.canvas.nativeCanvas.drawText("GAME OVER", width / 2, height / 2, paint)
        paint.textSize = 50f
        paint.color = android.graphics.Color.WHITE
        drawContext.canvas.nativeCanvas.drawText("Tap to Restart", width / 2, height / 2 + 100f, paint)
    } else if (state.isWon) {
        paint.textAlign = android.graphics.Paint.Align.CENTER
        paint.textSize = 100f
        paint.color = android.graphics.Color.GREEN
        drawContext.canvas.nativeCanvas.drawText("YOU WIN!", width / 2, height / 2, paint)
        paint.textSize = 50f
        paint.color = android.graphics.Color.WHITE
        drawContext.canvas.nativeCanvas.drawText("Tap to Restart", width / 2, height / 2 + 100f, paint)
    }
}