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 渲染。

效果图


相关推荐
William_cl2 小时前
【连载1】《假装自己是个小白 —— 重新认识 MySQL》实践指南
android·mysql·oracle
一直向钱3 小时前
android 字符串工具类(兼容 Android 16+ / API 16,无报错版)
android
猪哥帅过吴彦祖4 小时前
Flutter 系列教程:常用基础组件 (上) - `Text`, `Image`, `Icon`, `Button`
android·flutter·ios
恋猫de小郭4 小时前
Fluttercon EU 2025 :Let's go far with Flutter
android·前端·flutter
诺诺Okami5 小时前
Android Framework- AMS 之 Activity-暂停
android
2501_916013745 小时前
App 上架服务全流程解析,iOS 应用代上架、ipa 文件上传工具、TestFlight 测试与苹果审核实战经验
android·ios·小程序·https·uni-app·iphone·webview
建群新人小猿6 小时前
客户标签自动管理:标签自动化运营,画像持久保鲜
android·java·大数据·前端·git
一直向钱7 小时前
android 自定义样式 Toast 实现(兼容 Android 4.1+~Android 16(API 16))
android