Android,jetpack compose实现俄罗斯方块,简单案例♦️

代码如下:

复制代码
package com.example.tetris

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.layout.*
import androidx.compose.foundation.lazy.LazyColumn
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.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
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.window.Dialog
import kotlinx.coroutines.delay
import kotlin.math.max

val CELL_SIZE: Dp = 24.dp
val GRID_WIDTH = 10
val GRID_HEIGHT = 20
val PREVIEW_SIZE = 4

data class Block(val x: Int, val y: Int, val color: Color)

sealed class Shape(val blocks: List<Block>) {
    object I : Shape(
        listOf(
            Block(0, 0, Color.Cyan), Block(1, 0, Color.Cyan),
            Block(2, 0, Color.Cyan), Block(3, 0, Color.Cyan)
        )
    ) {
        override fun center(): Offset = Offset(1.5f, 0.5f)
    }

    object O : Shape(
        listOf(
            Block(0, 0, Color.Yellow), Block(1, 0, Color.Yellow),
            Block(0, 1, Color.Yellow), Block(1, 1, Color.Yellow)
        )
    ) {
        override fun center(): Offset = Offset(0.5f, 0.5f)
        override fun rotate(): Shape = this
    }

    object T : Shape(
        listOf(
            Block(0, 0, Color.Magenta), Block(1, 0, Color.Magenta),
            Block(2, 0, Color.Magenta), Block(1, 1, Color.Magenta)
        )
    ) {
        override fun center(): Offset = Offset(1f, 1f)
    }

    object L : Shape(
        listOf(
            Block(0, 0, Color(0xFFFFA500)), Block(1, 0, Color(0xFFFFA500)),
            Block(2, 0, Color(0xFFFFA500)), Block(2, 1, Color(0xFFFFA500))
        )
    ) {
        override fun center(): Offset = Offset(1f, 1f)
    }

    object J : Shape(
        listOf(
            Block(0, 0, Color.Blue), Block(1, 0, Color.Blue),
            Block(2, 0, Color.Blue), Block(0, 1, Color.Blue)
        )
    ) {
        override fun center(): Offset = Offset(1f, 1f)
    }

    object S : Shape(
        listOf(
            Block(1, 0, Color.Green), Block(2, 0, Color.Green),
            Block(0, 1, Color.Green), Block(1, 1, Color.Green)
        )
    ) {
        override fun center(): Offset = Offset(1f, 1f)
    }

    object Z : Shape(
        listOf(
            Block(0, 0, Color.Red), Block(1, 0, Color.Red),
            Block(1, 1, Color.Red), Block(2, 1, Color.Red)
        )
    ) {
        override fun center(): Offset = Offset(1f, 1f)
    }

    abstract fun center(): Offset

    open fun rotate(): Shape {
        val centerPoint = this.center()
        val rotatedBlocks = this.blocks.map { block ->
            val x = (block.x - centerPoint.x)
            val y = (block.y - centerPoint.y)
            val newX = (y + centerPoint.x).toInt()
            val newY = (-x + centerPoint.y).toInt()
            block.copy(x = newX, y = newY)
        }
        return ShapeImpl(rotatedBlocks, centerPoint)
    }

    companion object {
        fun random(): Shape = when ((0..6).random()) {
            0 -> I
            1 -> O
            2 -> T
            3 -> L
            4 -> J
            5 -> S
            6 -> Z
            else -> I
        }
    }
}

private class ShapeImpl(blocks: List<Block>, private val centerPoint: Offset) : Shape(blocks) {
    override fun center(): Offset = centerPoint
    override fun rotate(): Shape = super.rotate()
}

enum class GameState { PLAYING, PAUSED, GAME_OVER }

enum class Difficulty(val initialDropDelay: Long) {
    EASY(1000L),
    MEDIUM(500L),
    HARD(200L)
}

