欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

利用Jetpack Compose實(shí)現(xiàn)經(jīng)典俄羅斯方塊游戲

 更新時(shí)間:2022年05月18日 10:23:02   作者:業(yè)志陳  
你的童年是否有俄羅斯方塊呢,本文就來(lái)介紹如何通過(guò)Jetpack Compose實(shí)現(xiàn)一個(gè)俄羅斯方塊!感興趣的小伙伴快跟隨小編一起動(dòng)手嘗試一下吧

你的童年是否有俄羅斯方塊呢,本文就來(lái)介紹如何通過(guò) Jetpack Compose 實(shí)現(xiàn)一個(gè)俄羅斯方塊 ~~

先看下效果圖,功能還是挺完善的

就我自己的體驗(yàn)來(lái)說(shuō),使用 Compose 開(kāi)發(fā)的應(yīng)用我感受不到和 Android 原生開(kāi)發(fā)之間有什么性能差異,但 Compose 在開(kāi)發(fā)難度上會(huì)低很多

Google 官網(wǎng)上是這么介紹 Compose 的:Jetpack Compose 是用于構(gòu)建原生界面的新款 Android 工具包,它可簡(jiǎn)化并加快 Android 上的界面開(kāi)發(fā),使用更少的代碼、強(qiáng)大的工具和直觀的 Kotlin API,快速讓?xiě)?yīng)用生動(dòng)而精彩

長(zhǎng)期以來(lái),Android 的視圖層次結(jié)構(gòu)可以表示為一個(gè)視圖樹(shù),視圖樹(shù)中包含著若干個(gè) View 和 ViewGroup。當(dāng)應(yīng)用的數(shù)據(jù)由于用戶(hù)交互等原因而發(fā)生變化時(shí),界面的層次結(jié)構(gòu)就需要進(jìn)行更新以顯示最新數(shù)據(jù)。最常見(jiàn)的界面更新方式就是使用findViewById()等函數(shù)遍歷視圖樹(shù),并通過(guò)調(diào)用 button.setText(String)、container.addChild(View)img.setImageBitmap(Bitmap) 等方法來(lái)改變特定節(jié)點(diǎn),而這些方法就會(huì)改變 View 的內(nèi)部狀態(tài)。但這種手動(dòng)操縱視圖的方式提高了出錯(cuò)的可能性。如果一條數(shù)據(jù)需要在多個(gè)位置呈現(xiàn),開(kāi)發(fā)者可能一不小心就會(huì)忘記更新某個(gè)顯示它的視圖。此外,當(dāng)兩項(xiàng)更新以意外的方式發(fā)生沖突時(shí),也很容易造成異常狀態(tài)。例如,某項(xiàng)更新可能會(huì)嘗試修改剛剛從界面中移除的節(jié)點(diǎn)。一般來(lái)說(shuō),軟件維護(hù)復(fù)雜性會(huì)隨著需要更新的視圖數(shù)量增多而增長(zhǎng)

在過(guò)去的幾年中,整個(gè)行業(yè)已開(kāi)始轉(zhuǎn)向聲明性界面模型,該模型大大簡(jiǎn)化了與構(gòu)建和更新界面關(guān)聯(lián)的工程設(shè)計(jì)。該技術(shù)的工作原理是在概念上從頭開(kāi)始重新生成整個(gè)屏幕,然后僅執(zhí)行必要的更改。此方法可避免手動(dòng)更新有狀態(tài)視圖層次結(jié)構(gòu)的復(fù)雜性。Compose 就是一個(gè)適用于 Android 的新式聲明性界面工具包,提供了聲明性 API,讓開(kāi)發(fā)者可在不以命令方式改變前端視圖的情況下呈現(xiàn)應(yīng)用界面,從而使編寫(xiě)和維護(hù)應(yīng)用界面變得更加容易

可組合函數(shù)

Compose 的重點(diǎn)就在于 @Composable函數(shù),即可組合函數(shù),每個(gè)可組合函數(shù)可以接收若干入?yún)?shù)用于參與視圖結(jié)構(gòu)的繪制說(shuō)明,但函數(shù)不返回任何值。可組合函數(shù)只用于描述視圖結(jié)構(gòu)如何繪制以及如何與用戶(hù)進(jìn)行交互,但不需要返回視圖對(duì)象,而是由 Compose 根據(jù)開(kāi)發(fā)者的描述來(lái)生成具體的視圖對(duì)象

