俄罗斯方块是一款经典的休闲游戏,具有简单的规则、丰富的变化和高度的可玩性。

游戏说明

游戏目标非常简单直接,就是尽量长时间地维持游戏进行,通过安排不断下落的各种形状的方块(由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…

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。