@Composable
fun SetupScreen(onStartGame: (Difficulty) -> Unit) {
    var selectedDifficulty by remember { mutableStateOf(Difficulty.MEDIUM) }
    val difficulties = Difficulty.values().toList()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("俄罗斯方块", style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(bottom = 32.dp))
        Text("选择难度:", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 8.dp))

        LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            items(difficulties.size) { index ->
                val difficulty = difficulties[index]
                Card(
                    onClick = { selectedDifficulty = difficulty },
                    modifier = Modifier.fillMaxWidth(),
                    colors = if (selectedDifficulty == difficulty) {
                        CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
                    } else {
                        CardDefaults.cardColors()
                    }
                ) {
                    Text(
                        text = difficulty.name,
                        modifier = Modifier.padding(16.dp),
                        style = MaterialTheme.typography.bodyLarge
                    )
                }
            }
        }

        Spacer(modifier = Modifier.height(32.dp))

        Button(
            onClick = { onStartGame(selectedDifficulty) },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("开始游戏")
        }
    }
}

@Composable
fun TetrisGameContent(initialDropDelay: Long, onExit: () -> Unit) {
    val density = LocalDensity.current
    val canvasWidth = with(density) { CELL_SIZE.toPx() * GRID_WIDTH }
    val canvasHeight = with(density) { CELL_SIZE.toPx() * GRID_HEIGHT }
    val cellPx = with(density) { CELL_SIZE.toPx() }

    var grid by remember { mutableStateOf<List<Block>>(emptyList()) }
    var currentShape by remember { mutableStateOf(Shape.random()) }
    var currentPosition by remember { mutableStateOf(Offset(GRID_WIDTH / 2 - 1f, 0f)) }
    var nextShape by remember { mutableStateOf(Shape.random()) }
    var score by remember { mutableStateOf(0) }
    var level by remember { mutableStateOf(1) }
    var linesClearedTotal by remember { mutableStateOf(0) }
    var gameState by remember { mutableStateOf(GameState.PLAYING) }

    val baseDropDelay = initialDropDelay
    val dropDelay = (baseDropDelay - (level - 1) * 50L).coerceAtLeast(100L)

    fun clearLines(): Int {
        // 1. 找出需要消除的行
        val linesToClear = (0 until GRID_HEIGHT).filter { y ->
            (0 until GRID_WIDTH).all { x -> grid.any { it.x == x && it.y == y } }
        }
        if (linesToClear.isEmpty()) return 0

        // 2. 从网格中移除这些行的方块
        grid = grid.filterNot { it.y in linesToClear }

        // 3. 计算每一行需要下落的格数 (关键修复部分)
        val dropCounts = IntArray(GRID_HEIGHT) { 0 }
        var linesToDrop = 0
        // 从底部开始向上遍历
        for (y in GRID_HEIGHT - 1 downTo 0) {
            if (y in linesToClear) {
                linesToDrop++ // 遇到要消除的行,增加下落计数
            } else {
                dropCounts[y] = linesToDrop // 非消除行需要下落 linesToDrop 行
            }
        }

        // 4. 根据 dropCounts 更新方块的 Y 坐标
        grid = grid.map { block ->
            val drop = dropCounts[block.y]
            block.copy(y = block.y + drop)
        }

        return linesToClear.size
    }


    fun isValidPosition(dx: Float = 0f, dy: Float = 0f, shape: Shape = currentShape): Boolean {
        val newX = currentPosition.x + dx
        val newY = currentPosition.y + dy
        return shape.blocks.all { block ->
            val x = (newX + block.x).toInt()
            val y = (newY + block.y).toInt()
            x in 0 until GRID_WIDTH && y >= 0 && y < GRID_HEIGHT && grid.none { it.x == x && it.y == y }
        }
    }

    fun lockCurrentShape() {
        grid = grid + currentShape.blocks.map {
            Block(
                x = (currentPosition.x + it.x).toInt(),
                y = (currentPosition.y + it.y).toInt(),
                color = it.color
            )
        }
        val linesCleared = clearLines()
        linesClearedTotal += linesCleared
        score += when (linesCleared) {
            1 -> 100 * level
            2 -> 300 * level
            3 -> 500 * level
            4 -> 800 * level
            else -> 0
        }
        level = (linesClearedTotal / 10) + 1

        currentShape = nextShape
        nextShape = Shape.random()
        currentPosition = Offset(GRID_WIDTH / 2 - 1f, 0f)
        if (!isValidPosition()) {
            gameState = GameState.GAME_OVER
        }
    }

    fun rotate() {
        if (gameState != GameState.PLAYING || currentShape is Shape.O) return

        val rotatedShape = currentShape.rotate()

        val kicks = listOf(0f to 0f, -1f to 0f, 1f to 0f, 0f to -1f)
        for ((dx, dy) in kicks) {
            if (isValidPosition(dx = dx, dy = dy, shape = rotatedShape)) {
                currentShape = rotatedShape
                currentPosition = currentPosition.copy(x = currentPosition.x + dx, y = max(0f, currentPosition.y + dy))
                return
            }
        }
    }

    LaunchedEffect(gameState, dropDelay) {
        while (gameState == GameState.PLAYING) {
            delay(dropDelay)
            if (isValidPosition(dy = 1f)) {
                currentPosition = currentPosition.copy(y = currentPosition.y + 1f)
            } else {
                lockCurrentShape()
            }
        }
    }

    fun restart() {
        grid = emptyList()
        currentShape = Shape.random()
        nextShape = Shape.random()
        currentPosition = Offset(GRID_WIDTH / 2 - 1f, 0f)
        score = 0
        level = 1
        linesClearedTotal = 0
        gameState = GameState.PLAYING
    }

    fun endGame() {
        gameState = GameState.GAME_OVER
        onExit()
    }

    fun moveLeft() {
        if (gameState == GameState.PLAYING && isValidPosition(dx = -1f)) {
            currentPosition = currentPosition.copy(x = currentPosition.x - 1f)
        }
    }

    fun moveRight() {
        if (gameState == GameState.PLAYING && isValidPosition(dx = 1f)) {
            currentPosition = currentPosition.copy(x = currentPosition.x + 1f)
        }
    }

    fun moveDown() {
        if (gameState == GameState.PLAYING) {
            if (isValidPosition(dy = 1f)) {
                currentPosition = currentPosition.copy(y = currentPosition.y + 1f)
            } else {
                lockCurrentShape()
            }
        }
    }

    fun drop() {
        if (gameState == GameState.PLAYING) {
            while (isValidPosition(dy = 1f)) {
                currentPosition = currentPosition.copy(y = currentPosition.y + 1f)
            }
            lockCurrentShape()
        }
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(top = 48.dp, start = 8.dp, end = 8.dp, bottom = 8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text("得分: $score", style = MaterialTheme.typography.titleMedium)
            Text("等级: $level", style = MaterialTheme.typography.titleMedium)
            Text("行数: $linesClearedTotal", style = MaterialTheme.typography.titleMedium)
        }

        Spacer(Modifier.height(8.dp))

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.Top
        ) {
            Box {
                Canvas(
                    modifier = Modifier
                        .size(with(density) { CELL_SIZE * GRID_WIDTH }, with(density) { CELL_SIZE * GRID_HEIGHT })
                        .padding(4.dp)
                ) {
                    for (i in 0..GRID_WIDTH) {
                        drawLine(
                            color = Color.Gray.copy(alpha = 0.3f),
                            start = Offset(i * cellPx, 0f),
                            end = Offset(i * cellPx, size.height),
                            strokeWidth = 1f
                        )
                    }
                    for (j in 0..GRID_HEIGHT) {
                        drawLine(
                            color = Color.Gray.copy(alpha = 0.3f),
                            start = Offset(0f, j * cellPx),
                            end = Offset(size.width, j * cellPx),
                            strokeWidth = 1f
                        )
                    }

                    grid.forEach { block ->
                        drawRect(
                            color = block.color,
                            topLeft = Offset(block.x * cellPx, block.y * cellPx),
                            size = Size(cellPx, cellPx)
                        )
                        drawRect(
                            color = Color.Black,
                            topLeft = Offset(block.x * cellPx, block.y * cellPx),
                            size = Size(cellPx, cellPx),
                            style = Stroke(width = 1f)
                        )
                    }

                    if (gameState != GameState.GAME_OVER) {
                        currentShape.blocks.forEach { block ->
                            val x = (currentPosition.x + block.x) * cellPx
                            val y = (currentPosition.y + block.y) * cellPx
                            drawRect(
                                color = block.color,
                                topLeft = Offset(x, y),
                                size = Size(cellPx, cellPx)
                            )
                            drawRect(
                                color = Color.Black,
                                topLeft = Offset(x, y),
                                size = Size(cellPx, cellPx),
                                style = Stroke(width = 1f)
                            )
                        }
                    }
                }

                when (gameState) {
                    GameState.PAUSED -> {
                        Box(
                            modifier = Modifier
                                .matchParentSize()
                                .background(Color.Black.copy(alpha = 0.7f)),
                            contentAlignment = Alignment.Center
                        ) {
                            Text("暂停", color = Color.White, style = MaterialTheme.typography.headlineMedium)
                        }
                    }
                    GameState.GAME_OVER -> {
                        Box(
                            modifier = Modifier
                                .matchParentSize()
                                .background(Color.Black.copy(alpha = 0.7f)),
                            contentAlignment = Alignment.Center
                        ) {
                            Text("游戏结束", color = Color.White, style = MaterialTheme.typography.headlineMedium)
                        }
                    }
                    else -> {}
                }
            }

            Spacer(Modifier.width(16.dp))

            Column(
                modifier = Modifier.width(IntrinsicSize.Min),
                horizontalAlignment = Alignment.Start
            ) {
                Text("下一个:", style = MaterialTheme.typography.titleMedium)
                Spacer(Modifier.height(4.dp))
                Canvas(modifier = Modifier.size(with(density) { CELL_SIZE * PREVIEW_SIZE })) {
                    val offsetX = (PREVIEW_SIZE - nextShape.blocks.maxOfOrNull { it.x }!! - 1) / 2f
                    val offsetY = (PREVIEW_SIZE - nextShape.blocks.maxOfOrNull { it.y }!! - 1) / 2f
                    nextShape.blocks.forEach { block ->
                        val cx = (block.x + offsetX) * cellPx
                        val cy = (block.y + offsetY) * cellPx
                        drawRect(
                            color = block.color,
                            topLeft = Offset(cx, cy),
                            size = Size(cellPx, cellPx)
                        )
                        drawRect(
                            color = Color.Black,
                            topLeft = Offset(cx, cy),
                            size = Size(cellPx, cellPx),
                            style = Stroke(width = 1f)
                        )
                    }
                }
                Spacer(Modifier.height(8.dp))
                Row {
                    Button(
                        onClick = {
                            if (gameState == GameState.PLAYING) gameState = GameState.PAUSED
                            else if (gameState == GameState.PAUSED) gameState = GameState.PLAYING
                        },
                        enabled = gameState != GameState.GAME_OVER,
                        modifier = Modifier.weight(1f),
                        colors = ButtonDefaults.buttonColors(
                            contentColor = Color.Red
                        )
                    ) {
                        Text(if (gameState == GameState.PAUSED) "继续" else "暂停")
                    }
                    Spacer(Modifier.width(8.dp))
                    Button(
                        onClick = ::restart,
                        modifier = Modifier.weight(1f),
                        colors = ButtonDefaults.buttonColors(
                            contentColor = Color.Red
                        )
                    ) {
                        Text("重新开始")
                    }
                }
                Spacer(Modifier.height(8.dp))
                Button(
                    onClick = ::endGame,
                    enabled = gameState != GameState.GAME_OVER,
                    modifier = Modifier.fillMaxWidth(),
                    colors = ButtonDefaults.buttonColors(
                        contentColor = Color.Red
                        )
                ) {
                    Text("结束游戏")
                }
            }
        }

        Spacer(Modifier.height(16.dp))

        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Row(
                horizontalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                Button(
                    onClick = { moveLeft() },
                    enabled = gameState == GameState.PLAYING,
                    modifier = Modifier.size(80.dp, 50.dp)
                ) {
                    Text("左")
                }
                Button(
                    onClick = { moveDown() },
                    enabled = gameState == GameState.PLAYING,
                    modifier = Modifier.size(80.dp, 50.dp)
                ) {
                    Text("下")
                }
                Button(
                    onClick = { moveRight() },
                    enabled = gameState == GameState.PLAYING,
                    modifier = Modifier.size(80.dp, 50.dp)
                ) {
                    Text("右")
                }
            }

            Spacer(Modifier.height(16.dp))

            Row(
                horizontalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                Button(
                    onClick = { rotate() },
                    enabled = gameState == GameState.PLAYING,
                    modifier = Modifier.size(120.dp, 50.dp)
                ) {
                    Text("旋转")
                }
                Button(
                    onClick = { drop() },
                    enabled = gameState == GameState.PLAYING,
                    modifier = Modifier.size(120.dp, 50.dp)
                ) {
                    Text("DROP")
                }
            }
        }
    }

    if (gameState == GameState.GAME_OVER) {
        Dialog(onDismissRequest = {  }) {
            Surface(
                shape = MaterialTheme.shapes.medium,
                color = MaterialTheme.colorScheme.surface,
                tonalElevation = 6.dp
            ) {
                Column(
                    modifier = Modifier.padding(24.dp),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text("游戏结束", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall)
                    Spacer(Modifier.height(12.dp))
                    Text("你的得分:$score", style = MaterialTheme.typography.bodyMedium)
                    Text("等级:$level", style = MaterialTheme.typography.bodyMedium)
                    Text("消除行数:$linesClearedTotal", style = MaterialTheme.typography.bodyMedium)
                    Spacer(Modifier.height(20.dp))
                    Button(onClick = ::restart) {
                        Text("重新开始")
                    }
                }
            }
        }
    }
}