本游戲的 icon 就是通過(guò)這種方式來(lái)生成的。可以看到 PreviewTetrisIcon() 函數(shù)并不包含返回值,當(dāng)然這種情況下也不需要入?yún)?shù)。此外,Compose 的一個(gè)優(yōu)點(diǎn)就是所見(jiàn)即所得,通過(guò)添加 @Preview 注解就可以預(yù)覽實(shí)現(xiàn)效果,每次修改過(guò)后無(wú)需編譯,只要刷新一下就可以看到修改結(jié)果

Compose 是一個(gè)聲明性界面框架,這本身也帶有一點(diǎn)組合的意味。每個(gè)視圖結(jié)點(diǎn)均通過(guò)函數(shù)的形式來(lái)進(jìn)行聲明,那么我們自然也可以將每個(gè)視圖結(jié)點(diǎn)均聲明為一個(gè)個(gè)函數(shù),然后將每個(gè)函數(shù)作為最終視圖樹(shù)函數(shù)的入?yún)?shù)來(lái)進(jìn)行組合

以本游戲?yàn)槔麄€(gè)游戲只包含一個(gè)頁(yè)面,頁(yè)面可以再細(xì)分為三個(gè)節(jié)點(diǎn):游戲機(jī)身(TetrisBody)、游戲屏幕(TetrisScreen)、游戲按鈕(TetrisButton)

TetrisBody 函數(shù)就包含兩個(gè)入?yún)?shù)用于容納 TetrisScreen 和 TetrisButton

@Composable
fun TetrisBody(
    tetrisScreen: @Composable (() -> Unit),
    tetrisButton: @Composable (() -> Unit),
)

游戲機(jī)身 - TetrisBody

TetrisBody 比較簡(jiǎn)單,需要實(shí)現(xiàn)的功能有三個(gè):

  • 繪制背景色
  • 為 TetrisScreen 和 TetrisButton 預(yù)留位置
  • 為 TetrisScreen 繪制陰影邊框
@Composable
fun TetrisBody(
    tetrisScreen: @Composable (() -> Unit),
    tetrisButton: @Composable (() -> Unit),
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = BodyBackground)
            .padding(bottom = 30.dp)
    ) {
        Box(
            Modifier
                .align(alignment = Alignment.CenterHorizontally)
                .fillMaxWidth()
                .weight(weight = 1f)
                .padding(start = 40.dp, top = 50.dp, end = 40.dp, bottom = 10.dp),
        ) {

            //繪制游戲屏幕的邊框
            val borderPadding = 8.dp
            Canvas(modifier = Modifier.fillMaxSize()) {
                drawScreenBorder(
                    leftTop = Offset(x = 0f, y = 0f),
                    width = size.width,
                    height = size.height,
                    borderPadding = borderPadding,
                )
            }

            //游戲屏幕
            Row(
                modifier = Modifier
                    .matchParentSize()
                    .padding(all = borderPadding)
            ) {
                tetrisScreen()
            }
        }

        //游戲按鈕
        tetrisButton()
    }
}

游戲按鈕 - TetrisButton

TetrisButton 也很簡(jiǎn)單,需要實(shí)現(xiàn)的功能有兩個(gè):

  • 繪制九個(gè)操作按鈕
  • 向外透?jìng)饔脩?hù)的點(diǎn)擊操作,對(duì)事件類(lèi)型進(jìn)行區(qū)分

因此 TetrisButton 函數(shù)就需要包含一個(gè)入?yún)?shù) PlayListener 對(duì)象,TetrisButton 需要根據(jù)用戶(hù)點(diǎn)擊了哪個(gè)按鈕來(lái)回調(diào) PlayListener 相應(yīng)的方法,向外透?jìng)鼽c(diǎn)擊事件

enum class TransformationType {
    Left, Right, Rotate, Down, FastDown, Fall
}

data class PlayListener constructor(
    val onStart: () -> Unit,
    val onPause: () -> Unit,
    val onReset: () -> Unit,
    val onSound: () -> Unit,
    val onTransformation: (TransformationType) -> Unit
)

