利用Jetpack Compose實(shí)現(xiàn)經(jī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é)歡迎查閱源碼了解
到此這篇關(guān)于利用Jetpack Compose實(shí)現(xiàn)經(jīng)典俄羅斯方塊游戲的文章就介紹到這了,更多相關(guān)Jetpack Compose俄羅斯方塊內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Android中分析Jetpack?Compose動(dòng)畫(huà)內(nèi)部的實(shí)現(xiàn)原理
- Jetpack?Compose?實(shí)現(xiàn)一個(gè)圖片選擇框架功能
- Jetpack Compose實(shí)現(xiàn)列表和動(dòng)畫(huà)效果詳解
- 利用Jetpack?Compose實(shí)現(xiàn)繪制五角星效果
- 通過(guò)Jetpack Compose實(shí)現(xiàn)雙擊點(diǎn)贊動(dòng)畫(huà)效果
- 利用Jetpack Compose繪制可愛(ài)的天氣動(dòng)畫(huà)
- Jetpack Compose布局的使用詳細(xì)介紹
相關(guān)文章
Android音頻開(kāi)發(fā)之音頻采集的實(shí)現(xiàn)示例
本篇文章主要介紹了Android音頻開(kāi)發(fā)之音頻采集的實(shí)現(xiàn)示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04Android中AlertDialog的六種創(chuàng)建方式
這篇文章主要介紹了Android中AlertDialog的六種創(chuàng)建方式的相關(guān)資料,需要的朋友可以參考下2016-07-07Android編程實(shí)現(xiàn)將應(yīng)用強(qiáng)制安裝到手機(jī)內(nèi)存的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)將應(yīng)用強(qiáng)制安裝到手機(jī)內(nèi)存的方法,分析了Android程序安裝的相關(guān)屬性設(shè)置技巧及注意事項(xiàng),需要的朋友可以參考下2015-12-12Android實(shí)現(xiàn)多次閃退清除數(shù)據(jù)
這篇文章主要介紹了Android實(shí)現(xiàn)多次閃退清除數(shù)據(jù)的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-04-04Android實(shí)現(xiàn)網(wǎng)易云音樂(lè)高仿版流程
這篇文章主要介紹了Android實(shí)現(xiàn)網(wǎng)易云音樂(lè)高仿版,包含了首頁(yè)復(fù)雜發(fā)現(xiàn)界面布局和功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08Android仿外賣(mài)購(gòu)物車(chē)功能
這篇文章主要為大家詳細(xì)介紹了Android仿外賣(mài)購(gòu)物車(chē)功能的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06Android打印機(jī)--小票打印格式及模板設(shè)置實(shí)例代碼
這篇文章主要介紹了Android打印機(jī)--小票打印格式及模板設(shè)置實(shí)例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04