@Preview(showBackground = true, device = "spec:width=1080px,height=2400px,dpi=440")
@Composable
fun PreviewSetupScreen() {
    MaterialTheme {
        SetupScreen {}
    }
}

@Preview(showBackground = true, device = "spec:width=1080px,height=2400px,dpi=440")
@Composable
fun PreviewTetrisGame() {
    MaterialTheme {
        TetrisGameContent(Difficulty.MEDIUM.initialDropDelay, {})
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        var showSetup by mutableStateOf(true)
        var selectedDifficulty by mutableStateOf<Difficulty?>(null)

        val goToSetupScreen = {
            showSetup = true
            selectedDifficulty = null
        }

        setContent {
            MaterialTheme {
                if (showSetup) {
                    SetupScreen { difficulty ->
                        selectedDifficulty = difficulty
                        showSetup = false
                    }
                } else {
                    selectedDifficulty?.let { difficulty ->
                        TetrisGameContent(difficulty.initialDropDelay, onExit = goToSetupScreen)
                    } ?: run {
                        Text("Error: Difficulty not selected")
                    }
                }
            }
        }
    }
}

这段 Kotlin 代码使用 Jetpack Compose 框架实现了一个完整的俄罗斯方块(Tetris)游戏。

以下是代码逻辑的总结:

  1. 核心数据结构:

    • Block: 代表一个方块,包含其在网格中的坐标 (x, y) 和颜色 (color)。
    • Shape: 一个 sealed class,定义了七种不同的俄罗斯方块(I, O, T, L, J, S, Z)。每个形状由其构成的 Block 列表定义,并提供了旋转中心点和旋转方法。O 形是特殊的,它不能旋转。ShapeImpl 是一个私有类,用于处理旋转后形状的创建。
    • GameState: 枚举类型,表示游戏的三种状态:PLAYING(游戏中)、PAUSED(暂停)、GAME_OVER(游戏结束)。
    • Difficulty: 枚举类型,定义了三种难度(EASY, MEDIUM, HARD),主要通过初始的方块下落速度(initialDropDelay)来区分。
  2. UI 组件:

    • SetupScreen: 游戏开始前的设置界面,允许玩家选择难度(EASY, MEDIUM, HARD)并点击"开始游戏"按钮。
    • TetrisGameContent: 游戏的主界面,包含了游戏逻辑和 UI 渲染。
    • MainActivity: Android 的主活动,负责管理 SetupScreenTetrisGameContent 之间的切换。
  3. 游戏逻辑 (TetrisGameContent):

    • 状态管理 : 使用 remembermutableStateOf 来管理游戏状态,如游戏网格 (grid)、当前下落的方块 (currentShape) 及其位置 (currentPosition)、下一个方块 (nextShape)、得分 (score)、等级 (level)、总消除行数 (linesClearedTotal) 和游戏状态 (gameState)。
    • 游戏循环 : 使用 LaunchedEffect 启动一个协程,根据当前等级计算出的 dropDelay(方块自动下落的时间间隔)来驱动游戏。在 PLAYING 状态下,协程会定期检查方块是否可以向下移动,如果可以则移动,否则调用 lockCurrentShape
    • 移动与旋转 :
      • moveLeft, moveRight, moveDown: 处理左右移动和软降(手动下移)。它们会先检查移动后的位置是否有效(isValidPosition),有效则执行移动。
      • rotate: 处理方块旋转。对于非 O 形方块,它先计算旋转后的新形状,然后尝试在原位及几个偏移位置(踢墙)放置旋转后的方块,如果找到有效位置则执行旋转和可能的位移。
      • drop: 硬降(瞬间下落)。将当前方块尽可能地向下移动直到无法移动,然后锁定。
    • 碰撞检测 : isValidPosition 函数检查给定位置(或移动/旋转后的位置)的方块是否与游戏边界或已锁定的方块发生冲突。
    • 锁定与消行 :
      • lockCurrentShape: 当方块无法再下落时被调用。它将当前方块的所有 Block 添加到 grid 中。
      • clearLines: 检查 grid 中是否有满行,如果有则移除这些行,并将上方的方块下移相应行数。同时根据消除的行数计算得分并更新等级。
    • 得分与等级 : 得分基于消除的行数和当前等级。等级随着总消除行数的增加而提升,等级越高,方块下落速度越快(dropDelay 减小)。
    • 游戏控制 :
      • restart: 重置所有游戏状态,开始新游戏。
      • endGame: 将游戏状态设为 GAME_OVER 并返回设置界面。
      • 暂停/继续按钮可以切换 PLAYINGPAUSED 状态。
    • UI 渲染 :
      • 使用 Canvas 绘制游戏网格、已锁定的方块和当前下落的方块。
      • 绘制下一个方块的预览。
      • 显示得分、等级、行数。
      • 根据 gameState 显示"暂停"或"游戏结束"的覆盖层。
      • 游戏结束时弹出 Dialog 显示最终得分信息,并提供"重新开始"选项。
  4. 流程:

    • 应用启动后,首先显示 SetupScreen
    • 玩家选择难度并点击"开始游戏"。
    • MainActivity 切换到 TetrisGameContent,传入选定的难度对应的初始下落延迟。
    • TetrisGameContent 初始化游戏状态并启动游戏循环。
    • 玩家通过按钮控制方块移动、旋转、下落。
    • 游戏自动处理方块下落、碰撞检测、锁定、消行、得分计算和等级提升。
    • 当新方块无法在顶部中间位置生成时(即游戏区域被填满),游戏结束。
    • 游戏结束后显示 Dialog,玩家可以选择"重新开始"(调用 restart)或通过 Dialog 外部或"结束游戏"按钮返回设置界面。

总的来说,这是一个功能相对完整的俄罗斯方块游戏实现,涵盖了核心玩法、用户交互、状态管理和 UI 渲染。

效果图


相关推荐
阿巴斯甜15 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker15 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952716 小时前
Andorid Google 登录接入文档
android
黄林晴18 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android