@Preview(backgroundColor = 0xffefcc19, showBackground = true)
@Composable
fun TetrisButton(
    playListener: PlayListener = combinedPlayListener()
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight(),
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight(),
            horizontalArrangement = Arrangement.Center
        ) {
            val controlPadding = 20.dp
            ControlButton(hint = "Start", modifier = Modifier.padding(end = controlPadding)) {
                playListener.onStart()
            }
            ControlButton(
                hint = "Pause",
                modifier = Modifier.padding(start = controlPadding, end = controlPadding)
            ) {
                playListener.onPause()
            }
            ControlButton(
                hint = "Reset",
                modifier = Modifier.padding(start = controlPadding, end = controlPadding)
            ) {
                playListener.onReset()
            }
            ControlButton(hint = "Sound", modifier = Modifier.padding(start = controlPadding)) {
                playListener.onSound()
            }
        }
        ConstraintLayout(
            modifier = Modifier
                .padding(top = 20.dp)
                .fillMaxWidth()
                .wrapContentWidth(align = Alignment.CenterHorizontally)
        ) {
            val (leftBtn, rightBtn, fastDownBtn, rotateBtn, fallBtn) = createRefs()
            val innerMargin = 24.dp
            PlayButton(icon = "?", modifier = Modifier.constrainAs(leftBtn) {
                start.linkTo(anchor = parent.start)
                top.linkTo(anchor = parent.top)
                end.linkTo(anchor = rightBtn.start, margin = innerMargin)
            }) {
                playListener.onTransformation(Left)
            }
            PlayButton(icon = "?", modifier = Modifier.constrainAs(rightBtn) {
                start.linkTo(anchor = leftBtn.end, margin = innerMargin)
                top.linkTo(anchor = leftBtn.top)
                bottom.linkTo(anchor = leftBtn.bottom)
            }) {
                playListener.onTransformation(Right)
            }
            PlayButton(
                icon = "Rotate",
                fontSize = 18.sp,
                modifier = Modifier.constrainAs(rotateBtn) {
                    top.linkTo(anchor = rightBtn.top)
                    start.linkTo(anchor = rightBtn.end, margin = innerMargin)
                }) {
                playListener.onTransformation(Rotate)
            }
            PlayButton(icon = "▼", modifier = Modifier.constrainAs(fastDownBtn) {
                top.linkTo(anchor = leftBtn.bottom)
                start.linkTo(anchor = leftBtn.start)
                end.linkTo(anchor = rightBtn.end)
            }) {
                playListener.onTransformation(FastDown)
            }
            PlayButton(
                icon = "▼\n▼",
                modifier = Modifier.constrainAs(fallBtn) {
                    top.linkTo(anchor = fastDownBtn.top)
                    start.linkTo(anchor = rightBtn.end)
                    end.linkTo(anchor = rotateBtn.start)
                }) {
                playListener.onTransformation(Fall)
            }
        }
    }

}

游戲屏幕 - TetrisScreen

TetrisScreen 比較復(fù)雜,需要實(shí)現(xiàn)的功能點(diǎn)主要有五個(gè):

  • 繪制游戲屏幕背景
  • 繪制不斷下落的方塊
  • 為方塊提供左移、右移、勻速下降、加速下降、旋轉(zhuǎn)等功能
  • 當(dāng)方塊無(wú)法再下落時(shí),根據(jù)需要決定是否進(jìn)行消行,然后保存該方塊的坐標(biāo)信息到屏幕背景中,根據(jù)坐標(biāo)信息繪制實(shí)心方塊,然后生成新的方塊,重復(fù)第二個(gè)步驟
  • 當(dāng)方塊無(wú)法再下落時(shí),如果方塊超出當(dāng)前屏幕,則結(jié)束游戲,執(zhí)行清屏動(dòng)畫(huà)

Compose 是根據(jù)函數(shù)的入?yún)?shù)是否發(fā)生了變化來(lái)決定是否需要進(jìn)行界面更新的,所以我們?cè)诶L制下落的方塊時(shí)可以將整個(gè)頁(yè)面視為靜態(tài)的,僅需要根據(jù)當(dāng)前的坐標(biāo)值進(jìn)行繪制即可,然后每隔幾百毫秒就改變方塊的坐標(biāo)信息,由此生成新的入?yún)?shù),通知 Compose 進(jìn)行頁(yè)面更新即可

