利用Jetpack?Compose復(fù)刻游戲Flappy?Bird
Flappy Bird是13年紅極一時的小游戲,其簡單有趣的玩法和變態(tài)的難度形成了強烈反差,引發(fā)全球玩家競相把玩,欲罷不能!遂選擇復(fù)刻這個小游戲,在實現(xiàn)的過程中向大家演示Compose工具包的UI組合、數(shù)據(jù)驅(qū)動等重要思想。
1.拆解游戲
不記得這個游戲或完全沒玩過的朋友,可以點擊下面的鏈接,體驗一下Flappy Bird的玩法。
為拆解游戲,筆者也錄了一段游戲過程。
反復(fù)觀看這段GIF,可以發(fā)現(xiàn)游戲的一些規(guī)律:
- 遠處的建筑和近處的土壤是靜止不動的
- 小鳥一直在上下移動,伴隨著翅膀和身體的飛翔姿態(tài)
- 管道和路面則不斷地向左移動,營造出小鳥向前飛翔的視覺效果
通過截圖、切圖、填充像素和簡單的PS,可以拿到各元素的圖片。
2.復(fù)刻畫面
各方卡司已就位,接下來開始布置整個畫面。暫不實現(xiàn)元素的移動效果,先把靜態(tài)的整體效果搭建好。
ⅰ.布置遠近景
靜止不動的建筑遠景最為簡單,封裝到可組合函數(shù)FarBackground
里,內(nèi)部放置一張圖片即可。
@Composable fun?FarBackground(modifier:?Modifier)?{ ????Column?{ ????????Image( ????????????painter?=?painterResource(id?=?R.drawable.background), ????????????contentScale?=?ContentScale.FillBounds, ????????????contentDescription?=?null, ????????????modifier?=?modifier.fillMaxSize() ????????) ????} }
遠景的下面由分割線、路面和土壤組成,封裝到NearForeground
函數(shù)里。通過Modifier
的fraction
參數(shù)控制路面和土壤的比例,保證在不同尺寸屏幕上能按比例呈現(xiàn)游戲界面。
@Composable fun?NearForeground(...)?{ ????Column(?modifier?)?{ ????????//?分割線 ????????Divider( ????????????color?=?GroundDividerPurple, ????????????thickness?=?5.dp ????????) ????????//?路面 ????????Box(modifier?=?Modifier.fillMaxWidth())?{ ????????????Image( ????????????????painter?=?painterResource(id?=?R.drawable.foreground_road), ????????????????... ????????????????modifier?=?modifier ????????????????????.fillMaxWidth() ????????????????????.fillMaxHeight(0.23f) ????????????????) ????????????} ????????} ????????//?土壤 ????????Image( ????????????painter?=?painterResource(id?=?R.drawable.foreground_earth), ???????????... ????????????modifier?=?modifier ????????????????.fillMaxWidth() ????????????????.fillMaxHeight(0.77f) ????????) ????} }
將整個游戲畫面抽象成GameScreen
函數(shù),通過Column
豎著排列遠景和前景??紤]到移動的小鳥和管道需要呈現(xiàn)在遠景之上,所以在遠景的外面包上一層Box
組件。
@Composable fun?GameScreen(?...?)?{ ????Column(?...??)?{ ????????Box(modifier?=?Modifier ????????????.align(Alignment.CenterHorizontally) ????????????.fillMaxWidth() ????????)?{ ????????????FarBackground(Modifier.fillMaxSize()) ????????} ????????Box(modifier?=?Modifier ????????????.align(Alignment.CenterHorizontally) ????????????.fillMaxWidth() ????????)?{ ????????????NearForeground( ????????????????modifier?=?Modifier.fillMaxSize() ????????????) ????????} ????} }
ⅱ.擺放管道
仔細觀察管道,會發(fā)現(xiàn)一些管道具備朝上朝下、高度隨機的特點。為此將管道的視圖分拆成蓋子和柱子兩部分:
- 蓋子和柱子的放置順序決定管道的朝向
- 柱子的高度則控制著管道整體的高度 這樣的話,只使用蓋子和柱子兩張圖片,就可以靈活實現(xiàn)各種形態(tài)的管道。
先來組合蓋子PipeCover
和柱子PipePillar
的可組合函數(shù)。
@Composable fun?PipeCover()?{ ????Image( ????????painter?=?painterResource(id?=?R.drawable.pipe_cover), ????????contentScale?=?ContentScale.FillBounds, ????????contentDescription?=?null, ????????modifier?=?Modifier.size(PipeCoverWidth,?PipeCoverHeight) ????) } @Composable fun?PipePillar(modifier:?Modifier?=?Modifier,?height:?Dp?=?90.dp)?{ ????Image( ????????painter?=?painterResource(id?=?R.drawable.pipe_pillar), ????????contentScale?=?ContentScale.FillBounds, ????????contentDescription?=?null, ????????modifier?=?modifier.size(50.dp,?height) ????) }
管道的可組合函數(shù)Pipe
可以根據(jù)照朝向和高度的參數(shù),組合成對應(yīng)的管道。
@Composable fun?Pipe(? ????height:?Dp?=?HighPipe, ????up:?Boolean?=?true )?{ ????Box(?...?)?{ ????????Column?{ ????????????if?(up)?{ ????????????????PipePillar(Modifier.align(CenterHorizontally),?height?-?30.dp) ????????????????PipeCover() ????????????}?else?{ ????????????????PipeCover() ????????????????PipePillar(Modifier.align(CenterHorizontally),?height?-?30.dp) ????????????} ????????} ????} }
另外,管道都是成對出現(xiàn)、且無論高度如何中間的間距是固定的。所以我們再實現(xiàn)一個管道組的可組合函數(shù)PipeCouple
。
@Composable fun?PipeCouple(?...?)?{ ????Box(...)?{ ????????GetUpPipe(height?=?upHeight, ????????????modifier?=?Modifier ????????????????.align(Alignment.TopEnd) ????????) ????????GetDownPipe(height?=?downHeight, ????????????modifier?=?Modifier ????????????????.align(Alignment.BottomEnd) ????????) ????} }
將PipeCouple添加到FarBackground的下面,管道就放置完畢了。
@Composable fun?GameScreen(?...?)?{ ????Column(...)?{ ????????Box(...)?{ ????????????FarBackground(Modifier.fillMaxSize()) ???????????? ????????????//?管道對添加遠景上去 ????????????PipeCouple( ????????????????modifier?=?Modifier.fillMaxSize() ????????????) ????????} ????????... ????} }
ⅲ.放置小鳥
小鳥通過Image組件即可實現(xiàn),默認情況下放置到布局的Center方位。
@Composable fun?Bird(?...?)?{ ????Box(?...?)?{ ????????Image( ????????????painter?=?painterResource(id?=?R.drawable.bird_match), ????????????contentScale?=?ContentScale.FillBounds, ????????????contentDescription?=?null, ????????????modifier?=?Modifier ????????????????.size(BirdSizeWidth,?BirdSizeHeight) ????????????????.align(Alignment.Center) ????????) ????} }
視覺上小鳥呈現(xiàn)在管道的前面,所以Bird
可組合函數(shù)要添加到管道組函數(shù)的后面。
@Composable fun?GameScreen(?...?)?{ ????Column(...)?{ ????????Box(...)?{ ????????????... ????????????PipeCouple(?...?) ????????????//?將小鳥添加到遠景上去 ????????????Bird( ????????????????modifier?=?Modifier.fillMaxSize(), ????????????????state?=?viewState ????????????) ????????} ????} }
至此,各元素都放置完了。接下來著手讓小鳥,管道和路面這些動態(tài)元素動起來。
3.狀態(tài)管理和架構(gòu)
Compose中Modifier#offset()函數(shù)可以更改視圖在橫縱方向上的偏移值,通過不斷地調(diào)整這個偏移值,即可營造出動態(tài)的視覺效果。無論是小鳥還是管道和路面,它們的移動狀態(tài)都可以依賴這個思路。
那如何管理這些持續(xù)變化的偏移值數(shù)據(jù)?如何將數(shù)據(jù)反映到畫面上?
Compose通過State驅(qū)動可組合函數(shù)進行重組,進而達到畫面的重繪。所以我們將這些數(shù)據(jù)封到ViewState
中,交由ViewModel
框架計算和更新,Compose訂閱State之后驅(qū)動所有元素活動起來。除了個元素的偏移值數(shù)據(jù),State中還要存放游戲分值,游戲狀態(tài)等額外信息。
data?class?ViewState( ????val?gameStatus:?GameStatus?=?GameStatus.Waiting, ????//?小鳥狀態(tài) ????val?birdState:?BirdState?=?BirdState(), ????//?管道組狀態(tài) ????val?pipeStateList:?List<PipeState>?=?PipeStateList, ????var?targetPipeIndex:?Int?=?-1, ????//?路面狀態(tài) ????val?roadStateList:?List<RoadState>?=?RoadStateList, ????var?targetRoadIndex:?Int?=?-1, ????//?分值數(shù)據(jù) ????val?score:?Int?=?0, ????val?bestScore:?Int?=?0, ) enum?class?GameStatus?{ ????Waiting, ????Running, ????Dying,? ????Over }
用戶點擊屏幕會觸發(fā)游戲開始、重新開始、小鳥上升等動作,這些視圖上的事件需要反向傳遞給ViewModel處理和做出響應(yīng)。事件由Clickable
數(shù)據(jù)類封裝,再轉(zhuǎn)為對應(yīng)的GameAction
發(fā)送到ViewModel中。
data?class?Clickable( ????val?onStart:?()?->?Unit?=?{}, ????val?onTap:?()?->?Unit?=?{}, ????val?onRestart:?()?->?Unit?=?{}, ????val?onExit:?()?->?Unit?=?{} ) sealed?class?GameAction?{ ????object?Start?:?GameAction() ????object?AutoTick?:?GameAction() ????object?TouchLift?:?GameAction() ????object?Restart?:?GameAction() }
前面說過,可以不斷調(diào)整下Offset數(shù)據(jù)使得視圖動起來。具體實現(xiàn)可以通過LaunchedEffect
啟動一個定時任務(wù),定期發(fā)送一個更新視圖的動作AutoTick
。注意:Compose里獲取ViewModel實例發(fā)生NoSuchMethodError
錯誤的話,記得按照官方構(gòu)建的版本重新Sync一下。
setContent?{ ????FlappyBirdTheme?{ ????????Surface(color?=?MaterialTheme.colors.background)?{ ????????????val?gameViewModel:?GameViewModel?=?viewModel() ????????????LaunchedEffect(key1?=?Unit)?{ ????????????????while?(isActive)?{ ????????????????????delay(AutoTickDuration) ????????????????????gameViewModel.dispatch(GameAction.AutoTick) ????????????????} ????????????} ????????????Flappy(Clickable( ????????????????onStart?=?{ ????????????????????gameViewModel.dispatch(GameAction.Start) ????????????????}... ????????????)) ????????} ????}
ViewModel收到Action后開啟協(xié)程,計算視圖的位置、更新對應(yīng)State,之后發(fā)射出去。
class?GameViewModel?:?ViewModel()?{ ????fun?dispatch(...)?{ ????????response(action,?viewState.value) ????} ????private?fun?response(action:?GameAction,?state:?ViewState)?{ ????????viewModelScope.launch?{ ????????????withContext(Dispatchers.Default)?{ ????????????????emit(when?(action)?{ ????????????????????GameAction.AutoTick?->?run?{ ????????????????????????//?路面,管道組以及小鳥移動的新State獲取 ????????????????????????... ???????????????????????state.copy( ????????????????????????????gameStatus?=?GameStatus.Running, ????????????????????????????birdState?=?newBirdState, ????????????????????????????pipeStateList?=?newPipeStateList, ????????????????????????????roadStateList?=?newRoadStateList ????????????????????????) ????????????????????} ????????????????????... ????????????????}) ????????????} ????????} ????} }
4.路面動起來
如果畫面上只放一張路面圖片,更改X軸Offset的話,剩余的部分會沒有路面,無法呈現(xiàn)出不斷移動的效果。
思前想后,發(fā)現(xiàn)放置兩張路面圖片可以解決:一張放在屏幕外側(cè),一張放在屏幕內(nèi)側(cè)。游戲的過程中同時同方向移動兩張圖片,當(dāng)前一張圖片移出屏幕后重置其位置,進而營造出道路不斷移動的效果。
@Composable fun?NearForeground(?...?)?{ ????val?viewModel:?GameViewModel?=?viewModel() ????Column(?...?)?{ ????????... ????????//?路面 ????????Box(modifier?=?Modifier.fillMaxWidth())?{ ????????????state.roadStateList.forEach?{?roadState?-> ????????????????Image( ????????????????????... ????????????????????modifier?=?modifier ????????????????????????... ?????????????????????????//?不斷調(diào)整路面在x軸的偏移值 ????????????????????????.offset(x?=?roadState.offset) ????????????????) ????????????} ????????} ????????... ????????if?(state.playZoneSize.first?>?0)?{ ????????????state.roadStateList.forEachIndexed?{?index,?roadState?-> ????????????????//?任意路面的偏移值達到兩張圖片位置差的時候 ????????????????//?重置路面位置,重新回到屏幕外 ????????????????if?(roadState.offset?<=?-?TempRoadWidthOffset)?{ ????????????????????viewModel.dispatch(GameAction.RoadExit,?roadIndex?=?index) ????????????????} ????????????} ????????} ????} }
ViewModel收到RoadExit
的Action之后通知路面State進行位置的重置。
class?GameViewModel?:?ViewModel()?{ ????private?fun?response(action:?GameAction,?state:?ViewState)?{ ????????viewModelScope.launch?{ ????????????withContext(Dispatchers.Default)?{ ????????????????emit(when?(action)?{ ????????????????????GameAction.RoadExit?->?run?{ ????????????????????????val?newRoadState:?List<RoadState>?= ????????????????????????????if?(state.targetRoadIndex?==?0)?{ ????????????????????????????????listOf(state.roadStateList[0].reset(),?state.roadStateList[1]) ????????????????????????????}?else?{ ????????????????????????????????listOf(state.roadStateList[0],?state.roadStateList[1].reset()) ????????????????????????????} ????????????????????????state.copy( ????????????????????????????gameStatus?=?GameStatus.Running, ????????????????????????????roadStateList?=?newRoadState ????????????????????????) ????????????????????} ????????????????}) ????????????} ????????} ????} } data?class?RoadState?(var?offset:?Dp?=?RoadWidthOffset)?{ ????//?移動路面 ????fun?move():?RoadState?=?copy(offset?=?offset?-?RoadMoveVelocity) ????//?重置路面 ????fun?reset():?RoadState?=?copy(offset?=?TempRoadWidthOffset) }
5.管道動起來
設(shè)備屏幕寬度有限,同一時間最多呈現(xiàn)兩組管道就可以了。和路面運動的思路類似,只需要放置兩組管道,就可以實現(xiàn)管道不停移動的視覺效果。
具體的話,兩組管道相隔一段距離放置,游戲中兩組管道一起同時向左移動。當(dāng)前一組管道運動到屏幕外的時候,將其位置重置。
那如何計算管道移動到屏幕外的時機?
畫面重組的時候判斷管道偏移值是否達到屏幕寬度,YES的話向ViewModel發(fā)送管道重置的Action。
@Composable fun?PipeCouple( ????modifier:?Modifier?=?Modifier, ????state:?ViewState?=?ViewState(), ????pipeIndex:?Int?=?0 )?{ ????val?viewModel:?GameViewModel?=?viewModel() ????val?pipeState?=?state.pipeStateList[pipeIndex] ????Box(?...?)?{ ????????//從State中獲取管道的偏移值,在重組的時候讓管道移動? ????????GetUpPipe(height?=?pipeState.upHeight, ????????????modifier?=?Modifier ????????????????.align(Alignment.TopEnd) ????????????????.offset(x?=?pipeState.offset) ????????) ????????GetDownPipe(...) ????????if?(state.playZoneSize.first?>?0)?{ ????????????... ????????????//?移動到屏幕外的時候發(fā)送重置Action ????????????if?(pipeState.offset?<?-?playZoneWidthInDP)?{ ????????????????viewModel.dispatch(GameAction.PipeExit,?pipeIndex?=?pipeIndex) ????????????} ????????} ????} }
ViewModel收到PipeExit
的Action后發(fā)起重置管道數(shù)據(jù),并將更新發(fā)射出去。
class?GameViewModel?:?ViewModel()?{ ????private?fun?response(action:?GameAction,?state:?ViewState)?{ ????????viewModelScope.launch?{ ????????????withContext(Dispatchers.Default)?{ ????????????????emit(when?(action)?{ ????????????????????GameAction.PipeExit?->?run?{ ????????????????????????val?newPipeStateList:?List<PipeState>?= ????????????????????????????if?(state.targetPipeIndex?==?0)?{ ????????????????????????????????listOf( ????????????????????????????????????state.pipeStateList[0].reset(), ????????????????????????????????????state.pipeStateList[1] ????????????????????????????????) ????????????????????????????}?else?{ ????????????????????????????????listOf( ????????????????????????????????????state.pipeStateList[0], ????????????????????????????????????state.pipeStateList[1].reset() ????????????????????????????????) ????????????????????????????} ????????????????????????state.copy( ????????????????????????????pipeStateList?=?newPipeStateList ????????????????????????) ????????????????????} ????????????????}) ????????????} ????????} ????} }
但相比路面,管道還具備高度隨機、間距固定的特性。所以重置位置的同時記得將柱子的高度隨機賦值,并給另一根柱子賦值剩余的高度。
data?class?PipeState?( ????var?offset:?Dp?=?FirstPipeWidthOffset, ????var?upHeight:?Dp?=?ValueUtil.getRandomDp(LowPipe,?HighPipe), ????var?downHeight:?Dp?=?TotalPipeHeight?-?upHeight?-?PipeDistance )?{ ????//?移動管道 ????fun?move():?PipeState?= ????????copy(offset?=?offset?-?PipeMoveVelocity) ????//?重置管道 ????fun?reset():?PipeState?{ ????????//?隨機賦值上面管道的高度 ????????val?newUpHeight?=?ValueUtil.getRandomDp(LowPipe,?HighPipe) ????????return?copy( ????????????offset?=?FirstPipeWidthOffset, ????????????upHeight?=?newUpHeight, ????????????//?下面管道的高度由差值賦值 ????????????downHeight?=?TotalPipeHeight?-?newUpHeight?-?PipeDistance ????????) ????} }
需要留意一點的是,如果希望管道組出現(xiàn)的節(jié)奏固定,那么管道組之間的橫向間距(不是上下管道的間距)始終需要保持一致。為此兩組管道初始的Offset數(shù)據(jù)要遵循一些規(guī)則,此處省略計算的過程,大概規(guī)則如下。
val?FirstPipeWidthOffset?=?PipeCoverWidth?*?2 //?第二組管道的offset等于 //?屏幕寬度?加上?三倍第一組管道offset?的一半 val?SecondPipeWidthOffset?=?(TotalPipeWidth?+?FirstPipeWidthOffset?*?3)?/?2 val?PipeStateList?=?listOf( ????PipeState(), ????PipeState(offset?=?(SecondPipeWidthOffset)) )
6.小鳥飛起來
不斷調(diào)整小鳥圖片在Y軸上的偏移值可以實現(xiàn)小鳥的上下移動。但相較于路面和管道,小鳥的需要些特有的處理:
- 監(jiān)聽用戶的點擊事件,向上調(diào)整偏移值實現(xiàn)上升效果
- 在上升和下降的過程中,調(diào)整小鳥的
Rotate
角度,以演示運動的姿態(tài) - 在觸碰到路面的時刻,發(fā)送
HitGround
的Action停止游戲
@Composable fun?GameScreen(...)?{ ????... ????Column( ????????modifier?=?Modifier ????????????.fillMaxSize() ????????????.background(ForegroundEarthYellow) ????????????.run?{ ????????????????pointerInteropFilter?{ ????????????????????when?(it.action)?{ ????????????????????????//?監(jiān)聽點擊事件,觸發(fā)游戲開始或小鳥上升 ????????????????????????ACTION_DOWN?->?{ ????????????????????????????if?(viewState.gameStatus?==?GameStatus.Waiting) ????????????????????????????????clickable.onStart() ????????????????????????????else?if?(viewState.gameStatus?==?GameStatus.Running) ????????????????????????????????clickable.onTap() ????????????????????????} ????????????????????????... ????????????????????} ????????????????????false ????????????????} ????????????} ????)?{?...?} }
小鳥根據(jù)State的Offset數(shù)據(jù)開始移動和調(diào)整姿態(tài),同時在觸地的時候告知ViewModel。因為下降的偏移值誤差可能導(dǎo)致觸地的那刻小鳥位置發(fā)生偏差,所以在小鳥下落到路面的臨界點后需要手動調(diào)整下Offset值。
@Composable fun?Bird(...)?{ ????... ????//?根據(jù)小鳥上升或下降的狀態(tài)調(diào)整小鳥的Roate角度 ????val?rotateDegree?= ????????if?(state.isLifting)?LiftingDegree ????????else?if?(state.isFalling)?FallingDegree ????????else?PendingDegree ????Box(...)?{ ????????var?correctBirdHeight?=?state.birdState.birdHeight ????????if?(state.playZoneSize.second?>?0)?{ ????????????... ????????????val?fallingThreshold?=?BirdHitGroundThreshold ????????????//?小鳥偏移值達到背景邊界時發(fā)送落地Action ????????????if?(correctBirdHeight?+?fallingThreshold?>=?playZoneHeightInDP?/?2)?{ ????????????????viewModel.dispatch(GameAction.HitGround) ????????????????//?修改下offset值避免下落到臨界位置的誤差 ????????????????correctBirdHeight?=?playZoneHeightInDP?/?2?-?fallingThreshold ????????????} ????????} ????????Image( ????????????... ????????????modifier?=?Modifier ????????????????.size(BirdSizeWidth,?BirdSizeHeight) ????????????????.align(Alignment.Center) ????????????????.offset(y?=?correctBirdHeight) ?????????????????//?將旋轉(zhuǎn)角度應(yīng)用到小鳥,展示飛翔姿態(tài) ????????????????.rotate(rotateDegree) ????????) ????} }
7.碰撞和實時分值
動態(tài)的元素都實現(xiàn)好了,下一步開始安排碰撞算法,并將實時分值同步展示到游戲上方。
仔細思考,發(fā)現(xiàn)當(dāng)管道組移動到小鳥飛翔區(qū)域的時候,計算小鳥是否處在管道區(qū)域即可判斷是否產(chǎn)生了碰撞。而當(dāng)管道移動出小鳥飛翔范圍的時候,即可判定小鳥成功穿過了管道,開始計分。
如下圖所示當(dāng)管道移動到小鳥飛翔區(qū)域的時候,紅色部分為危險地帶,綠色部分才是安全區(qū)域。
@Composable fun?GameScreen(...)?{ ????... ????Column(...)?{ ????????Box(...)?{ ????????????... ????????????//?添加實時展示分值的Text組件 ????????????ScoreBoard( ????????????????modifier?=?Modifier.fillMaxSize(), ????????????????state?=?viewState, ????????????????clickable?=?clickable ????????????) ????????????//?遍歷兩個管道組,檢查小鳥的穿過狀態(tài) ????????????if?(viewState.gameStatus?==?GameStatus.Running)?{ ????????????????viewState.pipeStateList.forEachIndexed?{?pipeIndex,?pipeState?-> ????????????????????CheckPipeStatus( ????????????????????????viewState.birdState.birdHeight, ????????????????????????pipeState, ????????????????????????playZoneWidthInDP, ????????????????????????playZoneHeightInDP ????????????????????).also?{ ????????????????????????when?(it)?{ ????????????????????????????//?碰撞到管道的話通知ViewModel,安排墜落 ????????????????????????????PipeStatus.BirdHit?->?{ ????????????????????????????????viewModel.dispatch(GameAction.HitPipe) ????????????????????????????} ????????????????????????????//?成功通過的話通知ViewModel計分 ????????????????????????????PipeStatus.BirdCrossed?->?{ ????????????????????????????????viewModel.dispatch(GameAction.CrossedPipe,?pipeIndex?=?pipeIndex) ????????????????????????????} ????????????????????????} ????????????????????} ????????????????} ????????????} ????????} ????} } @Composable fun?CheckPipeStatus(...):?PipeStatus?{ ????//?管道尚未移動到小鳥運動區(qū)域 ????if?(pipeState.offset?-?PipeCoverWidth?>?-?zoneWidth?/?2?+?BirdSizeWidth?/?2)?{ ????????return?PipeStatus.BirdComing ????}?else?if?(pipeState.offset?-?PipeCoverWidth?<?-?zoneWidth?/?2?-?BirdSizeWidth?/?2)?{ ????????//?小鳥成功穿過管道 ????????return?PipeStatus.BirdCrossed ????}?else?{ ????????val?birdTop?=?(zoneHeight?-?BirdSizeHeight)?/?2?+?birdHeightOffset ????????val?birdBottom?=?(zoneHeight?+?BirdSizeHeight)?/?2?+?birdHeightOffset ????????//?管道移動到小鳥運動區(qū)域并和小鳥重合 ????????if?(birdTop?<?pipeState.upHeight?||?birdBottom?>?zoneHeight?-?pipeState.downHeight)?{ ????????????return?PipeStatus.BirdHit ????????} ????????return?PipeStatus.BirdCrossing ????} ?}
ViewModel收到碰撞HitPipe
和穿過管道CrossedPipe
的Action后進行墜落或計分的處理。
class?GameViewModel?:?ViewModel()?{ ????private?fun?response(action:?GameAction,?state:?ViewState)?{ ????????viewModelScope.launch?{ ????????????withContext(Dispatchers.Default)?{ ????????????????emit(when?(action)?{ ????????????????????GameAction.HitPipe?->?run?{ ????????????????????????//?撞擊到管道后快速墜落 ????????????????????????val?newBirdState?=?state.birdState.quickFall() ????????????????????????state.copy( ????????????????????????????//?并將游戲Status更新為Dying ????????????????????????????gameStatus?=?GameStatus.Dying, ????????????????????????????birdState?=?newBirdState ????????????????????????) ????????????????????} ????????????????????GameAction.CrossedPipe?->?run?{ ????????????????????????val?targetPipeState?=?state.pipeStateList[state.targetPipeIndex] ????????????????????????//?計算過分值的話跳過,避免重復(fù)計分 ????????????????????????if?(targetPipeState.counted)?{ ????????????????????????????return@run?state.copy() ????????????????????????} ????????????????????????//?標(biāo)記該管道組已經(jīng)統(tǒng)計過分值 ????????????????????????val?countedPipeState?=?targetPipeState.count() ????????????????????????val?newPipeStateList?=?if?(state.targetPipeIndex?==?0)?{ ????????????????????????????listOf(countedPipeState,?state.pipeStateList[1]) ????????????????????????}?else?{ ????????????????????????????listOf(state.pipeStateList[0],?countedPipeState) ????????????????????????} ????????????????????????state.copy( ????????????????????????????pipeStateList?=?newPipeStateList, ????????????????????????????//?當(dāng)前分值累加 ????????????????????????????score?=?state.score?+?1, ????????????????????????????//?最高分取最高分和當(dāng)前分值的較大值即可 ????????????????????????????bestScore?=?(state.score?+?1).coerceAtLeast(state.bestScore) ????????????????????????) ????????????????????} ????????????????}) ????????????} ????????} ????} }
當(dāng)小鳥碰撞到了管道,立刻將下落的速度提高,并將Rotate角度加大,營造出快速墜落的效果。
@Composable fun?Bird(...)?{ ????... ????val?rotateDegree?= ????????if?(state.isLifting)?LiftingDegree ????????else?if?(state.isFalling)?FallingDegree ????????else?if?(state.isQuickFalling)?DyingDegree ????????else?if?(state.isOver)?DeadDegree ????????else?PendingDegree }
8.結(jié)束分值和重新開始
結(jié)束和實時兩種分值功能有交叉,統(tǒng)一封裝到ScoreBoard
可組合函數(shù)中,根據(jù)游戲狀態(tài)自由切換。
游戲結(jié)束時展示的信息較為豐富,包含本次分值、最高分值,以及重新開始和退出兩個按鈕。為了方便視圖的Preview
和提高重組性能,我們將其拆分為單個分值、按鈕、分值儀表盤和結(jié)束分值四個部分。
Compose的Preview功能很好用,但要留意一點:其Composable函數(shù)里不要放入ViewModel邏輯,否則會渲染失敗。我們可以拆分UI和ViewModel邏輯,在保證Preview能順利進行的同時能復(fù)用視圖部分的代碼。
@Composable fun?ScoreBoard(...)?{ ????when?(state.gameStatus)?{ ????????//?開始的狀態(tài)下展示簡單的實時分值 ????????GameStatus.Running?->?RealTimeBoard(modifier,?state.score) ????????//?結(jié)束的話展示豐富的儀表盤 ????????GameStatus.Over?->?GameOverBoard(modifier,?state.score,?state.bestScore,?clickable) ????} } //?包含豐富分值和按鈕的Box組件 @Composable fun?GameOverBoard(...)?{ ????Box(...)?{ ????????Column(...)?{ ????????????GameOverScoreBoard( ????????????????Modifier.align(CenterHorizontally), ????????????????score, ????????????????maxScore ????????????) ????????????Spacer(...) ????????????GameOverButton(modifier?=?Modifier.wrapContentSize().align(CenterHorizontally),?clickable) ????????} ????} }
豐富分值和按鈕的可組合函數(shù)的分別實現(xiàn)。
//?展示豐富分值,包括背景邊框、當(dāng)前分值和最高分值 @Composable fun?GameOverScoreBoard(...)?{ ????Box(...)?{ ????????//?Score?board?background ????????Image( ????????????painter?=?painterResource(id?=?R.drawable.score_board_bg), ????????????... ????????) ????????Column(...)?{ ????????????LabelScoreField(modifier,?R.drawable.score_bg,?score) ????????????Spacer( ????????????????modifier?=?Modifier ????????????????????.wrapContentWidth() ????????????????????.height(3.dp) ????????????) ????????????LabelScoreField(modifier,?R.drawable.best_score_bg,?maxScore) ????????} ????} } //?重新開始和退出按鈕 @Composable fun?GameOverButton(...)?{ ????Row(...)?{ ????????//?重新開始按鈕 ????????Image( ????????????painter?=?painterResource(id?=?R.drawable.restart_button), ????????????... ????????????modifier?=?Modifier ????????????????... ????????????????.clickable(true)?{ ????????????????????clickable.onRestart() ????????????????} ????????) ????????Spacer(...) ????????//?退出按鈕 ????????Image( ????????????painter?=?painterResource(id?=?R.drawable.exit_button), ????????????... ????????????modifier?=?Modifier ????????????????... ????????????????.clickable(true)?{ ????????????????????clickable.onExit() ????????????????} ????????) ????} }
再監(jiān)聽重新開始和退出按鈕的事件,發(fā)送Restart
和Exit
的Action。Exit的響應(yīng)比較簡單,直接關(guān)閉Activity即可。
setContent?{ ????FlappyBirdTheme?{ ????????Surface(color?=?MaterialTheme.colors.background)?{ ????????????val?gameViewModel:?GameViewModel?=?viewModel() ????????????Flappy(Clickable( ????????????????... ????????????????onRestart?=?{ ????????????????????gameViewModel.dispatch(GameAction.Restart) ????????????????}, ????????????????onExit?=?{ ????????????????????finish() ????????????????} ????????)) ????????} ????} }
Restart則要告知ViewModel去重置各種游戲數(shù)據(jù),包括小鳥位置、管道和道路的位置、以及分值,但最高分值數(shù)據(jù)應(yīng)當(dāng)保留下來。
class?GameViewModel?:?ViewModel()?{ ????private?fun?response(action:?GameAction,?state:?ViewState)?{ ????????viewModelScope.launch?{ ????????????withContext(Dispatchers.Default)?{ ????????????????emit(when?(action)?{ ????????????????????GameAction.Restart?->?run?{ ????????????????????????state.reset(state.bestScore) ????????????????????} ????????????????}) ????????????} ????????} ????} } data?class?ViewState( ????... ????//?重置State數(shù)據(jù),最高分值除外 ????fun?reset(bestScore:?Int):?ViewState?= ????????ViewState(bestScore?=?bestScore) }
9.最終效果
給復(fù)刻好的游戲做個Logo:采用小鳥的Icon和特有的藍色背景作成的Adaptive Icon
。
從點擊Logo到游戲結(jié)束再到重新開始,錄制一段完整游戲。
復(fù)刻的效果還是比較完整的,但仍然有不少可以優(yōu)化和擴展的地方:
1.比如增加簡易模式的選擇??梢詮男▲B的升降幅度、管道的間隔、管道移動的速度、連續(xù)出現(xiàn)的組數(shù)等角度入手
2.增加翅膀扇動的姿態(tài)。實現(xiàn)的話也不難,比如將小鳥的翅膀部分扣出來,在飛翔的過程中不斷地來回Rotate一定角度
3.Canvas自定義描畫。部分視圖元素采用的是圖片,其實也可以通過Canvas來實現(xiàn),順道強化一下Compose的描畫使用
以上就是利用Jetpack Compose復(fù)刻游戲Flappy Bird的詳細內(nèi)容,更多關(guān)于Jetpack Compose Flappy Bird游戲的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解關(guān)于MIUI 9沉浸式狀態(tài)欄的最新適配
由于各系統(tǒng)版本的限制,沉浸式狀態(tài)欄對系統(tǒng)有要求,本篇文章主要介紹了詳解關(guān)于MIUI 9沉浸式狀態(tài)欄的最新適配,非常具有實用價值,需要的朋友可以參考下2018-05-05android中soap協(xié)議使用(ksoap調(diào)用webservice)
kSOAP是如何調(diào)用ebservice的呢,首先要使用SoapObject,這是一個高度抽象化的類,完成SOAP調(diào)用。可以調(diào)用它的addProperty方法填寫要調(diào)用的webservice方法的參數(shù)2014-02-02兩分鐘讓你徹底明白Android Activity生命周期的詳解(圖文介紹)
本篇文章是對Android的生命周期進行了詳細的分析介紹,需要的朋友參考下2013-05-05Android ContentProvider基礎(chǔ)應(yīng)用詳解
ContentProvider是android四大組件之一。它是不同應(yīng)用程序之間交換數(shù)據(jù)的標(biāo)準(zhǔn)api,ContentProvider以某種uri的形式對外提供數(shù)據(jù),允許其它應(yīng)用程序?qū)ζ湓L問或者修改數(shù)據(jù)。本文將介紹ContentProvider的基礎(chǔ)應(yīng)用,感興趣的可以學(xué)習(xí)一下2021-12-12android上一個可追蹤代碼具體到函數(shù)某行的日志類
追蹤代碼到函數(shù)具體某行,這樣的功能,是每一個程序員都希望會有的,因為它可以幫助我們追蹤到某行代碼的錯誤,接下來介紹下android上一個可追蹤代碼到函數(shù)具體某行的日志類,希望對開發(fā)者有所幫助2012-12-12Android EventBus 3.0.0 使用總結(jié)(必看篇)
下面小編就為大家?guī)硪黄狝ndroid EventBus 3.0.0 使用總結(jié)(必看篇)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05android studio 使用Mocklocation虛擬定位
這篇文章主要介紹了android studio 使用Mocklocation虛擬定位總結(jié),本文分步驟給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12