很简单的逻辑,下面有预览视频,实现了失败,成功和积分。待实现的逻辑:
- 积分越大,速度越快,横板越小
- 关卡概念
- 随机分布砖块概念
- 添加不可消除的砖块增加可玩性
简单的预览:
实现代码:
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)
}
}