整個(gè)游戲的所有狀態(tài)信息都保存在一個(gè) TetrisState 對(duì)象中,Compose 就通過(guò)監(jiān)聽(tīng)State<TetrisState>中值的變化來(lái)決定是否需要進(jìn)行界面更新。整個(gè)游戲屏幕就被定義為一個(gè) 10 x 24 的二維數(shù)組,即 brickArray,當(dāng)數(shù)組值等于一時(shí),就對(duì)應(yīng)實(shí)心方塊,否則就是空心方塊。Tetris 就對(duì)應(yīng)處于下落狀態(tài)中的方塊

data class TetrisState(
    val brickArray: Array<IntArray>, //屏幕坐標(biāo)系
    val tetris: Tetris, //下落的方塊
    val gameStatus: GameStatus = GameStatus.Welcome, //游戲狀態(tài)
    val soundEnable: Boolean = true, //是否開(kāi)啟音效
    val nextTetris: Tetris = Tetris(), //下一個(gè)方塊
)

方塊類(lèi)型一共可以分為七種,用字母表示就分別是:I、S、Z、L、O、J、T。每種類(lèi)型都可以容納在一個(gè) 4 x 4 的二維數(shù)組里,不管其如何旋轉(zhuǎn),都不會(huì)超出這個(gè)范圍??梢杂靡韵聰?shù)組來(lái)方便記憶每種可能的旋轉(zhuǎn)結(jié)果

val mockData = arrayOf(
    arrayOf(//I
        intArrayOf(
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            1, 1, 1, 1
        ),
        intArrayOf(
            0, 1, 0, 0,
            0, 1, 0, 0,
            0, 1, 0, 0,
            0, 1, 0, 0
        )
    ),
    arrayOf(//S
        intArrayOf(
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 1, 1, 0,
            1, 1, 0, 0
        ),
        intArrayOf(
            0, 0, 0, 0,
            1, 0, 0, 0,
            1, 1, 0, 0,
            0, 1, 0, 0
        )
    ),
    arrayOf(//Z
        intArrayOf(
            0, 0, 0, 0,
            0, 0, 0, 0,
            1, 1, 0, 0,
            0, 1, 1, 0
        ),
        intArrayOf(
            0, 0, 0, 0,
            0, 1, 0, 0,
            1, 1, 0, 0,
            1, 0, 0, 0
        )
    ),
    ···
)

每個(gè)處于下落狀態(tài)的方塊都被定義為 Tetris 對(duì)象。初始狀態(tài)下 brickArray 的值都等于 0,而 Tetris 的初始位置是在屏幕之外的,方塊每次下落時(shí)都將方塊在 brickArray 中的位置的坐標(biāo)值改變?yōu)?1,從而決定了需要在屏幕的哪個(gè)位置繪制實(shí)心方塊;再通過(guò)改變方塊相對(duì)屏幕左上角的偏移量 Offset 的值,以此改變方塊相對(duì)屏幕的位置,從而實(shí)現(xiàn)方塊的左右移動(dòng)和下落

data class Location(val x: Int, val y: Int)

data class Tetris constructor(
    val shapes: List<List<Location>>, //此方塊所有可能的旋轉(zhuǎn)結(jié)果
    val type: Int, //用于標(biāo)記當(dāng)前處于哪種旋轉(zhuǎn)狀態(tài)
    val offset: Location, //方塊相對(duì)屏幕左上角的偏移量
) {

    //此方塊當(dāng)前的形狀
    val shape: List<Location>
        get() = shapes[type]
    
}

