package com.example.myels import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.example.myels.ui.theme.MyElsTheme import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.geometry.Offset import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Size import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.LocalDensity import androidx.compose.foundation.focusable import androidx.compose.ui.input.key.* import androidx.compose.runtime.* import kotlinx.coroutines.delay import android.view.KeyEvent import androidx.compose.foundation.layout.Row import androidx.compose.material3.Button import androidx.compose.ui.Alignment import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import android.util.Log // 新增Log导入 import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import android.media.RingtoneManager import android.media.Ringtone import android.net.Uri import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope import kotlin.coroutines.cancellation.CancellationException // 定义一个枚举类 Tetromino,用于表示俄罗斯方块的不同形状 enum class Tetromino(val color: Color) { I(Color.Cyan), // I型 O(Color.Yellow), // O型 T(Color.Magenta), // T型 L(Color(0xFFFFA500)), // 橙色L型 J(Color.Blue), // J型 S(Color.Green), // S型 Z(Color.Red) // Z型 } class GameState(private val context: android.content.Context) { // 当前正在下落的方块 var currentPiece by mutableStateOf(Tetromino.I) // 下一个将要下落的方块 var nextPiece by mutableStateOf(Tetromino.values().random()) // 新增下一个方块 // 当前方块的当前位置 var currentPosition by mutableStateOf(Offset(4f, 0f)) // 初始位置 // 游戏区域,20行10列,每个格子用Boolean表示是否被占用 val gameArea = mutableStateListOf<MutableList<Boolean>>().apply { repeat(20) { add(MutableList(10) { false }) } } // 游戏是否正在运行 var isRunning by mutableStateOf(true) // 当前方块的旋转状态 var currentRotation by mutableStateOf(0) // 游戏是否暂停 var isPaused by mutableStateOf(false) // 新增暂停状态 // 当前得分 var score by mutableStateOf(0) // 新增分数状态 // 历史最高分 var maxScore by mutableStateOf(0) init { // 从SharedPreferences加载历史最高分 val prefs = context.getSharedPreferences("game_prefs", Context.MODE_PRIVATE) maxScore = prefs.getInt("max_score", 0) } // 保存最高分到SharedPreferences // 这是一个私有函数,用于将当前的最高分保存到SharedPreferences中 private fun saveMaxScore() { // 获取SharedPreferences实例,名称为"game_prefs",模式为私有模式 // 私有模式表示只有该应用程序可以访问这些偏好设置 val prefs = context.getSharedPreferences("game_prefs", Context.MODE_PRIVATE) // 获取SharedPreferences的编辑器,用于进行数据的写入操作 // putInt方法用于将一个整数键值对存入SharedPreferences // 这里将键名为"max_score"的值设置为当前的最高分maxScore prefs.edit().putInt("max_score", maxScore).apply() } // 检查并更新最高分 fun checkAndUpdateHighScore() { // 如果当前得分大于最高分 if (score > maxScore) { // 更新最高分为当前得分 maxScore = score // 保存更新后的最高分 saveMaxScore() } } // 播放音效 fun playSound(soundType: Int) { // 根据传入的soundType参数,选择对应的音效 val ringtone = when (soundType) { 0 -> (context as MainActivity).getRotateSound() // 如果soundType为0,获取旋转音效 1 -> (context as MainActivity).getClearSound() // 如果soundType为1,获取清除音效 2 -> (context as MainActivity).getDropSound() // 如果soundType为2,获取掉落音效 else -> null // 如果soundType不是0、1或2,返回null } // 如果ringtone不为null,执行以下操作 ringtone?.run { stop() // 立即停止前次播放 play() // 播放当前选中的音效 } } // 重置游戏状态 fun reset() { // 随机选择一个新的当前方块 currentPiece = Tetromino.values().random() // 随机选择一个新的下一个方块 nextPiece = Tetromino.values().random() // 设置当前方块的初始位置为(4, 0) currentPosition = Offset(4f, 0f) // 遍历游戏区域,将所有单元格重置为false(表示没有方块) gameArea.forEach { row -> row.fill(false) } // 设置游戏状态为运行中 isRunning = true // 设置当前方块的初始旋转角度为0 currentRotation = 0 // 重置得分为0 score = 0 // 重置得分 } // 将当前方块固定到游戏区域 suspend fun lockPiece() { // 立即触发音效不等待后续逻辑 CoroutineScope(Dispatchers.Main).launch { playSound(2) }.invokeOnCompletion { // 添加协程取消检查 if (it is CancellationException) return@invokeOnCompletion } // 将后续操作包裹在协程作用域中 coroutineScope { // 将当前方块固定到游戏区域 synchronized(gameArea) { getPieceShape(currentPiece, currentRotation).forEach { (dx, dy) -> val y = (currentPosition.y + dy).toInt() // 添加数组边界检查 if (y in gameArea.indices) { val x = (currentPosition.x + dx).toInt() if (x in 0 until 10) { gameArea[y][x] = true } } } } val clearJob = launch(Dispatchers.Default) { clearLines() } clearJob.join() // 等待清行完成再生成新方块 spawnNewPiece() } } // 动画显示并清除一行 private suspend fun animateLineClear(y: Int) { // 使用withContext切换到主线程执行UI操作 withContext(Dispatchers.Main) { // 播放清除行的声音 playSound(1) // 添加安全访问检查,确保y在gameArea的有效范围内 if (y in gameArea.indices) { // 将指定行的所有单元格标记为已填充,模拟动画效果 gameArea[y].fill(true) // 延迟100毫秒,显示动画效果 delay(100) // 添加同步块和边界检查,确保在多线程环境下安全操作gameArea synchronized(gameArea) { // 再次检查y是否在gameArea的有效范围内 if (y in gameArea.indices) { // 从gameArea中移除指定行 gameArea.removeAt(y) } } } } } // 清除满行并更新得分 suspend fun clearLines() { // 找出所有满行的索引 val fullLines = gameArea.indices.filter { y -> y in gameArea.indices && gameArea[y].all { it } // 双重检查,确保y在gameArea的范围内,并且该行所有元素都为true } // 添加空检查和安全访问 if (fullLines.isEmpty()) return // 如果没有满行,直接返回 // 并行执行所有行的动画 try { coroutineScope { // 反向遍历满行索引,确保从底部开始清除 fullLines.reversed().forEach { y -> launch(Dispatchers.Main) { // 指定调度器为Main,确保在主线程执行动画 animateLineClear(y) // 执行清除行的动画 } } } } catch (e: CancellationException) { // 处理协程取消 throw e // 重新抛出取消异常 } catch (e: Exception) { Log.e("ClearLines", "Error clearing lines", e) // 记录错误日志 } // 根据清除的行数更新得分 when (fullLines.size) { 1 -> score += 100 // 清除1行,得分加100 2 -> score += 300 // 清除2行,得分加300 3 -> score += 500 // 清除3行,得分加500 4 -> score += 1200 // 清除4行,得分加1200 } // 添加线程安全操作 synchronized(gameArea) { // 根据清除的行数,在游戏区域顶部添加相应数量的空行 repeat(fullLines.size) { gameArea.add(0, MutableList(10) { false }) // 添加一个包含10个false的空行 } } } // 生成新的方块 fun spawnNewPiece() { currentPiece = nextPiece // 使用预存的nextPiece,将其设置为当前方块 nextPiece = Tetromino.values().random() // 生成新的下一个方块,随机选择一个Tetromino类型的方块 currentPosition = Offset(4f, -getSpawnOffset(currentPiece)) // 根据方块类型调整生成位置 currentRotation = 0 // 添加调试日志 Log.d("Spawn", "New ${currentPiece} at $currentPosition") if (!canSpawn(currentPiece, currentPosition)) { Log.e("GameOver", "Cannot spawn new piece") isRunning = false checkAndUpdateHighScore() // 新增分数检查 } } // 获取方块的初始偏移量 // 这个函数用于根据传入的方块类型(Tetromino)返回初始偏移量 private fun getSpawnOffset(piece: Tetromino): Float = when (piece) { Tetromino.I -> 1f // I型需要额外上移 else -> 0f } // 检查新方块是否可以生成 private fun canSpawn(piece: Tetromino, position: Offset): Boolean { // 获取当前方块形状和旋转状态 getPieceShape(piece, currentRotation).forEach { (dx, dy) -> // 计算方块在游戏区域中的实际坐标 val x = (position.x + dx).toInt() val y = (position.y + dy).toInt() // 修改碰撞检测逻辑 // 检查x坐标是否在游戏区域的宽度范围内 if (x !in 0 until 10) return false // 检查y坐标是否超出游戏区域的高度范围 if (y >= 20) return false // 允许y为负值(暂存区) // 检查y坐标是否在有效区域内且该位置是否已被占用 if (y >= 0 && gameArea[y][x]) return false // 仅检查有效区域 } // 如果所有检查都通过,则返回true,表示可以生成新方块 return true } } // 添加扩展函数帮助删除行 // 定义一个扩展函数dropAt,用于删除数组中的指定行 // 这个函数是私有的,只能在定义它的文件中访问 private fun Array<Array<Boolean>>.dropAt(index: Int) { // 使用for循环,从指定的index开始向下遍历到1 // 这里的downTo表示从大到小的遍历 for (i in index downTo 1) { // 将当前行的数据复制到下一行 // 使用copyOf()方法创建一个新数组,内容与当前行相同 this[i] = this[i - 1].copyOf() } // 将数组的第0行初始化为全为false的新数组 // Array(this[0].size) { false } 创建一个与原第0行大小相同的新数组,所有元素都为false this[0] = Array(this[0].size) { false } } class MainActivity : ComponentActivity() { // 定义旋转音效变量 private var rotateRingtone: Ringtone? = null // 定义清除音效变量 private var clearRingtone: Ringtone? = null // 定义下落音效变量 private var dropSound: Ringtone? = null // 新增下落音效 // 获取旋转音效的方法 fun getRotateSound() = rotateRingtone // 获取清除音效的方法 fun getClearSound() = clearRingtone // 获取下落音效的方法 fun getDropSound() = dropSound // 重写onCreate方法,在Activity创建时调用 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 初始化系统音效 // 获取旋转音效 rotateRingtone = RingtoneManager.getRingtone( this, Uri.parse("android.resource://${packageName}/${R.raw.xuanzhuan}") ) // 获取清除音效 clearRingtone = RingtoneManager.getRingtone( this, Uri.parse("android.resource://${packageName}/${R.raw.xiaohang}") // 请确保有clear.mp3 ) // 获取下落音效 dropSound = RingtoneManager.getRingtone( this, Uri.parse("android.resource://${packageName}/${R.raw.xialuo}") // 请确保有drop.mp3 ) // 启用边缘到边缘的布局 enableEdgeToEdge() // 设置内容视图 setContent { MyElsTheme { TetrisGameScreen() } } } // 添加销毁时释放资源 override fun onDestroy() { // 调用父类的onDestroy方法,确保父类的销毁逻辑被执行 super.onDestroy() // 如果rotateRingtone对象不为空,则调用其stop方法停止播放 rotateRingtone?.stop() // 如果clearRingtone对象不为空,则调用其stop方法停止播放 clearRingtone?.stop() // 如果dropSound对象不为空,则调用其stop方法停止播放 dropSound?.stop() } } // 新增游戏主界面组件 @Composable fun TetrisGameScreen() { // 游戏区域尺寸(20行 x 10列) val gridRows = 20 val gridColumns = 10 val cellSize = 24.dp val context = LocalContext.current val gameState = remember { GameState(context) } Box(modifier = Modifier.fillMaxSize().background(Color.Black)) { // 游戏主面板 GameBoard( rows = 20, columns = 10, cellSize = cellSize, gameState = gameState ) Text( text = "得分: ${gameState.score}", color = Color.White, modifier = Modifier .align(Alignment.TopEnd) // 改为右上对齐 .padding(top = 32.dp, end = 32.dp) // 增加右侧间距 ) Column( modifier = Modifier .align(Alignment.TopEnd) // 从 CenterEnd 改为 TopEnd .padding(top = 64.dp) // 增加顶部间距避免与得分重叠 ) { // 新增下一个方块预览面板 NextPiecePreview( modifier = Modifier .align(Alignment.Start) // 从 End 改为 Start .padding(start = 16.dp, bottom = 16.dp) // 新增左侧间距 .offset(x = (250).dp), // 新增向左偏移 cellSize = 16.dp, nextPiece = gameState.nextPiece ) // 控制按钮组(预览面板下方) Spacer(modifier = Modifier.height(48.dp)) // 新增间距 Column( horizontalAlignment = Alignment.End, // 从 CenterHorizontally 改为 End modifier = Modifier.fillMaxWidth() ){ Button( onClick = { gameState.reset() }, modifier = Modifier .size(width = 90.dp, height = 40.dp) // 改为长条形尺寸 .padding(end = 8.dp), // 右侧微调 shape = RoundedCornerShape(8.dp) // 替换 CircleShape ) { Text("重开") } Spacer(modifier = Modifier.height(16.dp)) // 在重开按钮后添加暂停按钮 Button( onClick = { gameState.isPaused = !gameState.isPaused }, modifier = Modifier .size(width = 90.dp, height = 40.dp) // 统一尺寸 .padding(end = 8.dp), shape = RoundedCornerShape(8.dp) ) { Text(if (gameState.isPaused) "继续" else "暂停") } Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp)) Text( text = "历史最高: ", color = Color.White.copy(alpha = 0.7f), modifier = Modifier.padding(top = 8.dp, end = 8.dp) ) Text( text = "${gameState.maxScore}", color = Color.White.copy(alpha = 0.7f), modifier = Modifier.padding(top = 8.dp, end = 8.dp) ) } } Box( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 16.dp) ){ // 速降按钮 Button( onClick = { while (gameState.canMoveTo(gameState.currentPiece, gameState.currentPosition + Offset(0f, 1f))) { gameState.currentPosition += Offset(0f, 1f) } }, modifier = Modifier .size(64.dp) .align(Alignment.BottomStart) .offset(x = 50.dp,y=(-54).dp), // 新增向左偏移, shape = CircleShape ) { Text("↓") } Row( modifier = Modifier .align(Alignment.TopCenter) .padding(bottom = 100.dp), horizontalArrangement = Arrangement.spacedBy(32.dp) ) { Button( onClick = { if (gameState.canMoveTo(gameState.currentPiece, gameState.currentPosition + Offset(-1f, 0f))) { gameState.currentPosition += Offset(-1f, 0f) } }, modifier = Modifier.size(64.dp), shape = CircleShape ) { Text("←") } Button( onClick = { if (gameState.canMoveTo(gameState.currentPiece, gameState.currentPosition + Offset(1f, 0f))) { gameState.currentPosition += Offset(1f, 0f) } }, modifier = Modifier.size(64.dp), shape = CircleShape ) { Text("→") } // 旋转按钮 Button( onClick = { // 在主线程立即播放音效 CoroutineScope(Dispatchers.Main).launch { gameState.playSound(0) } val newRotation = (gameState.currentRotation + 1) % 4 val adjustments = listOf(0 to 0, 1 to 0, -1 to 0, 2 to 0, -2 to 0) adjustments.forEach { (dx, dy) -> val newPos = gameState.currentPosition + Offset(dx.toFloat(), dy.toFloat()) if (gameState.canMoveTo(gameState.currentPiece, newPos, newRotation)) { gameState.currentPosition = newPos gameState.currentRotation = newRotation return@forEach } } }, modifier = Modifier.size(96.dp), shape = CircleShape ) { Text("↻") } } } } } // 新增下一个方块预览组件 @Composable fun NextPiecePreview(modifier: Modifier, cellSize: Dp, nextPiece: Tetromino) { // 使用Box来包裹Canvas,以便应用修饰符 Box(modifier = modifier) { // 创建一个Canvas,用于绘制下一个方块的预览 Canvas( modifier = Modifier .size(cellSize * 4, cellSize * 4) // 设置Canvas的大小为4个单元格的大小 .background(Color.Black.copy(alpha = 0.7f)) // 设置背景颜色为半透明黑色 ) { // 计算居中偏移量 val centerOffset = Offset(2f, 2f) // 居中显示 // 调用drawTetromino函数绘制下一个方块 drawTetromino( tetromino = nextPiece, // 要绘制的方块 position = centerOffset, // 方块的位置 cellSize = cellSize.toPx(), // 单元格的大小转换为像素 rotation = 0 // 方块的旋转角度为0 ) } } } // 新增游戏主面板组件 @Composable fun GameBoard(rows: Int, columns: Int, cellSize: Dp,gameState: GameState) { //val gameState = remember { GameState() } // 添加性能监控代码 val frameTime = remember { mutableStateOf(0L) } LaunchedEffect(Unit) { while (true) { val start = System.currentTimeMillis() delay(16) // 60FPS frameTime.value = System.currentTimeMillis() - start if (frameTime.value > 30) { Log.w("PerfWarning", "Frame drop detected: ${frameTime.value}ms") } } } val density = LocalDensity.current val cellSizePx = with(density) { cellSize.toPx() } // 正确的单位转换方式 val focusRequester = remember { FocusRequester() } // 新增焦点请求器 val coroutineScope = rememberCoroutineScope() LaunchedEffect(gameState.isRunning) { if (gameState.isRunning&& !gameState.isPaused) { coroutineScope.launch(Dispatchers.Default) { while (gameState.isRunning) { delay(500) if (!gameState.isPaused) { withContext(Dispatchers.Main) { with(gameState) { if (canMoveTo(currentPiece, currentPosition + Offset(0f, 1f))) { currentPosition += Offset(0f, 1f) } else { lockPiece() //spawnNewPiece() } } } } } } } } /* LaunchedEffect(Unit) { focusRequester.requestFocus() // 自动请求焦点 }*/ Canvas( modifier = Modifier .size(width = cellSize * columns, height = cellSize * rows) .padding(16.dp) .offset(y = 32.dp) // 新增垂直偏移 .focusable() // 添加焦点支持 .focusRequester(focusRequester) // 绑定焦点请求器 .keyboardInput { direction -> // 添加移动逻辑 coroutineScope.launch(Dispatchers.Default) { handleInput(direction, gameState) } } ) { // 绘制网格线 drawGrid(rows, columns, cellSizePx) // 新增:绘制已固定的方块 gameState.gameArea.forEachIndexed { y, row -> row.forEachIndexed { x, occupied -> if (occupied) { drawRect( color = Color.Gray, // 保持固定方块为灰色 topLeft = Offset(x * cellSizePx, y * cellSizePx), size = Size(cellSizePx, cellSizePx) ) } } } // 绘制当前方块 drawTetromino( tetromino = gameState.currentPiece, position = gameState.currentPosition, cellSize = cellSizePx, rotation = gameState.currentRotation ) } } // 定义一个私有挂起函数 handleInput,用于处理用户输入 private suspend fun handleInput(direction: Int, gameState: GameState) { // 根据输入的方向值进行不同的处理 when (direction) { 0 -> { val newRotation = (gameState.currentRotation + 1) % 4 // 添加旋转后的位置微调逻辑 val adjustments = listOf(0 to 0, 1 to 0, -1 to 0, 2 to 0, -2 to 0) for ((dx, dy) in adjustments) { val newPos = gameState.currentPosition + Offset(dx.toFloat(), dy.toFloat()) if (gameState.canMoveTo(gameState.currentPiece, newPos, newRotation)) { gameState.currentPosition = newPos gameState.currentRotation = newRotation break } } } -1, 1 -> withContext(Dispatchers.Main) { gameState.currentPosition += Offset(direction.toFloat(), 0f) } 2 -> { while (gameState.canMoveTo(gameState.currentPiece, gameState.currentPosition + Offset(0f, 1f))) { gameState.currentPosition += Offset(0f, 1f) } } } } // 新增网格绘制扩展函数 private fun DrawScope.drawGrid(rows: Int, columns: Int, cellSize: Float) { // 绘制垂直网格线 // 使用repeat函数循环绘制垂直网格线,循环次数为列数+1(包括起始线) repeat(columns + 1) { i -> // 调用drawLine函数绘制一条垂直线 drawLine( // 设置线的颜色为灰色,透明度为1(完全不透明) color = Color.Gray.copy(alpha = 1f), // 设置线的起始点,x坐标为当前列数乘以单元格大小,y坐标为0 start = Offset(x = i * cellSize, y = 0f), // 设置线的终点,x坐标与起始点相同,y坐标为行数乘以单元格大小 end = Offset(x = i * cellSize, y = rows * cellSize), // 设置线的宽度为1 strokeWidth = 1f ) } // 绘制水平网格线 // 使用repeat函数循环绘制水平网格线,循环次数为行数+1(包括起始线) repeat(rows + 1) { i -> // 调用drawLine函数绘制一条水平线 drawLine( // 设置线的颜色为灰色,透明度为1(完全不透明) color = Color.Gray.copy(alpha = 1f), // 设置线的起始点,x坐标为0,y坐标为当前行数乘以单元格大小 start = Offset(x = 0f, y = i * cellSize), // 设置线的终点,x坐标为列数乘以单元格大小,y坐标与起始点相同 end = Offset(x = columns * cellSize, y = i * cellSize), // 设置线的宽度为1 strokeWidth = 1f ) } } private fun DrawScope.drawTetromino(tetromino: Tetromino, position: Offset, cellSize: Float,rotation: Int) { val shape = getPieceShape(tetromino,rotation) // 使用新的形状获取方法 shape.forEach { (dx, dy) -> drawRect( brush = Brush.linearGradient( colors = listOf(tetromino.color, tetromino.color.copy(alpha = 0.7f)) ), topLeft = Offset( x = (position.x + dx) * cellSize, y = (position.y + dy) * cellSize ), size = Size(cellSize, cellSize) ) } } // 新增碰撞检测方法 // 该方法用于检测给定的俄罗斯方块(Tetromino)在新的位置(newPosition)和旋转角度(rotation)下是否可以移动 private fun GameState.canMoveTo(piece: Tetromino, newPosition: Offset,rotation: Int = currentRotation): Boolean { // 获取当前旋转角度下的方块形状 getPieceShape(piece, rotation).forEach { (dx, dy) -> // 计算方块在新位置上的具体坐标 val x = (newPosition.x + dx).toInt() val y = (newPosition.y + dy).toInt() // 检查新的坐标是否超出游戏区域 // x !in 0 until 10 表示 x 超出游戏区域的宽度范围 // y >= 20 表示 y 超出游戏区域的高度范围 // (y >= 0 && gameArea[y][x]) 表示新的坐标位置已经有其他方块存在 if (x !in 0 until 10 || y >= 20 || (y >= 0 && gameArea[y][x])) return false } // 如果所有坐标都未超出游戏区域且没有与其他方块重叠,则返回 true,表示可以移动 return true } // 添加在 GameState 类下方 private fun getPieceShape(piece: Tetromino, rotation: Int = 0): List<Pair<Int, Int>> { return when (piece) { // 添加 O 型方块(2x2 正方形) Tetromino.O -> listOf(0 to 0, 1 to 0, 0 to 1, 1 to 1) // I 型方块(中心点偏移补偿) Tetromino.I -> when (rotation % 2) { 0 -> listOf(-2 to 1, -1 to 1, 0 to 1, 1 to 1) // 水平(中心点 ( -0.5, 0 )) 1 -> listOf(0 to -1, 0 to 0, 0 to 1, 0 to 2) // 垂直(中心点 ( 0, -0.5 )) else -> emptyList() } // T 型方块(中心点 (0,0)) Tetromino.T -> when (rotation % 4) { 0 -> listOf(-1 to 1, 0 to 1, 1 to 1, 0 to 0) // 朝上 1 -> listOf(0 to -1, 0 to 0, 0 to 1, 1 to 0) // 朝右 2 -> listOf(-1 to 0, 0 to 0, 1 to 0, 0 to 1) // 朝下 3 -> listOf(0 to -1, 0 to 0, 0 to 1, -1 to 0) // 朝左 else -> emptyList() } // L 型方块(中心点 (0.5, 0.5)) Tetromino.L -> when (rotation % 4) { 0 -> listOf(-1 to 0, -1 to 1, 0 to 1, 1 to 1) // 直立 1 -> listOf(0 to -1, 1 to -1, 0 to 0, 0 to 1) // 右转 2 -> listOf(-1 to 0, 0 to 0, 1 to 0, 1 to 1) // 倒立 3 -> listOf(0 to -1, 0 to 0, -1 to 1, 0 to 1) // 左转 else -> emptyList() } // J 型方块(中心点 (0.5, 0.5)) Tetromino.J -> when (rotation % 4) { 0 -> listOf(-1 to 1, 0 to 1, 1 to 1, 1 to 0) // 直立 1 -> listOf(0 to -1, 0 to 0, 0 to 1, 1 to 1) // 右转 2 -> listOf(-1 to 0, -1 to 1, 0 to 0, 1 to 0) // 倒立 3 -> listOf(-1 to -1, 0 to -1, 0 to 0, 0 to 1) // 左转 else -> emptyList() } // S 型方块(中心点 (0.5, 0.5)) Tetromino.S -> when (rotation % 2) { 0 -> listOf(-1 to 0, 0 to 0, 0 to 1, 1 to 1) // 横放 1 -> listOf(0 to -1, 0 to 0, -1 to 0, -1 to 1) // 竖放 else -> emptyList() } // Z 型方块(中心点 (0.5, 0.5)) Tetromino.Z -> when (rotation % 2) { 0 -> listOf(-1 to 1, 0 to 1, 0 to 0, 1 to 0) // 横放 1 -> listOf(0 to -1, 0 to 0, 1 to 0, 1 to 1) // 竖放 else -> emptyList() } else -> emptyList() } } // 新增键盘事件处理 // 定义一个扩展函数 keyboardInput,用于处理键盘输入事件 // 参数 onDirection 是一个函数类型,接受一个 Int 参数,用于处理方向键事件 private fun Modifier.keyboardInput(onDirection: (Int) -> Unit) = this.then( // 使用 then 函数将当前 Modifier 与一个新的 Modifier 连接起来 // 新的 Modifier 用于处理键盘事件 Modifier.onKeyEvent { keyEvent -> // 当键盘事件发生时,根据按键的 keyCode 执行相应的操作 when (keyEvent.nativeKeyEvent.keyCode) { // 如果按键是左方向键 KeyEvent.KEYCODE_DPAD_LEFT -> onDirection(-1) // 如果按键是右方向键 KeyEvent.KEYCODE_DPAD_RIGHT -> onDirection(1) // 如果按键是上方向键 KeyEvent.KEYCODE_DPAD_UP -> onDirection(0) // 如果按键是下方向键 KeyEvent.KEYCODE_DPAD_DOWN -> onDirection(2) } // 返回 true 表示事件已被处理 true })