俄罗斯方块是一款经典的休闲游戏,具有简单的规则、丰富的变化和高度的可玩性。
游戏说明
游戏目标非常简单直接,就是尽量长时间地维持游戏进行,通过安排不断下落的各种形状的方块(由4个小正方形组成的规则图形,包括S形、Z形、L形、I形、O形、T形等),使它们在游戏区域的底部堆叠起来排列成完整的一行或多行,消除这些行并获得分数。
下面我们使用Android原生控件来实现这个小游戏(PS:不包含自定义View的方式)
实现思路游戏场景
俄罗斯方块游戏场景包括游戏板和方块,因为使用Android原生控件实现游戏,所以游戏板我们可以用GridLayout来绘制背景方格
GridLayout
android:id="@+id/gameBoard"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:columnCount="20"
android:rowCount="20" />
游戏背景放置好之后就是往背景上加入方块,创建Piece类来表示方块每个Piece包含形状shape。
data class Piece(var x: Int, var y: Int, val shape: Array) {
fun rotate(): Piece {
val rotated = Array(shape[0].size) { IntArray(shape.size) }
for (i in shape.indices) {
for (j in shape[i].indices) {
rotated[j][shape.size - 1 - i] = shape[i][j]
}
}
return Piece(x, y, rotated)
}
companion object {
private val shapes = listOf(
arrayOf(intArrayOf(1, 1, 1, 1)), // I
arrayOf(intArrayOf(1, 1), intArrayOf(1, 1)), // O
arrayOf(intArrayOf(1, 1, 1), intArrayOf(0, 1, 0)), // T
arrayOf(intArrayOf(1, 1, 0), intArrayOf(0, 1, 1)), // Z
arrayOf(intArrayOf(0, 1, 1), intArrayOf(1, 1, 0)), // S
arrayOf(intArrayOf(1, 1, 1), intArrayOf(1, 0, 0)), // L
arrayOf(intArrayOf(1, 1, 1), intArrayOf(0, 0, 1)) // J
)
fun random(): Piece {
val shape = shapes[Random.nextInt(shapes.size)]
return Piece(10 / 2 - shape[0].size / 2, 0, shape)
}
}
}
方块实现了rotate旋转方法用于在游戏中旋转方块,同时添加一个随机生成方块的random方法
游戏主循环
使用Android自带的Handler和Runnable创建游戏循环,在循环中处理方块下落、放置、生成新方块、游戏结束判断等逻辑,同时可以控制游戏速度,随着等级提高而加快。
private val gameLoop = object : Runnable {
override fun run() {
if (isGameRunning) {
if (!movePiece(0, 1)) {
placePiece()
clearLines()
if (!spawnNewPiece()) {
gameOver()
return
}
}
updateBoardUI()
handler.postDelayed(this, 800)
}
}
}
gameLoop是一个Runnable对象,用于定期执行游戏逻辑(800ms刷新一次视图),要想游戏速度加快,修改该数值即可,进而实现随着等级提高而加快
方块操作
方块有左移,右移,旋转,下落四种操作,我们定义三个方法响应相应的操作
private fun movePiece(dx: Int, dy: Int): Boolean {
val piece = currentPiece ?: return false
if (canPlacePiece(piece, dx, dy)) {
drawPiece(piece, Color.BLACK)
piece.x += dx
piece.y += dy
drawPiece(piece, Color.CYAN)
return true
}
return false
}
private fun rotatePiece() {
val piece = currentPiece ?: return
val rotatedPiece = piece.rotate()
if (canPlacePiece(rotatedPiece, 0, 0)) {
currentPiece = rotatedPiece
updateBoardUI()
}
}
private fun dropPiece() {
var dropDistance = 0
while (movePiece(0, 1)) {
dropDistance++
}
score += dropDistance * 2
updateScore()
}
下落的方法也是调用的movePiece移动方块,增加y值即为下落
碰撞检测
在移动和旋转操作中不断地进行碰撞检测
private fun canPlacePiece(piece: Piece, dx: Int, dy: Int): Boolean {
for (i in piece.shape.indices) {
for (j in piece.shape[i].indices) {
if (piece.shape[i][j] == 1) {
val newX = piece.x + j + dx
val newY = piece.y + i + dy
if (newX 0 || newX >= boardWidth || newY >= boardHeight || (newY >= 0 && board[newY][newX] == 1)) {
return false
}
}
}
}
return true
}
canPlacePiece方法检测是否可以放置方块,碰撞检测的原理如下:
遍历当前方块的每个格子:只检查方块中值为1的格子(实际占用的格子):计算方块在游戏板上的新位置:检查是否发生碰撞,有四种情况:如果任何一个格子发生碰撞,立即返回 false如果所有格子都没有碰撞,返回 true方块放置和行清除
private fun placePiece() {
val piece = currentPiece ?: return
for (i in piece.shape.indices) {
for (j in piece.shape[i].indices) {
if (piece.shape[i][j] == 1) {
val y = piece.y + i
val x = piece.x + j
if (y in 0..= 0 && x 1
}
}
}
}
updateBoardUI()
}
private fun clearLines() {
var linesCleared = 0
for (i in boardHeight - 1 downTo 0) {
if (board[i].all { it == 1 }) {
for (j in i downTo 1) {
board[j] = board[j - 1].clone()
}
board[0] = IntArray(boardWidth)
linesCleared++
}
}
if (linesCleared > 0) {
val baseScore = when (linesCleared) {
1 -> 100
2 -> 300
3 -> 500
4 -> 800
else -> 0
}
combo++
val comboBonus = combo * 50
score += (baseScore + comboBonus) * level
this.linesCleared += linesCleared
updateLevel()
updateScore()
} else {
combo = 0
}
}
placePiece方法将当前方块固定到游戏板上,clearLines方法检查并清除已填满的行,并且在清除行时更新分数和等级。
下一个方块预览
在布局中添加一个用于预览的GridLayout用于显示预览下一个方块的网格
GridLayout
android:id="@+id/nextPieceBoard"
android:layout_width="60dp"
android:layout_height="60dp"
android:columnCount="4"
android:rowCount="4" />
生成新方块的时候随机生成下一个方块显示在预览网格中
private fun spawnNewPiece(): Boolean {
currentPiece = nextPiece ?: Piece.random()
nextPiece = Piece.random()
updateNextPieceUI()
if (canPlacePiece(currentPiece!!, 0, 0)) {
updateBoardUI()
return true
}
return false
}
private fun updateNextPieceUI() {
for (i in 0 until 4) {
for (j in 0 until 4) {
val cell = nextPieceBoard.getChildAt(i * 4 + j)
cell.setBackgroundColor(Color.BLACK)
}
}
nextPiece?.let { piece ->
for (i in piece.shape.indices) {
for (j in piece.shape[i].indices) {
if (piece.shape[i][j] == 1) {
val cell = nextPieceBoard.getChildAt(i * 4 + j)
cell.setBackgroundColor(Color.CYAN)
}
}
}
}
}
updateNextPieceUI方法来显示下一个方块的预览
游戏界面更新
private fun updateBoardUI() {
for (i in 0 until boardHeight) {
for (j in 0 until boardWidth) {
val cell = gameBoard.getChildAt(i * boardWidth + j)
cell.setBackgroundColor(if (board[i][j] == 0) Color.BLACK else Color.MAGENTA)
}
}
currentPiece?.let { drawPiece(it, Color.MAGENTA) }
}
在游戏主线程及其它操作时不提你调用updateBoardUI方法刷新游戏界面
游戏结束处理
在无法生成新方块时触发游戏结束
private fun gameOver() {
isGameRunning = false
gameOverLayout.visibility = View.VISIBLE
restartButton.visibility = View.VISIBLE
}
处理游戏结束逻辑,显示重新开始按钮
完整代码
源码/Reathin/Sam…