簡(jiǎn)單起見(jiàn),可以事先就定義好 Tetris 各種可能的方塊類(lèi)型,以及該方塊的各種旋轉(zhuǎn)結(jié)果

		private val allShapes = listOf(
            //I
            listOf(
                listOf(Location(0, 3), Location(1, 3), Location(2, 3), Location(3, 3)),
                listOf(Location(1, 0), Location(1, 1), Location(1, 2), Location(1, 3)),
            ),
            //S
            listOf(
                listOf(Location(0, 3), Location(1, 2), Location(1, 3), Location(2, 2)),
                listOf(Location(0, 1), Location(0, 2), Location(1, 2), Location(1, 3)),
            ),
            //Z
            listOf(
                listOf(Location(0, 2), Location(1, 2), Location(1, 3), Location(2, 3)),
                listOf(Location(0, 2), Location(0, 3), Location(1, 1), Location(1, 2)),
            ),
            //L
            listOf(
                listOf(Location(0, 1), Location(0, 2), Location(0, 3), Location(1, 3)),
                listOf(Location(0, 2), Location(0, 3), Location(1, 2), Location(2, 2)),
                listOf(Location(0, 1), Location(1, 1), Location(1, 2), Location(1, 3)),
                listOf(Location(0, 3), Location(1, 3), Location(2, 3), Location(2, 2)),
            ),
            //O
            listOf(
                listOf(Location(0, 2), Location(0, 3), Location(1, 2), Location(1, 3)),
            ),
            //J
            listOf(
                listOf(Location(0, 3), Location(1, 1), Location(1, 2), Location(1, 3)),
                listOf(Location(0, 2), Location(0, 3), Location(1, 3), Location(2, 3)),
                listOf(Location(0, 1), Location(0, 2), Location(0, 3), Location(1, 1)),
                listOf(Location(0, 2), Location(1, 2), Location(2, 2), Location(2, 3)),
            ),
            //T
            listOf(
                listOf(Location(0, 2), Location(1, 2), Location(2, 2), Location(1, 3)),
                listOf(Location(1, 1), Location(0, 2), Location(1, 2), Location(1, 3)),
                listOf(Location(1, 2), Location(0, 3), Location(1, 3), Location(2, 3)),
                listOf(Location(0, 1), Location(0, 2), Location(0, 3), Location(1, 2)),
            ),
        )

之后在每次生成 Tetris 對(duì)象時(shí),都隨機(jī)從 allShapes 中取值。并且每個(gè) Tetris 對(duì)象的初始偏移量 offset 的 Y 值固定是 -4,即默認(rèn)處于屏幕之外,當(dāng)方塊不斷移動(dòng)時(shí),其 Offset 就會(huì)變成 Location(0, -3)、Location(1, -2) .... Location(2, 10)等各種值,通過(guò)改變 X 值來(lái)實(shí)現(xiàn)左右移動(dòng)、改變 Y 值來(lái)實(shí)現(xiàn)下移

        operator fun invoke(): Tetris {
            val shapes = allShapes.random()
            val type = Random.nextInt(0, shapes.size)
            return Tetris(
                shapes = shapes,
                type = type,
                offset = Location(
                    Random.nextInt(
                        0,
                        BRICK_WIDTH - 3
                    ), -4
                )
            )
        }

每個(gè)方塊就可以通過(guò) Canvas 來(lái)進(jìn)行繪制,方便起見(jiàn)就將其定義為擴(kuò)展函數(shù),通過(guò) color 來(lái)控制是要繪制實(shí)心方塊還是虛心方塊

fun DrawScope.drawBrick(brickSize: Float, color: Color) {
    drawRect(color = color, size = Size(brickSize, brickSize))
    val strokeWidth = brickSize / 9f
    translate(left = strokeWidth, top = strokeWidth) {
        drawRect(
            color = ScreenBackground,
            size = Size(
                width = brickSize - 2 * strokeWidth,
                height = brickSize - 2 * strokeWidth
            )
        )
    }
    val brickInnerSize = brickSize / 2.0f
    val translateLeft = (brickSize - brickInnerSize) / 2
    translate(left = translateLeft, top = translateLeft) {
        drawRect(
            color = color,
            size = Size(brickInnerSize, brickInnerSize)
        )
    }
}

之后只需要遍歷代表整個(gè)屏幕坐標(biāo)值的 screenMatrix 進(jìn)行繪制就可以繪制出屏幕背景以及下落的方塊,如果值等于一就使用 BrickFill 顏色,否則就使用 BrickAlpha。每當(dāng)有方塊無(wú)法繼續(xù)下落時(shí),該方塊所在的坐標(biāo)值就都會(huì)被寫(xiě)入到 screenMatrix 中,以此來(lái)保存各個(gè)固定的實(shí)心方塊

	Canvas(
        modifier = Modifier
            .fillMaxSize()
            .background(color = ScreenBackground)
            .padding(
                start = screenPadding, top = screenPadding,
                end = screenPadding, bottom = screenPadding
            )
    ) {
        val width = size.width
        val height = size.height
        val screenPaddingPx = screenPadding.toPx()
        val spiritPaddingPx = spiritPadding.toPx()
        val brickSize = (height - spiritPaddingPx * (matrixHeight - 1)) / matrixHeight

        kotlin.run {
            screenMatrix.forEachIndexed { y, ints ->
                ints.forEachIndexed { x, isFill ->
                    translate(
                        left = x * (brickSize + spiritPaddingPx),
                        top = y * (brickSize + spiritPaddingPx)
                    ) {
                        drawBrick(
                            brickSize = brickSize,
                            color = if (isFill == 1) BrickFill else BrickAlpha
                        )
                    }
                }
            }
        }

        ···
    }

調(diào)度器 - TetrisViewModel

TetrisViewModel 是整個(gè)游戲的調(diào)度器,其大體結(jié)構(gòu)如下所示。dispatch 方法負(fù)責(zé)接收外部的各個(gè)事件,事件類(lèi)型就對(duì)應(yīng)密封類(lèi) Action

class TetrisViewModel : ViewModel() {

    companion object {

        private const val DOWN_SPEED = 500L

        private const val CLEAR_SCREEN_SPEED = 30L

    }

    private val _tetrisStateLD: MutableStateFlow<TetrisState> = MutableStateFlow(TetrisState())

    val tetrisStateLD = _tetrisStateLD.asStateFlow()

    private val tetrisState: TetrisState
        get() = _tetrisStateLD.value

    private var downJob: Job? = null

    private var clearScreenJob: Job? = null

    fun dispatch(action: Action) {
        playSound(action)
        val unit = when (action) {
            Action.Welcome, Action.Reset -> {
                ···
            }
            Action.Start -> {
                ···
            }
            Action.Background, Action.Pause -> {
                ···
            }
            Action.Resume -> {

            }
            Action.Sound -> {
                ···
            }
            is Action.Transformation -> {
                ···
            }
        }
    }
    
    ···
    
}

sealed class Action {
    object Welcome : Action()
    object Start : Action()
    object Pause : Action()
    object Reset : Action()
    object Sound : Action()
    object Background : Action()
    object Resume : Action()
    data class Transformation(val transformationType: TransformationType) : Action()
}

enum class TransformationType {
    Left, Right, Rotate, Down, FastDown, Fall
}

游戲第一次啟動(dòng)時(shí),由 MainActivity 來(lái)主動(dòng)下發(fā) Action.Welcome 事件,執(zhí)行歡迎動(dòng)畫(huà)。當(dāng)后續(xù)用戶(hù)點(diǎn)擊 Start 按鈕啟動(dòng)游戲時(shí),則會(huì)下發(fā) Action.Start 事件,從而啟動(dòng)一個(gè)執(zhí)行延時(shí)任務(wù)的協(xié)程任務(wù) downJob,downJob 負(fù)責(zé)下發(fā) TransformationType.Down 事件,即方塊下落事件,當(dāng)消耗了該事件后,又會(huì)重復(fù)調(diào)用 startDownJob() 方法,從而實(shí)現(xiàn)自我驅(qū)動(dòng)方塊勻速下降

    private var downJob: Job? = null

    private fun onStartGame() {
        dispatchState(TetrisState().copy(gameStatus = GameStatus.Running))
        startDownJob()
    }

    private fun startDownJob() {
        cancelDownJob()
        cancelClearScreenJob()
        downJob = viewModelScope.launch {
            delay(DOWN_SPEED)
            dispatch(Action.Transformation(TransformationType.Down))
        }
    }

Action.Transformation 代表的是多種操作行為,例如左右移動(dòng)、旋轉(zhuǎn)等。但并不是每種操作都能生效,因?yàn)閳?zhí)行該操作可能會(huì)導(dǎo)致方塊超出屏幕。所以如果 onTransformation 方法返回 null 的話,說(shuō)明此次行為無(wú)效,無(wú)需更新界面

fun TetrisState.onTransformation(transformationType: TransformationType): TetrisState {
    return when (transformationType) {
        TransformationType.Left -> {
            onLeft()
        }
        TransformationType.Right -> {
            onRight()
        }
        TransformationType.Down -> {
            onDown()
        }
        TransformationType.FastDown -> {
            onFastDown()
        }
        TransformationType.Fall -> {
            onFall()
        }
        TransformationType.Rotate -> {
            onRotate()
        }
    }?.finalize() ?: this.finalize()
}

對(duì)于 Left、Right、Down、FastDown、Fall 這幾種事件,都是在對(duì) offset 做操作,通過(guò)改變 offset 的 X 坐標(biāo)和 Y 坐標(biāo)來(lái)移動(dòng)方塊的位置

private fun TetrisState.onLeft(): TetrisState? {
    return copy(
        tetris = tetris.copy(offset = Location(tetris.offset.x - 1, tetris.offset.y))
    ).takeIf { it.isValidInMatrix() }
}

private fun TetrisState.onRight(): TetrisState? {
    return copy(
        tetris = tetris.copy(offset = Location(tetris.offset.x + 1, tetris.offset.y))
    ).takeIf { it.isValidInMatrix() }
}

private fun TetrisState.onDown(): TetrisState? {
    return copy(
        tetris = tetris.copy(
            offset = Location(tetris.offset.x, tetris.offset.y + 1)
        )
    ).takeIf { it.isValidInMatrix() }
}

private fun TetrisState.onFastDown(): TetrisState? {
    return copy(
        tetris = tetris.copy(
            offset = Location(tetris.offset.x, tetris.offset.y + 2)
        )
    ).takeIf { it.isValidInMatrix() }
}

private fun TetrisState.onFall(): TetrisState? {
    while (true) {
        val result = onDown() ?: return this
        return result.onFall()
    }
}

前文說(shuō)了,每種方塊類(lèi)型都包含有多種旋轉(zhuǎn)結(jié)果,所以 Rotate 事件就需要將方塊改變?yōu)槠渌D(zhuǎn)形狀。而由于當(dāng)旋轉(zhuǎn)過(guò)后方塊的坐標(biāo)系可能會(huì)超出當(dāng)前屏幕的范圍,所以還需要依靠 adjustOffset()方法將方塊的坐標(biāo)系遷移回屏幕內(nèi)

private fun TetrisState.onRotate(): TetrisState? {
    if (tetris.shapes.size == 1) {
        return null
    }
    val nextType = if (tetris.type + 1 < tetris.shapes.size) {
        tetris.type + 1
    } else {
        0
    }
    return copy(
        tetris = tetris.copy(
            type = nextType,
        )
    ).adjustOffset().takeIf { it.isValidInMatrix() }
}

當(dāng)方塊無(wú)法再下落,或者是已經(jīng)超出了屏幕時(shí),則需要依靠 finalize()方法將方塊的坐標(biāo)值寫(xiě)入到屏幕背景 brickArray 中,并重置游戲狀態(tài)

private fun TetrisState.finalize(): TetrisState {
    if (canDown()) {
        return this
    } else {
        var gameOver = false
        for (location in tetris.shape) {
            val x = location.x + tetris.offset.x
            val y = location.y + tetris.offset.y
            if (x in 0 until width && y in 0 until height) {
                brickArray[y][x] = 1
            } else {
                gameOver = true
            }
        }
        return if (gameOver) {
            copy(gameStatus = GameStatus.GameOver)
        } else {
            val clearRes = clearIfNeed()
            if (clearRes == null) {
                copy(
                    gameStatus = GameStatus.Running,
                    tetris = nextTetris,
                    nextTetris = Tetris()
                )
            } else {
                copy(
                    gameStatus = GameStatus.LineClearing,
                    tetris = nextTetris,
                    nextTetris = Tetris()
                )
            }
        }
    }
}

項(xiàng)目地址

游戲的大體實(shí)現(xiàn)思路就如上所述,表達(dá)能力所限,有些地方?jīng)]法講得太清楚,實(shí)現(xiàn)細(xì)節(jié)歡迎查閱源碼了解 

Github 地址

到此這篇關(guān)于利用Jetpack Compose實(shí)現(xiàn)經(jīng)典俄羅斯方塊游戲的文章就介紹到這了,更多相關(guān)Jetpack Compose俄羅斯方塊內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論